I have a WPF
application using LiveCharts
where I plot a LineGraph
with X-Axis being DateTime
. My end goal here is to achieve a 'two-way' zoom. That is, I have two DateTimePicker
controls (from WPF Toolkit) on my application that represent the minimum and maximum DateTime
of the currently displayed area of the graph, and if I use scroll wheel on the graph area to zoom in/out, the updated range should be reflected on the said controls, and (this is the part I'm struggling with) conversely, if I set min/max on the DateTimePicker
controls, the graph should zoom in/out accordingly.
My XAML
is pretty simple:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<lvc:CartesianChart Name="MyChart"
Series="{Binding SeriesCollection}"
Zoom="X">
<lvc:CartesianChart.AxisX>
<lvc:Axis LabelFormatter="{Binding Formatter}"
PreviewRangeChangedCommand="{Binding XRangeChangedCommand}"
MinValue="{Binding TimeStampMin, Mode=TwoWay}"
MaxValue="{Binding TimeStampMax, Mode=TwoWay}"/>
</lvc:CartesianChart.AxisX>
</lvc:CartesianChart>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<xceed:DateTimePicker Margin="4" Width="160" Name="dtpMinX"
Format="Custom" FormatString="yyyy/MM/dd HH:mm:ss"
Value="{Binding TimeStampMin, Mode=TwoWay}"/>
<xceed:DateTimePicker Margin="4" Width="160" Name="dtpMaxX"
Format="Custom" FormatString="yyyy/MM/dd HH:mm:ss"
Value="{Binding TimeStampMax, Mode=TwoWay}"/>
</StackPanel>
</Grid>
Here's my DataPoint
class:
public class DataPoint
{
public DataPoint() { }
public DataPoint(DateTime timeStamp, double value)
{
TimeStamp = timeStamp;
Value = value;
}
public double Value { get; set; }
public DateTime TimeStamp { get; set; }
}
And here's my PlotGraph()
method that does all the plotting work. I'll include my entire MainWindow()
code at the bottom of this post if you want to reproduce this application.
private void PlotGraph()
{
var mapper = Mappers.Xy<DataPoint>()
.X(dp => dp.TimeStamp.Ticks)
.Y(dp => dp.Value);
SeriesCollection = new SeriesCollection(mapper);
var lineSeries = new LineSeries
{
Values = DataPoints.AsChartValues(),
Fill = Brushes.Transparent
};
SeriesCollection.Add(lineSeries);
TimeStampMin = DataPoints.FirstOrDefault().TimeStamp;
TimeStampMax = DataPoints.LastOrDefault().TimeStamp;
Formatter = value => new DateTime((long)value).ToString("MM/dd/yy HH:mm:ss");
DataContext = this;
}
Now when I run this and zoom in/out using mouse, the updated end value of my time stamps will be reflected on the DateTimePicker
controls just fine. However if I try to set the value in the DateTimePicker
controls in order to zoom in/out, it won't co-operate. When I try, in my Output
window, I get the following two binding related error messages:
System.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property.; Value='10/09/2019 15:50:41' BindingExpression:Path=TimeStampMin; DataItem='MainWindow' (Name=''); target element is 'Axis' (Name=''); target property is 'MinValue' (type 'Double')
System.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property.; Value='10/09/2019 16:00:54' BindingExpression:Path=TimeStampMax; DataItem='MainWindow' (Name=''); target element is 'Axis' (Name=''); target property is 'MaxValue' (type 'Double')
This tells me that the problem is in binding, that Axis.MinValue
and Axis.MaxValue
are expecting a double whereas my TimeStampMin
and TimeStampMax
are, obviously, DateTime
objects. How would I do the conversion so that I can achieve the two-way zoom?
Here is my entire MainWindow
code if you want to reproduce it. I'm using MVVMLight
toolkit for commands etc., so you might have to get the NuGet
package if you want to run this as is.
Looks like some people can't access the link so here's the full code:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public SeriesCollection SeriesCollection
{
get;
set;
}
public Func<double, string> Formatter
{
get;
set;
}
private DateTime _timeStampMin;
public DateTime TimeStampMin
{
get
{
return _timeStampMin;
}
set
{
if (_timeStampMin == value)
return;
_timeStampMin = value;
OnPropertyChanged("TimeStampMin");
}
}
private DateTime _timeStampMax;
public DateTime TimeStampMax
{
get
{
return _timeStampMax;
}
set
{
if (_timeStampMax == value)
return;
_timeStampMax = value;
OnPropertyChanged("TimeStampMax");
}
}
public List<DataPoint> DataPoints
{
get;
set;
}
public RelayCommand<PreviewRangeChangedEventArgs> XRangeChangedCommand
{
get;
private set;
}
public MainWindow()
{
InitializeComponent();
XRangeChangedCommand = new RelayCommand<PreviewRangeChangedEventArgs>(e => XRangeChanged(e));
InitializeData();
PlotGraph();
}
private void InitializeData()
{
var now = DateTime.Now;
DataPoints = new List<DataPoint>{new DataPoint()
{Value = 1, TimeStamp = now.AddMinutes(1)}, new DataPoint()
{Value = 4, TimeStamp = now.AddMinutes(2)}, new DataPoint()
{Value = 9, TimeStamp = now.AddMinutes(3)}, new DataPoint()
{Value = 16, TimeStamp = now.AddMinutes(4)}, new DataPoint()
{Value = 25, TimeStamp = now.AddMinutes(5)}, new DataPoint()
{Value = 36, TimeStamp = now.AddMinutes(6)}, new DataPoint()
{Value = 49, TimeStamp = now.AddMinutes(7)}, new DataPoint()
{Value = 64, TimeStamp = now.AddMinutes(8)}, new DataPoint()
{Value = 81, TimeStamp = now.AddMinutes(9)}, new DataPoint()
{Value = 100, TimeStamp = now.AddMinutes(10)}, new DataPoint()
{Value = 11 * 11, TimeStamp = now.AddMinutes(11)}, new DataPoint()
{Value = 12 * 12, TimeStamp = now.AddMinutes(12)}, new DataPoint()
{Value = 13 * 13, TimeStamp = now.AddMinutes(13)}, new DataPoint()
{Value = 14 * 14, TimeStamp = now.AddMinutes(14)}, new DataPoint()
{Value = 15 * 15, TimeStamp = now.AddMinutes(15)}, new DataPoint()
{Value = 16 * 16, TimeStamp = now.AddMinutes(16)}, new DataPoint()
{Value = 17 * 17, TimeStamp = now.AddMinutes(17)}, new DataPoint()
{Value = 18 * 18, TimeStamp = now.AddMinutes(18)}, new DataPoint()
{Value = 19 * 19, TimeStamp = now.AddMinutes(19)}, new DataPoint()
{Value = 20 * 20, TimeStamp = now.AddMinutes(20)}, };
}
private void PlotGraph()
{
var mapper = Mappers.Xy<DataPoint>().X(dp => dp.TimeStamp.Ticks).Y(dp => dp.Value);
SeriesCollection = new SeriesCollection(mapper);
var lineSeries = new LineSeries{Values = DataPoints.AsChartValues(), Fill = Brushes.Transparent};
SeriesCollection.Add(lineSeries);
TimeStampMin = DataPoints.FirstOrDefault().TimeStamp;
TimeStampMax = DataPoints.LastOrDefault().TimeStamp;
Formatter = value => new DateTime((long)value).ToString("MM/dd/yy HH:mm:ss");
DataContext = this;
}
public void XRangeChanged(PreviewRangeChangedEventArgs e)
{
TimeStampMin = DateTime.FromBinary((long)e.PreviewMinValue);
TimeStampMax = DateTime.FromBinary((long)e.PreviewMaxValue);
}
}
Yes, you had a part of answer when you said the problem was between double and datetime for Min and Max value for Axis X:
You have two solutions: either using Converter, either separate the value between datepicker and min/max value: here the second solution:
in xaml file:
<lvc:CartesianChart.AxisX>
<lvc:Axis LabelFormatter="{Binding Formatter}"
PreviewRangeChangedCommand="{Binding XRangeChangedCommand}"
MinValue="{Binding TimeStampMinX, Mode=TwoWay}"
MaxValue="{Binding TimeStampMaxX, Mode=TwoWay}"/>
</lvc:CartesianChart.AxisX>
in cs.file:
private double _timeStampMinX;
public double TimeStampMinX
{
get
{
return _timeStampMinX;
}
set
{
if (_timeStampMinX == value)
return;
_timeStampMinX = value;
OnPropertyChanged("TimeStampMinX");
}
}
private double _timeStampMaxX;
public double TimeStampMaxX
{
get
{
return _timeStampMaxX;
}
set
{
if (_timeStampMaxX == value)
return;
_timeStampMaxX = value;
OnPropertyChanged("TimeStampMaxX");
}
}
private DateTime _timeStampMin;
public DateTime TimeStampMin
{
get
{
return _timeStampMin;
}
set
{
if (_timeStampMin == value)
return;
_timeStampMin = value;
TimeStampMinX = value.Ticks;
OnPropertyChanged("TimeStampMin");
}
}
private DateTime _timeStampMax;
public DateTime TimeStampMax
{
get
{
return _timeStampMax;
}
set
{
if (_timeStampMax == value)
return;
_timeStampMax = value;
TimeStampMaxX = value.Ticks;
OnPropertyChanged("TimeStampMax");
}
}
the link between DateTime and double value is the DateTime.Ticks.
the solution with a converter:
The class converter:
public class DateTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((DateTime)value).Ticks;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
the integration of converter in xaml file (change the namespace WpfApp2 with your namespace)
xmlns:dc="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<dc:DateTimeConverter x:Key="DateTimeConverter"></dc:DateTimeConverter>
</Window.Resources>
:
:
<lvc:CartesianChart.AxisX>
<lvc:Axis LabelFormatter="{Binding Formatter}"
PreviewRangeChangedCommand="{Binding XRangeChangedCommand}"
MinValue="{Binding TimeStampMin, Mode=TwoWay,Converter={StaticResource DateTimeConverter}}"
MaxValue="{Binding TimeStampMax, Mode=TwoWay,Converter={StaticResource DateTimeConverter}}"/>
</lvc:CartesianChart.AxisX>
in this case no need to separate the binding value, the converter does the job.