Overview
Plotting is one of those things people take for granted - only when you need it does its absence become painfully obvious. It's often viewed as "solved" - until it's time to implement your own.
The Design Principles
The goal of the plotting API in Divooka is to provide a very high-level, easy-to-use (ideally single-node) setup for common plot types: you just supply the source data, pick the plot type, behold and voila - you get the resulting chart. In the case of the Plotting
toolbox, the results are static images.
This scheme allows some fairly complex plot types, as seen here:
However, things can become tricky when we want to support advanced style configurations, for which the convention is to expose data on a node as direct inputs and style configurations as a separate Configurations input, as discussed in this blog article.
The real challenge is to support creating advanced custom plot types without sacrificing clarity - in which case we have to use OOP under the dataflow (functional programming) constraint:
For convenience and other practical reasons, we also want to support text-based methods like Mermaid - in our case, it's called Dhole.
Designing API for Custom Plot Types on Top of ScottPlot
We have chosen ScottPlot
as our underlying drawing backend, and we know it supports a host of features - now we need to design a Divooka API or wrapper of some sort so that it exposes the same feature set without exposing underlying ScottPlot types to avoid explicit dependencies.
ScottPlot 5.0 has a nice compositional API built in:
ScottPlot.Plot myPlot = new();
myPlot.Add.Signal(Generate.Sin(51));
myPlot.Add.Signal(Generate.Cos(51));
myPlot.Layout.Frameless();
myPlot.DataBackground.Color = Colors.WhiteSmoke;
And you can procedurally add elements with object instances:
ScottPlot.Plot myPlot = new();
ScottPlot.Plottables.LinePlot line = new()
{
Start = new Coordinates(1, 2),
End = new Coordinates(3, 4),
};
myPlot.Add.Plottable(line);
One way to create a wrapper library is to create wrappers for all types and Add.XXX
methods in Divooka:
// ScottPlot.PlottableAdder
namespace ScottPlot
{
public class PlottableAdder
{
public PlottableAdder(ScottPlot.Plot plot);
public ScottPlot.Plot Plot { get; }
public ScottPlot.IPalette Palette { get; set; }
public ScottPlot.Color GetNextColor(System.Boolean incrementCounter);
public ScottPlot.Plottables.Annotation Annotation(System.String text, ScottPlot.Alignment alignment);
public ScottPlot.Plottables.Ellipse AnnularEllipticalSector(ScottPlot.Coordinates center, System.Double outerRadiusX, System.Double outerRadiusY, System.Double innerRadiusX, System.Double innerRadiusY, ScottPlot.Angle startAngle, ScottPlot.Angle sweepAngle, Nullable<ScottPlot.Angle> rotation);
// ... more methods ...
public ScottPlot.Plottables.VerticalSpan VerticalSpan(System.Double y1, System.Double y2, Nullable<ScottPlot.Color> color);
}
}
Which, as you can see above, is a lot.
ScottPlot Plottable Types:
ScottPlot.IPlottable.cs
ScottPlot.Plottables.Annotation.cs
ScottPlot.Plottables.Arrow.cs
ScottPlot.Plottables.AxisLine.cs
ScottPlot.Plottables.AxisSpan.cs
ScottPlot.Plottables.BarPlot.cs
ScottPlot.Plottables.Benchmark.cs
ScottPlot.Plottables.BoxPlot.cs
ScottPlot.Plottables.Bracket.cs
ScottPlot.Plottables.Callout.cs
ScottPlot.Plottables.CandlestickPlot.cs
...
ScottPlot.Plottables.VerticalLine.cs
ScottPlot.Plottables.VerticalSpan.cs
ScottPlot.Plottables.ZoomRectangle.cs
Creating a wrapper for each of those is not too difficult - it's mechanical and can probably be sped up with ChatGPT, but the biggest concern is maintainability and the process can be error-prone during initial setup. A shortcut is to expose the ScottPlot namespace and types directly, since the API already sort of supports compositional use, but we don't want to create such explicit dependencies.
A workaround (or middle ground) is that in Divooka we can create a bunch of factory methods and indirectly expose the Plottables
as return values.
internal class CustomPlot
{
public record OtherConfigurations(string Title, string XAxisLabel, string YAxisLabel);
public static PixelImage GeneratePlot(int width, int height, IPlottable[] plottables, OtherConfigurations styles)
{
ScottPlot.Plot plot = new();
foreach (var item in plottables)
plot.Add.Plottable(item);
if (!string.IsNullOrEmpty(styles.Title))
plot.Title(styles.Title);
if (!string.IsNullOrEmpty(styles.XAxisLabel))
plot.Axes.Bottom.Label.Text = styles.XAxisLabel;
if (!string.IsNullOrEmpty(styles.YAxisLabel))
plot.Axes.Left.Label.Text = styles.YAxisLabel;
return plot.ConvertScottPlotToPixelImage(width, height);
}
}
However, this is not enough shielding - to vamp it up a level, we just need to define our own Plottable
.
public class Plottable
{
internal IPlottable Underlying;
public void SetProperty(string attribute, object value)
{
// Set actual property of the underlying object...
}
};
public static PixelImage GeneratePlot(int width, int height, Plottable[] plottables, OtherConfigurations styles)
{
ScottPlot.Plot plot = new();
foreach (var item in plottables)
plot.Add.Plottable(item.Underlying);
// ...
}
In addition to factory methods, we can provide a bunch of text-based WithXXX
methods to provide a functional re-configuration of data and plottable-specific properties:
public static Plottable WithData(Plottable original, double[] vector)
{
// ...
}
public static Plottable WithLegendText(Plottable original, string text)
{
// ...
}
The end result is that we wrote minimal code, avoided having to create a custom wrapper for every plottable, while still largely making use of existing ScottPlot components and exposing a fully functional API in Divooka for custom plotting.
Example - Advanced Plot Customization
In this case, we show how to customize a plot with OOP components:
Summary of Available Plots in Divooka
See table on Wiki page.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.