Search code examples
c#wpflivecharts

LiveCharts - Connect across missing points


I am using LiveCharts to plot several line charts on the same graph. Some of the charts have missing data points.

Current graph with gaps:

enter image description here

I would like to connect across these gaps:

The goal if possible:

enter image description here

MainWindow.xaml

<Window x:Class="LiveChartsTest.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:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <lvc:CartesianChart Series="{Binding Series}">
            <lvc:CartesianChart.AxisX>
                <lvc:Axis Title="Date" Labels="{Binding Labels}"/>
            </lvc:CartesianChart.AxisX>
        </lvc:CartesianChart>
    </Grid>
</Window>

MainWindow.xaml.cs

using LiveCharts;
using LiveCharts.Wpf;
using System;
using System.Windows;

namespace LiveChartsTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // Create date labels
            Labels = new string[10];
            for (int i = 0; i < Labels.Length; i++)
            {
                Labels[i] = DateTime.Now.Add(TimeSpan.FromDays(i)).ToString("dd MMM yyyy");
            }

            Series = new SeriesCollection
                {
                    new LineSeries
                    {
                        Title = "Dataset 1",
                        Values = new ChartValues<double>
                        {
                            4,
                            5,
                            7,
                            double.NaN,
                            double.NaN,
                            5,
                            2,
                            8,
                            double.NaN,
                            6
                        }
                    }, 
                    new LineSeries
                    {
                        Title = "Dataset 2",
                        Values = new ChartValues<double>
                        {
                            2,
                            3,
                            4,
                            5,
                            6,
                            3,
                            1,
                            4,
                            5,
                            3
                        }
                    }
                };

            DataContext = this;
        }

        public SeriesCollection Series { get; set; }
        public string[] Labels { get; set; }


    }
}

Is there any way to do this with LiveCharts?


Solution

  • That looks more like a math trigonometry problem, where you have to figure out the coordinates of the missing points and add them to SeriesCollection so you can end up with that flat looing joints between the fragments.

    Consider the following explanatory picture based on your graph:

    enter image description here

    Between the X and Y we have to deduce two points, A and B (we know that we need two in between because we can deduce that from the interval between X and Y or we can simply count the NaNs in the initial collection). A & B Y coordinates could be easily deduced using what we already know and the angle α. We are looking to calculate the |BB'| and |AA'| sizes (added to the distance between y and the index should represent the final A and B)

    We know basically that: tan(α)= |BB'|/|B'Y| = |AA'|/|A'Y| = |XZ|/|ZY|

    For simplicity now let's assume that all intervals in the X-axis and Y-axis are equal 1, I will come back to this later.

    Now we do know |XZ|/|ZY|, (xz is the difference between x and y, and zy is basically how many NaNs there is in between), so we can easily calculate |BB'| and |AA'|:

    • |BB'| = (|XZ|/|ZY|) * |B'Y| (Note that |B'Y| is equal to one since it's a one unit interval)
    • |AA'| = (|XZ|/|ZY|) * |A'Y| (Note that |A'Y| is equal to two-unit interval )

    Here how a basic implementation to what was explained above looks like (the code should be self-explanatory):

    public partial class MainWindow : Window
    {
    
        public MainWindow()
        {
            InitializeComponent();
            // Create date labels
            Labels = new string[10];
            for (int i = 0; i < Labels.Length; i++)
            {
                Labels[i] = DateTime.Now.Add(TimeSpan.FromDays(i)).ToString("dd MMM yyyy");
            }
    
            var chartValues = new ChartValues<double>
            {
                4,
                5,
                7,
                double.NaN,
                double.NaN,
                5,
                2,
                8,
                double.NaN,
                6
            };
            Series = new SeriesCollection
            {
                new LineSeries
                {
                    Title = "Dataset 1",
                    Values = ProcessChartValues(chartValues)
                },
                new LineSeries
                {
                    Title = "Dataset 2",
                    Values = new ChartValues<double>
                    {
                        2,
                        3,
                        4,
                        5,
                        6,
                        3,
                        1,
                        4,
                        5,
                        3
                    }
                }
            };
    
            DataContext = this;
        }
    
        private ChartValues<double> ProcessChartValues(ChartValues<double> chartValues)
        {
            var tmpChartValues = new ChartValues<double>();
            double bornLeft =0, bornRight=0;
            double xz = 0, zy = 0, xy = 0;
            bool gapFound = false;
            foreach (var point in chartValues)
            {
                if (!double.IsNaN(point))
                {
                    if (gapFound)
                    {
                        // a gap was found and it needs filling 
                        bornRight = point;
                        xz = Math.Abs(bornLeft - bornRight);
                        for (double i = zy; i >0; i--)
                        {
                            tmpChartValues.Add((xz / zy) * i + Math.Min(bornLeft, bornRight));
                        }
    
                        tmpChartValues.Add(point);
                        gapFound = false;
                        zy = 0;
                    }
                    else
                    {
                        tmpChartValues.Add(point);
                        bornLeft = point;
                    }
                }
                else if(gapFound)
                {
                    zy += 1;
                }
                else
                {
                    zy += 1;
                    gapFound = true;
                }
            }
            return tmpChartValues;
        }
    
        public SeriesCollection Series { get; set; }
        public string[] Labels { get; set; }
    }
    

    And here the output:

    enter image description here

    Now coming back to our interval size, notice how the fragments aren't sharp because of our interval=1 assumption, but on the other hand, this assumption gave the graph some smoothness which is most likely what anyone would be after. If you still need to have sharp fragments, you could explore the LiveChart API to get that interval in pixels which I am not sure they offer (then simply multiply with xz and zy sizes) otherwise you could deduce it from the ActualWidth and ActualHight of the chart drawn area.

    As a final note, the code should be extended to handle the NaN points on the edges (you have to either neglect them or define a direction to which the graph should go).