Introduction
I have a WPF application using OxyPlot
to display multiple line graphs with a common horizontal time axis.
I need to show a tracker consisting of a vertical cursor and a display of the values of all the variables at that time point.
Quick use of my favourite search engine revealed a number of people asking how to do that but no very helpful answers.
I was able to achieve almost what I wanted with a custom DefaultTrackerTemplate
and a converter to dig out extra data from the TrackerHitResult
that it gets as DataContext
. That worked, but it seemed a bit clunky and I was concerned about the amount of processing it required; it would probably be too slow on some machines. It also had some other disadvantages I don't need to bore you with. I decided I needed to find a better way.
Background
OxyPlot
is an excellent open-source cross-platform plotting library for .NET but the documentation is a bit sparse (to put it mildly). The capabilities of the software are some way ahead of the information about how to use them.
Using the Code
I wrote a new class derived from MouseManipulator
to use in place of the default TrackerManipulator
. In order to use it, you have to unbind the existing MouseKey
bindings in the plot controller (the ones that connect up the default TrackerManipulator
) and create a new binding:
var pc = Plot.ActualController;
pc.UnbindMouseDown(OxyMouseButton.Left);
pc.UnbindMouseDown(OxyMouseButton.Left, OxyModifierKeys.Control);
pc.UnbindMouseDown(OxyMouseButton.Left, OxyModifierKeys.Shift);
pc.BindMouseDown(OxyMouseButton.Left, new DelegatePlotCommand<OxyMouseDownEventArgs>(
(view, controller, args) =>
controller.AddMouseManipulator(view, new WpbTrackerManipulator(view), args)));
Here's the WpbTrackerManipulator
class:
public class WpbTrackerManipulator : MouseManipulator
{
private DataPointSeries currentSeries;
public WpbTrackerManipulator(IPlotView plotView)
: base(plotView)
{
}
public override void Completed(OxyMouseEventArgs e)
{
base.Completed(e);
e.Handled = true;
currentSeries = null;
PlotView.HideTracker();
}
public override void Delta(OxyMouseEventArgs e)
{
base.Delta(e);
e.Handled = true;
if (currentSeries == null)
{
PlotView.HideTracker();
return;
}
var actualModel = PlotView.ActualModel;
if (actualModel == null)
{
return;
}
if (!actualModel.PlotArea.Contains(e.Position.X, e.Position.Y))
{
return;
}
var time = currentSeries.InverseTransform(e.Position).X;
var points = currentSeries.ItemsSource as Collection<DataPoint>;
DataPoint dp = points.FirstOrDefault(d => d.X >= time);
if (dp.X != 0 || dp.Y != 0)
{
int index = points.IndexOf(dp);
var ss = PlotView.ActualModel.Series.Cast<DataPointSeries>();
double[] values = new double[6];
int i = 0;
foreach (var series in ss)
{
values[i++] = (series.ItemsSource as Collection<DataPoint>)[index].Y;
}
var position = XAxis.Transform(dp.X, dp.Y, currentSeries.YAxis);
position = new ScreenPoint(position.X, e.Position.Y);
var result = new WpbTrackerHitResult(values)
{
Series = currentSeries,
DataPoint = dp,
Index = index,
Item = dp,
Position = position,
PlotModel = PlotView.ActualModel
};
PlotView.ShowTracker(result);
}
}
public override void Started(OxyMouseEventArgs e)
{
base.Started(e);
currentSeries = PlotView?.ActualModel?.Series
.FirstOrDefault(s => s.IsVisible) as DataPointSeries;
Delta(e);
}
}
Notice that I'm also using a new WpbTrackerHitResult
class to package the values from all the series:
public class WpbTrackerHitResult : TrackerHitResult
{
public double[] Values { get; private set; }
[System.Runtime.CompilerServices.IndexerName("ValueString")]
public string this[int index]
{
get
{
return string.Format((index == 1 || index == 4) ?
"{0,7:###0 }" : "{0,7:###0.0#}", Values[index]);
}
}
public WpbTrackerHitResult(double[] values)
{
Values = values;
}
}
That makes it possible to bind to the values in the tracker template. As a bonus, you can get the values as double
s or as formatted string
s. Here's a partial example, just to show how it works:
...
<oxy:TrackerControl Position="{Binding Position}"
LineExtents="{Binding PlotModel.PlotArea}"
BorderBrush="Black" BorderThickness="1"
HorizontalLineVisibility="Hidden" >
<oxy:TrackerControl.Content>
<Grid Margin="4,0,4,7">
...
<TextBlock Grid.Column="1"
Text="{Binding ValueString[0]}" Margin="4,2" />
<TextBlock Grid.Column="2"
Text="{Binding Value[3]}" Margin="4,2" />
...
</Grid>
</oxy:TrackerControl.Content>
</oxy:TrackerControl>
Points of Interest
The code above makes several assumptions that are valid in my application. For example, there are exactly six series in the PlotModel
and they are all LineSeries
. It would be possible to make the code more generic (and I may yet do that) but I decided to leave it simple here for ease of understanding. More generic code would also be slower. I think this code is faster than the default tracker and it seems easily to keep up with fast mouse movement with six series to track.
History
- 29th January, 2016: First release