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:
I would like to connect across these gaps:
The goal if possible:
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?
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:
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:
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).
Related
There is way to create chart with missing points for StepLineSeries?
There is sample for LineSeries https://lvcharts.net/App/examples/v1/wpf/Missing%20Points
but set double.NaN value for StepLineSeries throw error:
NaN "is an invalid value for property" Y1 ". '
Sample from link:
public MissingPointsExample()
{
InitializeComponent();
Series = new SeriesCollection
{
new LineSeries
{
Values = new ChartValues<double>
{
4,
5,
7,
8,
double.NaN,
5,
2,
8,
double.NaN,
6,
2
}
}
};
DataContext = this;
}
public SeriesCollection Series { get; set; }
xaml:
<Grid>
<lvc:CartesianChart Series="{Binding Series}">
</lvc:CartesianChart>
</Grid>
Unfortunatelly LiveCharts do not support missing points for StepLineSeries. Missing points feature exists only for LineSeries.
I have plotted the Cartesian chart using X and Y values, to mimic step chart (x values are not consistent). Is it possible to add custom x Labels for each step changed in Y value? Or similar labels like in a bar chart.
var values = new ChartValues<Point>();
Loop:
var point = new Point() { X = entry.DailyXFrom, Y = entry.YValue };
values.Add(point);
point = new Point() { X = entry.DailyXTo, Y = entry.YValue };
values.Add(point);
tempLabels.Add(entry.DailyVolume.ToString());
seriesCollectionDaily = new SeriesCollection
{
new LineSeries
{
Configuration = new CartesianMapper<Point>()
.X(point => point.X)
.Y(point => point.Y)
,
Fill = Brushes.Transparent,
Title = "Series",
Values = values,
PointGeometry = null,
LineSmoothness = 0
}
};
XAxis.Separator.IsEnabled = true;
XAxis.Labels = tempLabels.ToArray();
chart.DataContext = this;
<lvc:CartesianChart Name="chart" Grid.Row="2" Grid.ColumnSpan="2" Series="{Binding seriesCollectionDaily }" >
<lvc:CartesianChart.AxisX >
<lvc:Axis Name="XAxis" Title="" LabelsRotation="0" Foreground="Black" >
<lvc:Axis.Separator>
<lvc:Separator Name="LabelSeparator"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
</lvc:CartesianChart>
What I got:
This is what I'm trying to achieve. Is it possible?
You can add sections to your chart axis.
To draw vertical sections you have to define a SectionsCollection of AxisSection items and bind it to Axis.Sections of the x-axis.
By setting AxisSection.Value you can define the position on the axis. Use AxisSection.SectionWidth to define the width of the section. The default is 1, which draws a simple stroke when AxisSection.StrokeThickness is set.
The following example uses StepLineSeries to plot a step chart.
To give an example, it shows two vertical lines placed at the corresponding x-value: a simple line (SectionWidth = 1) and a section where SectionWidth > 1 (e.g., to highlight a range):
Data Model
public class DataModel : INotifyPropertyChanged
{
public DataModel()
{
this.SeriesCollection = new SeriesCollection
{
new StepLineSeries()
{
Configuration = new CartesianMapper<ObservablePoint>()
.X(point => point.X)
.Y(point => point.Y)
.Stroke(point => point.Y > 0.3 ? Brushes.Red : Brushes.LightGreen),
Values = new ChartValues<ObservablePoint>
{
new ObservablePoint(0, 5),
new ObservablePoint(20, 0),
new ObservablePoint(30, 5),
new ObservablePoint(40, 0),
new ObservablePoint(80, 5),
}
}
};
// Add two sections at x=20 and x=30
this.SectionsCollection = new SectionsCollection()
{
new AxisSection()
{
Value = 20,
Stroke = Brushes.Red,
StrokeThickness = 1
},
new AxisSection()
{
Value = 30,
SectionWidth = 50,
Stroke = Brushes.Red,
StrokeThickness = 1
}
};
}
public Func<double, string> LabelFormatter => value => $"{value}ms";
public SeriesCollection SeriesCollection { get; set; }
public SectionsCollection SectionsCollection { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Chart View
<Window>
<Window.DataContext>
<DataModel />
</Window.DataContext>
<CartesianChart Height="500"
Series="{Binding SeriesCollection}">
<CartesianChart.AxisX>
<Axis Sections="{Binding SectionsCollection}"
LabelFormatter="{Binding LabelFormatter}">
<Axis.Separator>
<Separator Step="10" />
</Axis.Separator>
</Axis>
</CartesianChart.AxisX>
<CartesianChart.AxisY>
<Axis>
<Axis.Separator>
<Separator Step="5" />
</Axis.Separator>
</Axis>
</CartesianChart.AxisY>
</CartesianChart>
</Window>
I started working with C# and Live Charts a few days ago.
Based on examples found in the internet, I made a very simple graph in order to understand the concepts behind it.
That is what I have (it is working perfectly fine):
public partial class Wdw_graph : Window
{
public SeriesCollection SeriesCollection { get; set; }
public string[] Labels { get; set; }
public Wdw_graph(List<dated_value> serie)
{
InitializeComponent();
SeriesCollection = new SeriesCollection();
ColumnSeries col_serie = new ColumnSeries {
Values = new ChartValues<double>(),
DataLabels = true };
Labels = new string[serie.Count];
for(int i = 0; i < serie.Count; i++)
{
col_serie.Values.Add(serie[i].value);
Labels[i] = serie[i].date;
}
SeriesCollection.Add(col_serie);
DataContext = this;
}
}
<Grid>
<lvc:CartesianChart Series="{Binding SeriesCollection}" >
<lvc:CartesianChart.AxisX>
<lvc:Axis Labels="{Binding Labels}" LabelsRotation="80">
<lvc:Axis.Separator>
<lvc:Separator IsEnabled="False" Step="1"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
<lvc:CartesianChart.AxisY>
<lvc:Axis Title="Sold Apps"></lvc:Axis>
</lvc:CartesianChart.AxisY>
</lvc:CartesianChart>
</Grid>
As you can see, I have 'SeriesCollection' and 'Labels' handled in the class code and them they are bound to the XAML.
I would like to know if it is possible to use the same approach to handle other graph elements, such as the 'AxisX'. If so, how can I do it?
In this post "Change the format of the axis tick labels in LiveCharts" there is a code that shows what I would like to do:
cartesianChart2.AxisX.Add(new Axis
{
Name = "xAxis",
Title = "DateTime",
FontSize = 22,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = _amountValues,
});
But I could not reproduce that. I can't figure out where the 'cartesianChart2' comes from.
Well, I learned how to refer to the XAML elements in the class code.
I added 'x:Name="something"' to the CartesianChart and removed everything else (not using 'Binding' anymore):
<Grid>
<lvc:CartesianChart x:Name="cartesian_chart">
</lvc:CartesianChart>
</Grid>
Then, in the class code I inserted the axis and serie:
public partial class Wdw_graph : Window
{
public Wdw_graph(List<dated_value> serie)
{
InitializeComponent();
cartesian_chart.AxisX.Add(new Axis
{
Name = "xAxis",
Title = "Date and Time",
FontSize = 20,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = serie.Count,
Labels = new String[serie.Count],
});
cartesian_chart.AxisY.Add(new Axis
{
Name = "yAxis",
Title = "Currency",
FontSize = 20,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = 10,
});
cartesian_chart.AxisX[0].Separator.Step = 1;
cartesian_chart.AxisX[0].LabelsRotation = 80;
ColumnSeries col_serie = new ColumnSeries {
Values = new ChartValues<double>(),
DataLabels = true };
for(int i = 0; i < serie.Count; i++)
{
col_serie.Values.Add(serie[i].value);
cartesian_chart.AxisX[0].Labels[i] = serie[i].date;
}
cartesian_chart.Series.Add(col_serie);
DataContext = this;
}
}
I really preferred to handle the graph directly from the code, instead of using the XAML.
Can you tell me what are the drawbacks of this approach?
Is it possible to have a different marker style for ranges among (XY-Axis) values ? For example. The marker style is shown in steel blue color here, Can I have markers above 15 and below 13 to show another color ?
Display:
Oxyplot has both a TwoColorLineSeries and a ThreeColorLineSeries
Here is an example with the ThreeColorLineSeries
public class MainViewModel
{
public MainViewModel()
{
Model = new PlotModel
{
Title = "Colouring example"
};
var series = new ThreeColorLineSeries();
// Random data
var rand = new Random();
var x = 0;
while (x < 50)
{
series.Points.Add(new DataPoint(x, rand.Next(0, 20)));
x+=1;
}
// Colour limits
series.LimitHi = 14;
series.LimitLo = 7;
// Colours
series.Color = OxyColor.FromRgb(255,0,0);
series.ColorHi = OxyColor.FromRgb(0,255,0);
series.ColorLo = OxyColor.FromRgb(0,0,255);
Model.Series.Add(series);
}
public PlotModel Model { get; set; }
}
I apologize for asking so many OxyPlot questions, but I seem to be really struggling with using the OxyPlot chart control.
My project is in WPF format so I was originally using a hosted WINFORMS chart and that worked like a charm and did absolutely everything I needed it to until I needed to overlay a WPF element on top of the hosted winform chart. Due to the "AirSpace" issue, I was not able to see the WPF element that I put on top of the hosted chart no matter what I did. That is when I decided to go with OxyPlot, which is giving me quite a few headaches so far.
Here is my origional question! that I asked over at CodePlex. I don't seem to be getting much help over there so I am trying again here.
My question is:
Does anyone know how to plot MULTIPLE LineSeries onto a Plot??
My approach so far:
I am taking a c# List array and adding a new copy of the LineSeries that holds new data to be plotted. My code:
// Function to plot data
private void plotData(double numWeeks, double startingSS)
{
// Initialize new Salt Split class for acess to data variables
Salt_Split_Builder calcSS = new Salt_Split_Builder();
calcSS.compute(numWeeks, startingSS, maxDegSS);
// Create the OxyPlot graph for Salt Split
OxyPlot.Wpf.PlotView plot = new OxyPlot.Wpf.PlotView();
var model = new PlotModel();
// Add Chart Title
model.Title = "Salt Split Degradation";
// Create new Line Series
LineSeries linePoints = new LineSeries() { StrokeThickness = 1, MarkerSize = 1, Title = numWeeks.ToString() + " weeks" };
// Add each point to the new series
foreach (var point in calcSS.saltSplitCurve)
{
DataPoint XYpoint = new DataPoint();
XYpoint = new DataPoint(point.Key, point.Value * 100);
linePoints.Format("%", XYpoint.Y);
linePoints.Points.Add(XYpoint);
}
listPointAray.Add(linePoints);
// Define X-Axis
var Xaxis = new OxyPlot.Axes.LinearAxis();
Xaxis.Maximum = numWeeks;
Xaxis.Minimum = 0;
Xaxis.Position = OxyPlot.Axes.AxisPosition.Bottom;
Xaxis.Title = "Number of Weeks";
model.Axes.Add(Xaxis);
//Define Y-Axis
var Yaxis = new OxyPlot.Axes.LinearAxis();
Yaxis.MajorStep = 15;
Yaxis.Maximum = calcSS.saltSplitCurve.Last().Value * 100;
Yaxis.MaximumPadding = 0;
Yaxis.Minimum = 0;
Yaxis.MinimumPadding = 0;
Yaxis.MinorStep = 5;
Yaxis.Title = "Percent Degradation";
model.Axes.Add(Yaxis);
// Add Each series to the
foreach (var series in listPointAray)
{
LineSeries newpoints = new LineSeries();
newpoints = linePoints;
model.Series.Add(newpoints);
}
// Add the plot to the window
plot.Model = model;
SaltSplitChartGrid.Children.Add(plot);
}
My code works the first time I press my "Graph Data" button, but fails on consecutive attempts with the following error:
The element cannot be added, it already belongs to a Plot Model
The following plot is the type of plot I would like to produce (it worked fine using WinForms Chart control):
I would like a new line with a new color to be plotted each time I run the method.
This is how I've created multi lines on an OxyPlot chart before, the key is creating a set of DataPoints for each series - called circlePoints & linePoints in the following example code, these are then bound to the CircleSeries and LineSeries:
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
StringFormat = Constants.MarketData.DisplayDateFormat,
Title = "End of Day",
IntervalLength = 75,
MinorIntervalType = DateTimeIntervalType.Days,
IntervalType = DateTimeIntervalType.Days,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.None,
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = "Value",
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.None
};
var plot = new PlotModel();
plot.Axes.Add(xAxis);
plot.Axes.Add(yAxis);
var circlePoints = new[]
{
new ScatterPoint(DateTimeAxis.ToDouble(date1), value1),
new ScatterPoint(DateTimeAxis.ToDouble(date2), value2),
};
var circleSeries = new ScatterSeries
{
MarkerSize = 7,
MarkerType = MarkerType.Circle,
ItemsSource = circlePoints
};
var linePoints = new[]
{
new DataPoint(DateTimeAxis.ToDouble(date1), value1),
new DataPoint(DateTimeAxis.ToDouble(date2), value2),
};
var lineSeries = new LineSeries
{
StrokeThickness = 2,
Color = LineDataPointColor,
ItemsSource = linePoints
};
plot.Series.Add(circleSeries);
plot.Series.Add(lineSeries);
Sucess!!!!
AwkwardCoder, thank you for the help, but I realized my mistake was just me having overlooked some things!
Here is the version of the code that works:
// Make a new plotmodel
private PlotModel model = new PlotModel();
// Create the OxyPlot graph for Salt Split
private OxyPlot.Wpf.PlotView plot = new OxyPlot.Wpf.PlotView();
// Function to plot data
private void plotData(double numWeeks, double startingSS)
{
List<LineSeries> listPointAray = new List<LineSeries>();
// Initialize new Salt Split class for acess to data variables
Salt_Split_Builder calcSS = new Salt_Split_Builder();
calcSS.compute(numWeeks, startingSS, maxDegSS);
// Create new Line Series
LineSeries linePoints = new LineSeries()
{ StrokeThickness = 1, MarkerSize = 1, Title = numWeeks.ToString() + " weeks" };
// Add each point to the new series
foreach (var point in calcSS.saltSplitCurve)
{
DataPoint XYpoint = new DataPoint();
XYpoint = new DataPoint(point.Key, point.Value * 100);
linePoints.Format("%", XYpoint.Y);
linePoints.Points.Add(XYpoint);
}
listPointAray.Add(linePoints);
// Add Chart Title
model.Title = "Salt Split Degradation";
// Add Each series to the
foreach (var series in listPointAray)
{
// Define X-Axis
OxyPlot.Axes.LinearAxis Xaxis = new OxyPlot.Axes.LinearAxis();
Xaxis.Maximum = numWeeks;
Xaxis.Minimum = 0;
Xaxis.Position = OxyPlot.Axes.AxisPosition.Bottom;
Xaxis.Title = "Number of Weeks";
model.Axes.Add(Xaxis);
//Define Y-Axis
OxyPlot.Axes.LinearAxis Yaxis = new OxyPlot.Axes.LinearAxis();
Yaxis.MajorStep = 15;
Yaxis.Maximum = calcSS.saltSplitCurve.Last().Value * 100;
Yaxis.MaximumPadding = 0;
Yaxis.Minimum = 0;
Yaxis.MinimumPadding = 0;
Yaxis.MinorStep = 5;
Yaxis.Title = "Percent Degradation";
//Yaxis.StringFormat = "{0.00} %";
model.Axes.Add(Yaxis);
model.Series.Add(series);
}
// Add the plot to the window
plot.Model = model;
plot.InvalidatePlot(true);
SaltSplitChartGrid.Children.Clear();
SaltSplitChartGrid.Children.Add(plot);
}
Here are the multiple things I did wrong:
In my foreach var series loop, I was adding the original series which had already been added and NOT the next var series in the list! (dumb!)
I was creating a new model each time I ran the method. This means that each time the code ran, I was adding a series that already existed in the previous model. (also dumb!)
I was creating a new plot every time and trying to add a model in the new plot that already belonged to a previous plot. (getting dummer..)
The plot was being added to the grid each time I ran the method, so I had to CLEAR the grid's children first before re-adding the same plot.
I was not refreshing the plot.
That was a lot of mistakes, but I worked through it. Hopefully this helps someone in the future. Also, I know I am not using ordinary data binding techniques, but this, at-least, works.
Final result:
Here is how you can achieve a similar result in XAML especially if you are using the MVVM approach.
ViewModel:
public ObservableCollection<DataPoint> DataPointList1 {get;set;}
public ObservableCollection<DataPoint> DataPointList2 {get;set;}
public ObservableCollection<DataPoint> DataPointList3 {get;set;}
public ObservableCollection<DataPoint> DataPointList4 {get;set;}
Using a for loop like below populates DataPointList1 to DataPointList4 with the appropriate datasets.
for (int i = 0; i < dataList.Count; i++)
{
DataPointList1 .Add(new DataPoint{dataList[i].XValue,dataList[i].YValue });
}
XAML:
xmlns:oxy="http://oxyplot.org/wpf"
<oxy:Plot LegendPlacement="Outside" LegendPosition="RightMiddle" Title="Your Chart Title" >
<oxy:Plot.Axes>
<oxy:LinearAxis Title="Your X-axis Title" Position="Bottom" IsZoomEnabled="True" />
<oxy:LinearAxis Title="Your Y-axis Title" Position="Left" IsZoomEnabled="True" />
</oxy:Plot.Axes>
<oxy:Plot.Series>
<oxy:LineSeries Title="Plot1" Color="Black" ItemsSource="{Binding DataPointList1 }"/>
<oxy:LineSeries Title="Plot2" Color="Green" ItemsSource="{Binding DataPointList2 }"/>
<oxy:LineSeries Title="Plot3" Color="Blue" ItemsSource="{Binding DataPointList3 }"/>
<oxy:LineSeries Title="Plot4" Color="Red" ItemsSource="{Binding DataPointList4 }"/>
</oxy:Plot.Series>
</oxy:Plot>