Livecharts Geared throws unhandled ArgumentOutOfRangeException when zoomed to specific position

2

I have modified the scrollable example to use Rachel Lim's VMMV navigation example and to fetch data from our database. I have also moved some code-behind logic to VM. Everything works fine with LiveCharts.ChartValues, but when using LiveCharts.Geared.GearedValues the library crashes when zoomed in/out to specific point.

The data has 6 hourly values with timestamp. I group and sum values by hour. Timestamp and values are never null and neither are calculated sums. I do not update data after the chart has been drawn.

If I fetch 1000 values(~1000/6 datapoints) from database, the library crashes when zoomed out approximately 5 times of data's range. If i fetch 10000 values(~10000/6 datapoints) the crash happens as soon as the user has navigated to usercontrol where the chart is. If i fetch 100000 values the crash happens when zooming in approximately to same minvalue & maxvalue.

However when using ChartValues instead of GearedValues or using just a few datapoints I can zoom in as close as I want and zoom out to DateTime.minvalue.

My view.xaml(Pretty much the example one but using ICommand RangeChangedCommand):

<UserControl 
 [ ....]
  xmlns:geared="clr-namespace:LiveCharts.Geared;assembly=LiveCharts.Geared">

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
        <RowDefinition Height="100"></RowDefinition>
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0"></TextBlock>
    <lvc:CartesianChart Grid.Row="1"
                        Zoom="X" 
                        DisableAnimations="True"
                        Hoverable="False">
        <lvc:CartesianChart.Resources>
            <Style TargetType="lvc:Separator">
                <Setter Property="StrokeThickness" Value="2.5"></Setter>
                <Setter Property="Stroke" Value="#E7E7E7"></Setter>
                <Style.Triggers>
                    <Trigger Property="AxisOrientation" Value="X">
                        <Setter Property="IsEnabled" Value="False"></Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </lvc:CartesianChart.Resources>
        <lvc:CartesianChart.Series>
            <geared:GLineSeries StrokeThickness="0" 
                                Values="{Binding Values}"
                                Fill="#2194F1"
                                AreaLimit="0"
                                PointGeometry="{x:Null}"
                                LineSmoothness="0"/>
        </lvc:CartesianChart.Series>
        <lvc:CartesianChart.AxisX>
            <lvc:Axis LabelFormatter="{Binding Formatter}" RangeChangedCommand="{Binding Axis_OnRangeChangedCommand}" 
                      MinValue="{Binding From, Mode=TwoWay}" MaxValue="{Binding To, Mode=TwoWay}"
                      Separator="{x:Static lvc:DefaultAxes.CleanSeparator}"/>
        </lvc:CartesianChart.AxisX>
    </lvc:CartesianChart>
    <lvc:CartesianChart Grid.Row="2" DisableAnimations="True" 
                        ScrollMode="X" 
                        ScrollHorizontalFrom="{Binding From, Mode=TwoWay}"
                        ScrollHorizontalTo="{Binding To, Mode=TwoWay}"
                        ScrollBarFill="#25303030"
                        DataTooltip="{x:Null}"
                        Hoverable="False"
                        Margin="20 10">
        <lvc:CartesianChart.Resources>
            <Style TargetType="lvc:Separator">
                <Setter Property="IsEnabled" Value="False"></Setter>
            </Style>
        </lvc:CartesianChart.Resources>
        <lvc:CartesianChart.Series>
            <geared:GLineSeries Values="{Binding Values}"
                                Fill="Silver"
                                StrokeThickness="0"
                                PointGeometry="{x:Null}"
                                AreaLimit="0"/>
        </lvc:CartesianChart.Series>
        <lvc:CartesianChart.AxisX>
            <lvc:Axis IsMerged="True" 
                      LabelFormatter="{Binding Formatter, Mode=OneTime}" 
                      Foreground="#98000000"
                      FontSize="22"
                      FontWeight="UltraBold"/>
        </lvc:CartesianChart.AxisX>
        <lvc:CartesianChart.AxisY>
            <lvc:Axis ShowLabels="False" />
        </lvc:CartesianChart.AxisY>
    </lvc:CartesianChart>
