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.