Oxyplot : Selectable Point

While OxyPlot continues to be one of the attractive plotting library for .Net developers, there are times when you find longing for features that could make it even more better.

One of such feature is ability to select/highlight a point in a series. While there is a Selectable Property with LineSeries, it doesn’t quite let you highlight the Data Point.

However, this can be easily achieved using another series with has a single point (the point which was selected). I guess it is easier to show the code than describe it. So let’s go ahead and hit Visual Studio.

We would begin by defining couple of Custom Series. For the sake of example, I am considering LineSeries in this case. The first Custom Series we would define would be the series which displays the Selected Item.

public class SelectedLineSeries:LineSeries
{
}

As you can observe, it hardly does anything new. The purpose of the definition is to allow is to easily differenciate between our special Series from Line Series. This would be further clarified when introduce our second custom series, which denotes a series which can be selected.

public class SelectableLineSeries:LineSeries
{
public bool IsDataPointSelectable { get; set; }

public DataPoint CurrentSelection { get; set; }

public OxyColor SelectedDataPointColor { get; set; } = OxyColors.Red;

public double SelectedMarkerSize { get; set; }

public SelectableLineSeries()
{
SelectedMarkerSize = MarkerSize;
MouseDown += SelectableLineSeries_MouseDown;
}

private void SelectableLineSeries_MouseDown(object sender, OxyMouseDownEventArgs e)
{
if (IsDataPointSelectable)
{
var activeSeries = (sender as OxyPlot.Series.Series);
var currentPlotModel = activeSeries.PlotModel;
var nearestPoint = activeSeries.GetNearestPoint(e.Position, false);
CurrentSelection = nearestPoint.DataPoint;

currentPlotModel = ClearCurrentSelection(currentPlotModel);

var selectedSeries = new SelectedLineSeries
{
MarkerSize = MarkerSize + 2,
MarkerFill = SelectedDataPointColor,
MarkerType = MarkerType
};

selectedSeries.Points.Add(CurrentSelection);
currentPlotModel.Series.Add(selectedSeries);
currentPlotModel.InvalidatePlot(true);
}
}

private PlotModel ClearCurrentSelection(PlotModel plotModel)
{
while(plotModel.Series.Any(x=> x is SelectedLineSeries))
{
plotModel.Series.Remove(plotModel.Series.First(x=> x is SelectedLineSeries));
}
return plotModel;
}

}

The core functionality of the Class is defined in the Series Mouse Down event. This ensures that whenever a point is selected, a new series is added to the Parent PlotModel, which has a single Data Point ( same as the currently selected data point).

Well, that’s all our definition is about. Now we can go ahead and define our PlotModel to be used along with Series definition.

var random = new Random();
var collection = Enumerable.Range(1, 5).Select(x => new DataPoint(x, random.Next(100, 400))).ToList();
var series = new SelectableLineSeries
{
IsDataPointSelectable = true,
MarkerFill = OxyColors.Blue,
MarkerType = MarkerType.Square,
LineStyle = LineStyle.Solid,
Color = OxyColors.Blue,
ItemsSource = collection,
MarkerSize = 5
};

GraphModel = new PlotModel();

GraphModel.Axes.Add(new OxyPlot.Axes.LinearAxis
{
Position = Axes.AxisPosition.Bottom
});

GraphModel.Axes.Add(new OxyPlot.Axes.LinearAxis
{
Position = Axes.AxisPosition.Left
});

GraphModel.Series.Add(series);
GraphModel.InvalidatePlot(true);

That’s it. Hit F5 and test your Selectable Line Series. You can access the source code shown in the demo in my Github.

Oxyplot Selectable

Zoom Rectangle with OxyPlot

Let’s stick to OxyPlot for some more time. This time, we would attempt to change the color of Zoom Rectangle. For those who are new to Oxyplot, the control allows you to zoom in a particular location by permitting the user to draw rectangles in the graph. This particular rectangle is known as Zoom Rectangle.

You can enable the Zoom Rectangle by using the PlotController.

public PlotController ChartController { get; set; }

