DevLog 20250610: Plotting in Divooka

BackerLeader posted Originally published at dev.to 4 min read

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;

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

Full Customizable OOP Functional Dataflow Compositional Plotting API

Summary of Available Plots in Divooka

See table on Wiki page.

References

If you read this far, tweet to the author to show them you care. Tweet a Thanks

More Posts

DevLog 20250611: Audio API Design for Divooka Glaze!

Methodox - Jun 11

A General Service Configuration Scheme in Graphical Context

Methodox - May 25

DevLog 20250711: Flappy Bird in Divooka! (Sneak Peak)

Methodox - Jul 11

DevLog 20250708: Procedural Context Visual Debugger in Divooka

Methodox - Jul 8

[Review] 20250620: The Plan to Supersede Excel

Methodox - Jun 20
chevron_left