</Grid>

My VM.cs

class ScrollableVM : ObservableObject, IPageViewModel
{
    public string Name => "Scrollable";
    private double _from;
    private double _to;
    private Func<double, string> _formatter;
    private ICommand _axis_OnRangeChanged;
    public GearedValues<DateTimePoint> Values { get; set; }
    //public ChartValues<DateTimePoint> Values { get; set; }

    #region setget
    public double From
    {
        get { return _from; }
        set
        {
            SetProperty(ref _from, value);
        }
    }
    public double To
    {
        get { return _to; }
        set
        {
            SetProperty(ref _to, value);
        }
    }


    public Func<double, string> Formatter
    {
        get { return _formatter; }
        set
        {
            SetProperty(ref _formatter, value);
        }
    }
    #endregion

    public ScrollableVM()
    {
        var l = new List<DateTimePoint>();


        using (/***getting data from db***/)
        {
            var q =(/***getting data from db***/).Take(1000).ToList();

            var grouped = q.GroupBy(t => new DateTime(t.Stamp.Value.Year, t.Stamp.Value.Month, t.Stamp.Value.Day, t.Stamp.Value.Hour, 0, 0));

            foreach (var item in grouped)
            {
                l.Add(new DateTimePoint((DateTime)item.Key, (double)item.Sum(x => x.value)));
            }
        }
        //Crashes
        //quality doesn't affect crashing
        Values = l.AsGearedValues().WithQuality(Quality.High);

        ////Works
        //Values = new GearedValues<DateTimePoint>() { new DateTimePoint(DateTime.Now, 0), new DateTimePoint(DateTime.Now.AddHours(1), 1) , new DateTimePoint(DateTime.Now.AddHours(2), 2) };


        ////Works
        //Values = l.AsChartValues();

        From = Values.Min(x => x.DateTime).Ticks;
        To = Values.Max(x => x.DateTime).Ticks;
        Formatter = x => new DateTime((long)x).ToString("yyyy");
    }



    private void Axis_OnRangeChanged(RangeChangedEventArgs eventargs)
    {
        var currentRange = eventargs.Range;

        if (currentRange < TimeSpan.TicksPerDay * 2)
        {
            Formatter = x => new DateTime((long)x).ToString("t");
            return;
        }

        if (currentRange < TimeSpan.TicksPerDay * 60)
        {
            Formatter = x => new DateTime((long)x).ToString("dd MMM yy");
            return;
        }

        if (currentRange < TimeSpan.TicksPerDay * 540)
        {
            Formatter = x => new DateTime((long)x).ToString("MMM yy");
            return;
        }

        Formatter = x => new DateTime((long)x).ToString("yyyy");
    }

    public ICommand Axis_OnRangeChangedCommand
    {
        get
        {
            if (_axis_OnRangeChanged == null)
            {
                _axis_OnRangeChanged = new RelayCommand(a => Axis_OnRangeChanged((RangeChangedEventArgs)a));
            }

            return _axis_OnRangeChanged;
        }
    }


}

view.xaml.cs only has constructor with InitializeComponent()

Exception details:

 System.ArgumentOutOfRangeException
  HResult=0x80131502
  Message=Specified argument was out of the range of valid values.