ChartController = new PlotController();
ChartController.BindMouseDown(OxyMouseButton.Left, PlotCommands.ZoomRectangle);

You can now bind the PlotController with your OxyPlot PlotView instance in XAML.

<oxy:PlotView Model="{Binding Model}" Controller="{Binding ChartController,UpdateSourceTrigger=PropertyChanged}"/>

While would enable the Zoom Rectangle, you would ideally would like to do a bit of Customization, for example, changing the appearance(color) of the rectangle.

You can do so by customizing the ZoomRectangleTemplate. Let’s change the default color of the Zoom Rectangle we just created.

<oxy:PlotView Model="{Binding Model}" Controller="{Binding ChartController,UpdateSourceTrigger=PropertyChanged}">
<oxy:PlotView.ZoomRectangleTemplate>
<ControlTemplate>
<Border BorderBrush="Black" BorderThickness="1">
<Rectangle Fill="Orange" />
</Border>
</ControlTemplate>
</oxy:PlotView.ZoomRectangleTemplate>
</oxy:PlotView>

That’s all you need. You now have your fully customized Zoom Rectangle.

Oxyplot

Oxyplot : Using Datagrid for tooltip

Oxyplot uses Tracker Controls for displaying tooltip. You could extend the display by Customizing the Default Tracker control. In this article, we will look deeper into that Tracker Control and aim to display a Grid in the Tracker Control.

For sake of example, let us assume a Fruit Seller, who wants to plot his sales per day on a Line Series. To begin with, let us create some random data to emulate the sales.

private enum eFruits
{
Apple,
Banana,
Mango,
Jackfruit
}
public IEnumerable<Fruit> Generate()
{
var random = new Random();
return Enumerable.Range(1, 10)
.SelectMany(x =>
{
return ((eFruits[])Enum.GetValues(typeof(eFruits))).Select(f =>
new Fruit
{
Name = f.ToString(),
ItemsSold = random.Next(0, 100),
Date = DateTime.Now.AddDays(x)
});
});
}

In his first attempt, he plots the Line Series, grouping the data by Date on which items where sold.

private void CreatePlotModel(IEnumerable<Fruit> totalSalesDetails)
{
var salesGrouping = totalSalesDetails.GroupBy(x => x.Date);
var yAxis = new LinearAxis
{
Position = AxisPosition.Left
};
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
Minimum = DateTimeAxis.ToDouble(salesGrouping.Min(x => x.Key)),
Maximum = DateTimeAxis.ToDouble(salesGrouping.Max(x => x.Key)),
};
DataPlotModel.Axes.Add(yAxis);
DataPlotModel.Axes.Add(xAxis);

var lineSeries = new LineSeries
{
MarkerFill = OxyColors.Blue,
MarkerType = MarkerType.Circle,
TrackerFormatString = "{0}\n{1}: {2:dd.MM.yy}\n{3}: {4:0.###}"
};
lineSeries.Points.AddRange(salesGrouping.Select(x => new DataPoint(DateTimeAxis.ToDouble(x.Key), x.Sum(c => c.ItemsSold))));
DataPlotModel.Series.Add(lineSeries);
}

XAML Part

<oxy:PlotView Model="{Binding DataPlotModel}"/>

This gave him his desired out as the following.

Initial Design

However, this left a lot to be desired. Currently the tooltip would display total sales per day. He knew he could decifer that from the chart itself and hence wanted to make use of tooltip for something more useful. He decided to display the details of sales, ie, number of each fruit sold during the day in the tooltip. This would give him a better insight into the sales.

Grid inside the Tooltip

While he knew the Oxyplot’s default Tracker control could be customized, he soon realized this wasn’t as straightward as he assumed it to be. As always, the problem lies in the details.

<oxy:PlotView Model="{Binding DataPlotModel}">
<oxy:PlotView.DefaultTrackerTemplate>
<ControlTemplate>
<oxy:TrackerControl>
<DataGrid ItemsSource="{Binding}"/>
</oxy:TrackerControl>
</ControlTemplate>
</oxy:PlotView.DefaultTrackerTemplate>
</oxy:PlotView>

