Click here to Skip to main content
15,881,898 members
Articles / Programming Languages / C#
Tip/Trick

Multi-line Tracker for OxyPlot

Rate me:
Please Sign up or sign in to vote.
4.67/5 (3 votes)
29 Jan 2016CPOL2 min read 42.5K   11   9
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:

C#
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:

C#
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:

C#
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:

XML
...
<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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
England England
Started my career as an electronics engineer.

Started software development in 4004 assembler.

Progressed to 8080, Z80 and 6802 assembler in the days when you built your own computer from discrete components.

Dabbled in Macro-11 and Coral66 by way of a small digression.

Moved on to C, first on Z80s and then on PCs when they were invented.

Continued to C++ and then C#

Now working mostly in C# and XAML while maintaining about half a million lines of C++

Comments and Discussions

 
PraiseThank you Pin
Inna CA19-Feb-21 4:55
Inna CA19-Feb-21 4:55 
QuestionNo Code; no UnbindMouseDown method. Pin
Member 128056366-Oct-17 14:34
Member 128056366-Oct-17 14:34 
AnswerRe: No Code; no UnbindMouseDown method. Pin
Phil J Pearson17-Oct-17 0:34
Phil J Pearson17-Oct-17 0:34 
GeneralRe: No Code; no UnbindMouseDown method. Pin
Member 1280563616-Apr-20 22:44
Member 1280563616-Apr-20 22:44 
QuestionHave you ever tried putting an image in the tracker? Pin
Member 841647024-Oct-16 12:38
Member 841647024-Oct-16 12:38 
QuestionSource Code Pin
Nirav Savla30-Sep-16 2:59
Nirav Savla30-Sep-16 2:59 
AnswerRe: Source Code Pin
Phil J Pearson30-Sep-16 3:22
Phil J Pearson30-Sep-16 3:22 
GeneralRe: Source Code Pin
Nirav Savla10-Oct-16 2:56
Nirav Savla10-Oct-16 2:56 
QuestionNeed some clue for mono development Pin
Pi ng21-Feb-16 16:56
Pi ng21-Feb-16 16:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.