Parameter name: index
  Source=WindowsBase
  StackTrace:
   at MS.Utility.FrugalStructList`1.Insert(Int32 index, T value)
   at System.Windows.Media.PathSegmentCollection.Insert(Int32 index, PathSegment value)
   at LiveCharts.Wpf.Points.HorizontalBezierPointView.DrawOrMove(ChartPoint previousDrawn, ChartPoint current, Int32 index, ChartCore chart)
   at LiveCharts.SeriesAlgorithms.LineAlgorithm.Update()
   at LiveCharts.ChartUpdater.Update(Boolean restartsAnimations, Boolean force)
   at LiveCharts.Wpf.Components.ChartUpdater.UpdaterTick(Boolean restartView, Boolean force)
   at LiveCharts.Wpf.Components.ChartUpdater.OnTimerOnTick(Object sender, EventArgs args)
   at System.Windows.Threading.DispatcherTimer.FireTick(Object unused)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at medidata.App.Main() in ....\source\repos\medidata\medidata\obj\Debug\App.g.cs:line 51

Versions:

  • LiveCharts 0.9.7.0
  • LiveCharts.Geared 1.2.8.2
  • LiveCharts.Wpf 0.9.7

Do I have something funky in my code / logic or is this a bug I should report? I didn't find quite similar problems reported by anyone else. Thank you in advance.

c#
wpf
livecharts
asked on Stack Overflow Mar 25, 2019 by kahko

2 Answers

0

Well it took a work day but I guess I found out what was so different with my implementation compared to the official example... THE LIST ORDER.

If I sort the original data by property on x-axis before calling AsGearedValues(), I get no crashes. This is not an issue in the free versions AsChartValues. I guess it has something to do with virtualization/optimization and AsGearedValues isn't smart enough to sort the list itself for future use. Also nowhere in documentation this is mentioned. I'll open an issue on github regarding this.

Simple demo:

MainWindow.xaml.cs

using LiveCharts.Defaults;
using LiveCharts.Geared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace bughunt
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public IGearedValues Values { get; set; }

        public MainWindow()
        {
            const int count = 1000;

            //Not sorting causes crashes when zooming deep in and back
            const bool SortBeforePassing = false;

            var r = new Random();

            var datepointlist = new List<DateTimePoint>();

            for (int i = 0; i < count; i++)
            {
                datepointlist.Add(new DateTimePoint(DateTime.Now.AddHours(-i), (double)r.Next(1, 150)));
            }
            if (SortBeforePassing)
            {
                Values = datepointlist.OrderBy(x => x.DateTime).AsGearedValues().WithQuality(Quality.High);
            }
            else
            {
                Values = datepointlist.AsGearedValues().WithQuality(Quality.High);
            }

            DataContext = this;
            InitializeComponent();
        }
    }
}

MainWindow.xaml

<Window x:Class="bughunt.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:bughunt"
        xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
        xmlns:geared="clr-namespace:LiveCharts.Geared;assembly=LiveCharts.Geared"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <lvc:CartesianChart Zoom="X" 
                            DisableAnimations="True"
                            Hoverable="False">

            <lvc:CartesianChart.Series>
                <geared:GLineSeries
                                    Values="{Binding Values}"
                                    PointGeometry="{x:Null}"/>
            </lvc:CartesianChart.Series>
        </lvc:CartesianChart>
    </Grid>
</Window>
answered on Stack Overflow Mar 26, 2019 by kahko
0

I had the exact same problem: I have Geared DateTime values on the x-axis the ArgumentOutOfRangeException was thrown when zooming. The problem was, when zooming out too much, the x-axis value formatter received a negative value, which is of course incorrect for DateTime (negative ticks). There's a PreviewRangeChanged event you can bind to and cancel zoom if needed, but I solved it differently:

// what caused the exception
XFormatter = val => new DateTime((long)val).ToString("HH:mm:ss");
// solve it by testing the value and return 0 if negative
XFormatter = val => val < 0.0 ? (new DateTime((long)0.0).ToString("HH:mm:ss")) : (new DateTime((long)val).ToString("HH:mm:ss"));

So if you just prevent the value formatter from getting a negative value, you completely get rid of this problem. This might not be the smartest solution, but it's simple and good enough for my case.

answered on Stack Overflow Jan 27, 2020 by Steve

User contributions licensed under CC BY-SA 3.0