The TrackerControl accepted a TrackerHitResult as its DataContext, displaying the TrackerHitResult.Text as tooltip, and there seemed no direct way for him to bind a collection of items to the TrackerControl if he was to customize it to include a DataGrid.

A bit of googling and with a tip from his mentor, he soon realized that there was a way instead. What if he can write a Converter that reads the required information from TrackerHitResult and returns a collection for DataGrid to bind to.

For this, he needs to extend the DataPoint so that it would contain the sub-collection information. This would ensure that the same would be available in the DataContext (TrackerHitResult)

public class ExtendedDataPoint : IDataPointProvider
{
public DateTime Date { get; set; }
public IEnumerable<Fruit> ItemsSold { get; set; }

public DataPoint GetDataPoint()
{
return new DataPoint(DateTimeAxis.ToDouble(Date), ItemsSold.Sum(x=>x.ItemsSold));
}
}

He can now rewrite his CreatePlotModel method to use the ExtendedDataPoint for adding points to the Line Series.

var lineSeries = new LineSeries
{
MarkerFill = OxyColors.Blue,
MarkerType = MarkerType.Circle,
};

lineSeries.ItemsSource = salesGrouping.Select(x => new ExtendedDataPoint
{
Date = x.Key.Date,
ItemsSold = x.ToList()
});

As you can notice, he has removed the TrackerFormatString as well, as he would not require it any longer. The next step would be to write the Converter required for transforming the TrackerHitResult to IEnumerable<Fruit>.

public class TrackerHitResultConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is TrackerHitResult data)
{
return (data.Item as ExtendedDataPoint).ItemsSold;
}
return null;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Since he had already defined his required sub-collection as a part of ExtendedDataPoint, the role of TrackerHitResultConverter became a lot easier. The last piece in the puzzle was to modify the Xaml to use the Converter.

<oxy:PlotView Model="{Binding DataPlotModel}">
<oxy:PlotView.DefaultTrackerTemplate>
<ControlTemplate>
<oxy:TrackerControl Position="{Binding Position}" Background="Transparent" BorderBrush="Transparent">
<DataGrid HeadersVisibility="None" ItemsSource="{Binding Converter={StaticResource TrackerHitResultConverter}}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Date,StringFormat=d}" Header="Date" />
<DataGridTextColumn Binding="{Binding Name}" Header="Fruit"/>
<DataGridTextColumn Binding="{Binding ItemsSold}" Header="Items Sold" />
</DataGrid.Columns>
</DataGrid>
</oxy:TrackerControl>
</ControlTemplate>
</oxy:PlotView.DefaultTrackerTemplate>
</oxy:PlotView>

With that in place, he was ready to Hit F5 and vola, his tooltip was ready.

Final output

Entire source code discussed in this article is available in my Github.

External Tracker for OxyPlot

Back with another hack with Oxyplot. This time around what I would like to achieve is to actively update a TextBlock with Tracker values as the user moves around the graph. This could be useful when you want to have an external mini-dashboard which would the tracker values as User works on the graph after turning off your original tracker.

For sake of demo, we would leave the OxyPlot Tracker on, so that the results could be verified with our external tracker.

Let’s set up the View first.

<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>

<oxy:PlotView Model="{Binding DemoPlotModel}"/>
<TextBlock Grid.Row="1" Text="{Binding CurrentTrackerValue}"/>
</Grid>

We will now define our PlotModel and define our Series.

DemoPlotModel = new PlotModel();
var xAxis = new LinearAxis
{
Position = AxisPosition.Left,
Maximum = 100,
Minimum = 0
};

var yAxis = new LinearAxis
{
Position = AxisPosition.Bottom,
Maximum = 100,
Minimum = 0
};

DemoPlotModel.Axes.Add(xAxis);
DemoPlotModel.Axes.Add(yAxis);

