DEV Community

Cover image for DevLog 20250610: Plotting in Divooka

DevLog 20250610: Plotting in Divooka

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.

IBasic Setup

This scheme allows some fairly complex plot types, as seen here:

Population Pyramid Example

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:

Custom Plot Concept

For convenience and other practical reasons, we also want to support text-based methods like Mermaid - in our case, it's called Dhole.

Dhole Setup

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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)
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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:

Full Customizable OOP Functional Dataflow Compositional Plotting API

Summary of Available Plots in Divooka

See table on Wiki page.

References

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.