Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multi-line Tracker for OxyPlot

0.00/5 (No votes)
29 Jan 2016 1  
How to show a tracker for multiple line graphs in OxyPlot

Sample multiline tracker

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
    {
    /// <summary>
    /// The current series.
    /// </summary>
    private DataPointSeries currentSeries;

    public WpbTrackerManipulator(IPlotView plotView)
        : base(plotView)
        {
        }

    /// <summary>
    /// Occurs when a manipulation is complete.
    /// </summary>
    /// <param name="e">
    /// The <see cref="OxyPlot.OxyMouseEventArgs" /> instance containing the event data.
    /// </param>
    public override void Completed(OxyMouseEventArgs e)
        {
        base.Completed(e);
        e.Handled = true;

        currentSeries = null;
        PlotView.HideTracker();
        }

    /// <summary>
    /// Occurs when the input device changes position during a manipulation.
    /// </summary>
    /// <param name="e">
    /// The <see cref="OxyPlot.OxyMouseEventArgs" /> instance containing the event data.
    /// </param>
    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);
        // Exclude default DataPoint.
        // It has insignificant downside and is more performant than using First above
        // and handling exceptions.
        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);
            }
        }

    /// <summary>
    /// Occurs when an input device begins a manipulation on the plot.
    /// </summary>
    /// <param name="e">
    /// The <see cref="OxyPlot.OxyMouseEventArgs" /> instance containing the event data.
    /// </param>
    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; }

    // can't use the default indexer name (Item) since the base class uses that for something else
    [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 doubles or as formatted strings. 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here