var listOfDataPoints = new DataPoint[]
{
new DataPoint(10,5),
new DataPoint(20,15),
new DataPoint(30,20),
new DataPoint(40,28),
new DataPoint(50,39),
new DataPoint(60,55),
new DataPoint(70,60),
new DataPoint(80,73),
new DataPoint(90,90),
};
var series = new LineSeries
{
ItemsSource = listOfDataPoints,
Color = OxyColors.Green,
MarkerType = MarkerType.Square,
MarkerSize = 5,
MarkerFill = OxyColors.DarkGreen
};

DemoPlotModel.Series.Add(series);

And now it is time our adding our little hack to update the Label’s bound property – CurrentTrackerValue.

DemoPlotModel.TrackerChanged += (sender, eventArgs) =>
{
CurrentTrackerValue = eventArgs.HitResult != null ? eventArgs.HitResult.Text : CurrentTrackerValue;
NotifyOfPropertyChange(nameof(CurrentTrackerValue));
};

public string CurrentTrackerValue { get; set; }

That’s it, now you have your external live tracker.

Rectangle Annotation With Rounded Corner in Oxyplot

It has been few months since I started playing around with Oxyplot, and it continues to impresses me. Having said that, there are times when certain challenges are thrown showing light on certain limitation of the tool. One of such limitation and a way to overcome is being discussed in this blog post.

Oxyplot supports a wide range of Annotations including Rectangle Annotation, Text Annotation, Image Annotation, Ellipse Annotation and Line Annotation among others. These by itself are extremely powerful and easy to use. However, recently I came across a business need where I would need to draw a Rectangle Annotation with Rounded Corner.

The Oxyplot library doesn’t allow us an in-build mechanism to do it. However, the good part is, that is not end of the road. We can create our own custom annotation. Before we hit code, it would be a good idea to discuss how we plan to create a RoundedCornerRectangleAnnotation.

Rounded Rectangle OxyPlot

The crux of the idea is to generate the rounded effect by drawing a set of two rectangle and 4 circle/ellipse. The Radius of the circles would be the intended corner radius of the final rectangle.

Okay, that’s enough for idea, let’s go ahead and write some code.

