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.

kahko
  • 31
  • 5
  • I think the issue is with following line : l.Add(new DateTimePoint((DateTime)item.Key, (double)item.Sum(x => x.value))); I think the zooming may be making the sum larger than the graph y size and giving an overflow. } – jdweng Mar 25 '19 at 12:58
  • The VM constructor is only called once at the startup. Zooming doesn't cause Values to change or re-loaded from database. – kahko Mar 25 '19 at 13:05
  • I kind of agree, but it is not obvious what is happening. Is it possible the timer is going off while the chart is getting update and causing the exception? The error indicates the chart is being updated but there is not data in the chart. Normally on a change event you test the number of row in object and if it <= 0 then return from event. – jdweng Mar 25 '19 at 13:35
  • Thank you for your suggestions. I think it should be causing the same exception with non-geared ChartValues as well if that was the case, right? Also no breakpoints are being hit in the constructor. – kahko Mar 25 '19 at 13:40
  • I don't know the number of points between geared and non-geared. If it is a timing issue the problem may be occurring when you have more points. Still you do not want to update while the data is being changed. So I would stop the timer when updating. I would also check if you have rows in the object before plotting as part of robust design. – jdweng Mar 25 '19 at 13:45
  • I do not update the data after it's been initialized. Are you talking about some internal timers livecharts-library is using? Also I'm getting same error if I generate the data myself with `var r = new Random(); for(int i = 0; i<1000; i++) { l.Add(new DateTimePoint((DateTime)DateTime.Now.AddHours(-i), (double)r.Next(1,150))); } Values = l.AsGearedValues().WithQuality(Quality.High);` – kahko Mar 25 '19 at 13:50
  • The exception shows following : at LiveCharts.Wpf.Components.ChartUpdater.OnTimerOnTick(Object sender, EventArgs args) – jdweng Mar 25 '19 at 13:54
  • [ChartUpdater.cs](https://github.com/Live-Charts/Live-Charts/blob/master/WpfView/Components/ChartUpdater.cs) looks to me it's just about drawing and updating the view(scrolling, animations) and I have no control on the timer. – kahko Mar 25 '19 at 14:04
  • Make chart invisible when updating : if (!force && !wpfChart.IsVisible && !wpfChart.IsMocked) return; You can always add a Timer Off method to code. – jdweng Mar 25 '19 at 14:12
  • I think I found the bug. There are a lot of overrides for "override void DrawOrMove". The one giving error is in the class HorizontalBezierPointView. the error is on these two lines Container.Segments.Remove(Segment); Container.Segments.Insert(index, Segment); I think index is -1 which would give the out-of-range error. When a control is being constructed the index is always set to -1. A control is set to 0 when the columns are added just before rows get added. So if there are no columns then the index is zero and deleting would make -1.Maybe only do when !IsNew. – jdweng Mar 25 '19 at 15:00
  • @jdweng I found what was causing this. Kind of interesting how simple it was. Check the answer if you are interested. Thanks for your time. – kahko Mar 26 '19 at 09:36

2 Answers2

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>
kahko
  • 31
  • 5
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.

Steve
  • 232
  • 2
  • 10