Part of Custom Annotation Class

 private void DrawRoundedRectangle(IRenderContext rc)
        {
            var xMin = double.IsNaN(this.MinimumX) || this.MinimumX.Equals(double.MinValue)
                            ? this.ClipByXAxis
                                ? this.XAxis.ActualMinimum
                                : this.XAxis.InverseTransform(this.PlotModel.PlotArea.Left)
                            : this.MinimumX;
            var xMax = double.IsNaN(this.MaximumX) || this.MaximumX.Equals(double.MaxValue)
                            ? this.ClipByXAxis
                                ? this.XAxis.ActualMaximum
                                : this.XAxis.InverseTransform(this.PlotModel.PlotArea.Right)
                            : this.MaximumX;
            var yMin = double.IsNaN(this.MinimumY) || this.MinimumY.Equals(double.MinValue)
                            ? this.ClipByYAxis
                                ? this.YAxis.ActualMinimum
                                : this.YAxis.InverseTransform(this.PlotModel.PlotArea.Bottom)
                            : this.MinimumY;
            var yMax = double.IsNaN(this.MaximumY) || this.MaximumY.Equals(double.MaxValue)
                            ? this.ClipByYAxis
                                ? this.YAxis.ActualMaximum
                                : this.YAxis.InverseTransform(this.PlotModel.PlotArea.Top)
                            : this.MaximumY;
            var xCornerRadius = (CornerRadius / (XAxis.Maximum - XAxis.Minimum)) * 100;
            var yCornerRadius = (CornerRadius / (YAxis.Maximum - YAxis.Minimum)) * 100;
            this.screenRectangleWithClippedXAxis = new OxyRect(this.Transform(xMin + xCornerRadius, yMin), this.Transform(xMax - xCornerRadius, yMax));
            this.screenRectangleWithClippedYAxis = new OxyRect(this.Transform(xMin, yMin + yCornerRadius), this.Transform(xMax, yMax - yCornerRadius));
            this.screenEllipseLeftBottom = new OxyRect(this.Transform(xMin, yMin), this.Transform(xMin + 2* xCornerRadius, yMin + 2* yCornerRadius));
            this.screenEllipseLeftTop = new OxyRect(this.Transform(xMin, yMax), this.Transform(xMin + 2 * xCornerRadius, yMax - 2 * yCornerRadius));
            this.screenEllipseRightBottom = new OxyRect(this.Transform(xMax, yMin), this.Transform(xMax - 2 * xCornerRadius, yMin + 2 * yCornerRadius));
            this.screenEllipseRightTop = new OxyRect(this.Transform(xMax, yMax), this.Transform(xMax - 2 * xCornerRadius, yMax - 2 * yCornerRadius));
            // clip to the area defined by the axes
            var clippingRectangle = OxyRect.Create(
                this.ClipByXAxis ? this.XAxis.ScreenMin.X : this.PlotModel.PlotArea.Left,
                this.ClipByYAxis ? this.YAxis.ScreenMin.Y : this.PlotModel.PlotArea.Top,
                this.ClipByXAxis ? this.XAxis.ScreenMax.X : this.PlotModel.PlotArea.Right,
                this.ClipByYAxis ? this.YAxis.ScreenMax.Y : this.PlotModel.PlotArea.Bottom);
            rc.DrawClippedRectangle(clippingRectangle,this.screenRectangleWithClippedXAxis,
                                            this.GetSelectableFillColor(this.Fill),
                                            this.GetSelectableColor(this.Stroke),
                                            this.StrokeThickness);
            rc.DrawClippedRectangle(clippingRectangle, this.screenRectangleWithClippedYAxis,
                                            this.GetSelectableFillColor(this.Fill),
                                            this.GetSelectableColor(this.Stroke),
                                            this.StrokeThickness);
            rc.DrawClippedEllipse(clippingRectangle, screenEllipseLeftBottom,
                                            this.GetSelectableFillColor(this.Fill),
                                            this.GetSelectableColor(this.Stroke),
                                            this.StrokeThickness);
            rc.DrawClippedEllipse(clippingRectangle, screenEllipseLeftTop,
                                this.GetSelectableFillColor(this.Fill),
                                this.GetSelectableColor(this.Stroke),
                                this.StrokeThickness);
            rc.DrawClippedEllipse(clippingRectangle, screenEllipseRightBottom,
                                this.GetSelectableFillColor(this.Fill),
                                this.GetSelectableColor(this.Stroke),
                                this.StrokeThickness);
            rc.DrawClippedEllipse(clippingRectangle, screenEllipseRightTop,
                                this.GetSelectableFillColor(this.Fill),
                                this.GetSelectableColor(this.Stroke),
                                this.StrokeThickness);
            if (!string.IsNullOrEmpty(this.Text))
            {
                var textPosition = this.GetActualTextPosition(() => this.screenRectangle.Center);
                rc.DrawClippedText(
                    clippingRectangle,
                    textPosition,
                    this.Text,
                    this.ActualTextColor,
                    this.ActualFont,
                    this.ActualFontSize,
                    this.ActualFontWeight,
                    this.TextRotation,
                    HorizontalAlignment.Center,
                    VerticalAlignment.Middle);
            }
        }

 

You can find the complete class code in Gist Link. The complete source code, including the WPF Wrapper can be found in the forked implementation at my GitHub.

PS:The implementation, of course has its own flaws. You cannot set Stroke for the Rounded Rectangle, hopefully, I would be able to workaround this issue soon.

Custom Tooltip in Oxyplot

Working with OxyPlot sometimes throw these interesting problems, mainly due to lack of documentation. One of the recent challenges involved creating a custom tooltip, which at the hindsight, was pretty straightforward – only if the developers had continued their effort in completing the documentation.

 

Alright back to out problem. Oxyplot, by default provides a neat little tooltip for the Data Points in the graph. On most occasions, these are more than enough. On others, you need to tweak the tooltip a bit more to include additional information.

 

If you browse through the Series Class (LineSeries, BarSeries etc), you would notice it has a property named TrackerFormatString. This is the key to unlocking the tooltip. For sake of example, we will examine the LineSeries in this post. By default, following is the format of TrackerFormatString

 

"{0}\n{1}: {2:0.###}\n{3}: {4:0.###}"

 

Where
{0} = Title of Series
{1} = Title of X-Axis
{2} = X Value
{3} = Title of Y-Axis
{4} = Y Value

Some of the basic customization can happen within the TrackerFormatString property itself, say, suppose you want format the values to display only 2 decimal places. This again would be sufficient to cover a lot of cases.

However, at times, you might be interested to display additional information. Unfortunately the DataPoint class has only two properties, which you could use for loading your data. This cripples you desire to add a third value associated with the DataPoint in addition to X & Y, especially if you notice that the DataPoint is a sealed class and Series.Points.Points.AddRange accepts only DataPoints as parameter.

Thankfully, Oxyplot developers has left another door open while closing the DataPoint class. It allows you to assign the Points to Series using the Series.ItemSource Property, which accepts an IEnumerable. The only criteria for your IEnumerable is to the type needs to implement IDataPointProvider.
Let’s go ahead and implement our demo class.

 

public class CustomDataPoint : IDataPointProvider
{
  public double X { get; set; }
  public double Y { get; set; }
  public string Description { get; set; }
  public DataPoint GetDataPoint() => new DataPoint(X, Y);

  public CustomDataPoint(double x,double y,string description)
  {
    X = x;
    Y = y;
    Description = description;
  }
}

var points = new CustomDataPoint[]
{
  new CustomDataPoint(5,12,"Info 1"),
  new CustomDataPoint(6,10,"Info 2"),
  new CustomDataPoint(7,9,"Info 3"),
  new CustomDataPoint(8,13,"Info 4"),
  new CustomDataPoint(9,14,"Info 5"),
  new CustomDataPoint(10,10,"Info 6")
};

And now we will use the ItemSource property to assign the points to Series. We will also use the TrackerFormatString to format our tooltip to display additional information.

var seriesVisible = new OxyPlot.Series.LineSeries();
seriesVisible.ItemsSource = points;
seriesVisible.TrackerFormatString = "X={2},\nY={4},\nAdditionalInfo={Description}";

That’s it and we have our additional information displayed in tooltip. Go and run your application

OxyPlotWithCustomTooltip

Oxyplot and DateTime Axis

Anyone who has just been introduced to OxyPlot and had to deal with a graph comprising of a Time/DateTime axis might find themselves in a corner. The first thing you would notice is that the DataPoint structure accepts only double and that means trouble, especially with a not-so-exhaustive documentation the tool supports. But if you keep persisting and look around a bit, you will soon notice that the OxyPlot developers has done a fair bit of job to ensure TimeAxis (and many more) are possible. The DateTimeAxis Class enables you to add a DateTime object to your PlotModel.

But prior to jumping to your Axes, you need to find a solution to add a DateTime to your DataPoint Collection. This is again made possible by the DateTimeAxis class and the ToDouble method.

var  dataPoints = new[]
{
    new DataPoint(DateTimeAxis.ToDouble(new DateTime(2018,1,1,7,23,0)),12),
    new DataPoint(DateTimeAxis.ToDouble(new DateTime(2018,1,1,8,23,0)),9),
    new DataPoint(DateTimeAxis.ToDouble(new DateTime(2018,1,1,10,23,0)),13)
};

Now let’s add the required axes.

MainGraphModel.Axes.Add(new DateTimeAxis()
{
  Maximum = DateTimeAxis.ToDouble(new DateTime(2018, 1, 1, 12, 23, 0)),
  Minimum = DateTimeAxis.ToDouble(new DateTime(2018, 1, 1, 6, 23, 0)),
  Position = AxisPosition.Bottom,
  IntervalType = DateTimeIntervalType.Hours,
  MinorIntervalType = DateTimeIntervalType.Hours
});

 

Oxyplot does look pretty good, only if they had better documentation.