Table of Contents

Charts

Stride.CommunityToolkit.Charts draws a chart in your scene: axes, grid, labels, a legend, and any number of curves, points, fills and trails, all as pixel-width strokes in a ShapeBatch so they keep their weight at any zoom or distance. A chart is three things:

  • Options - everything about how it looks and what range it shows. Live: change a value, the next frame applies it.
  • Series - what is plotted. Added with Plot, PlotParametric, AddLine, AddMarkers, AddArea, AddTrajectory; taken off with Remove or Clear.
  • Update(camera) - call it once a frame. Nothing draws without it.

The package is preview and not published yet: reference the project src/Stride.CommunityToolkit.Charts from your own. The two examples this page draws on are Charts 2D and Charts 3D.

1. An empty chart with a grid

A 2D compositor, a 2D camera with the pan-and-zoom controller, one chart added to the scene, and Update in the loop:

using Stride.CommunityToolkit.Charts;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.Compositing;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;

using var game = new Game();

Chart? chart = null;
CameraComponent? camera = null;

game.Run(start: Start, update: Update);

void Start(Scene scene)
{
    game.Add2DGraphicsCompositor(clearColor: new Color(250, 250, 250));
    camera = game.Add2DCamera().Get<CameraComponent>();
    game.Add2DCameraController();

    var options = ChartOptions.Light2D();

    options.Range.XMin = -5f;
    options.Range.XMax = 5f;
    options.Range.YMin = -4f;
    options.Range.YMax = 4f;

    chart = new Chart(game, options);
    chart.Root.Scene = scene;
}

void Update(Scene scene, GameTime time)
{
    if (chart is not null && camera is not null)
    {
        chart.Update(camera);
    }
}

ChartOptions.Light2D() is the paper look: dark axes, a grey major and minor grid, no glow, labels that keep their pixel size. ChartOptions.Glow3D() is the other preset, glowing lines on a dark ground, and the default when you pass nothing. Both are ordinary option objects; change whatever you like after picking one.

The grid, ticks and labels come from Range:

options.Range.TickStep = 0.5f;       // a tick, a label and a major grid line every 0.5
options.Range.MinorDivisions = 5;    // five minor cells per major one; 0 or 1 for none

options.Grid.Visible = true;         // Light2D has it on, Glow3D off
options.Grid.Color = new Color(190, 190, 190);
options.Grid.Width = 1.5f;           // pixels, like every width on a chart
options.Grid.MinorColor = new Color(228, 228, 228);

Titles and axis names go in Title and Axes; the axes' colours, width and tick length are there too:

options.Title.Text = "Charts 2D";
options.Axes.XTitle = "x";
options.Axes.YTitle = "y";
options.Axes.Width = 1.5f;
options.Axes.TickLength = 8f;

Labels.Mode chooses between Screen text, a font size in pixels that never changes with zoom, and World text, a height in chart units that scales with the chart. Light2D uses Screen, Glow3D uses World; Labels.Format is the number format of every tick.

2. Lines

Plot takes a function of x and samples it across the chart's x range. Colours come from Options.Series.Palette in turn unless you pass one:

chart.Plot(x => 2f * MathF.Sin(x), name: "sin");
chart.Plot(x => 0.15f * x * x - 3f, name: "parabola");
chart.Plot(MathF.Log, name: "ln");                          // starts where its domain does
chart.Plot(MathF.Tan, name: "tan", samples: 600);           // cut into branches at the asymptotes
chart.Plot(MathF.Cos, color: new Color(180, 180, 180), name: "cos");

Values that are not finite break the curve, values outside the y range are clipped to the edge, and a jump between two samples larger than a quarter of the chart's height is treated as an asymptote rather than joined. More samples follow a wiggly function more closely.

Width and glow are per series through a style; whatever the style leaves unset comes from Options.Series, live:

chart.Plot(MathF.Sin, style: new ChartSeriesStyle { Width = 1.25f });        // half the default 2.5 px
chart.Plot(MathF.Cos, style: new ChartSeriesStyle { Glow = 0f });            // no halo, whatever the chart's glow

options.Series.CurveWidth = 3f;   // every series without a Width of its own

Every series you add is returned, and it is what you Remove later. The name is what the legend shows; Options.Legend.Visible turns the legend off.

3. Shapes and point data

A parametric curve is a function of t returning a point; closed joins the last point back to the first, which is how a circle or any loop is drawn:

chart.PlotParametric(
    t => new Vector3(1.5f * MathF.Cos(t), 1.5f * MathF.Sin(t), 0f), 0f, MathUtil.TwoPi,
    name: "circle", samples: 96, closed: true);

Measured data or a hand-drawn shape is a list of points:

var points = new List<Vector3> { new(-4, -1, 0), new(-2, 2, 0), new(1, 0.5f, 0), new(3, 3, 0) };

chart.AddLine(points, name: "measured");                    // clipped to the ranges
chart.AddLine(points, name: "outline", closed: true);       // a polygon
chart.AddLine(points, name: "free", clip: false);           // may leave the chart

Markers are one × per point, sized in pixels, so they stay the same size at any zoom:

chart.AddMarkers(points, name: "samples");                  // Options.Series.MarkerSize, MarkerWidth
chart.AddMarkers(points, size: 12f, width: 2f, color: new Color(96, 66, 166));

A filled region is the area between a function and a baseline, or between two functions, over a stretch of x. It is translucent at Options.Series.AreaOpacity and drawn behind the curves, so the grid shows through it:

chart.AddArea(x => 2f * MathF.Sin(x), from: 0f, to: MathF.PI, name: "integral");
chart.AddArea(x => 2f * MathF.Sin(x), x => MathF.Cos(x), from: -2f, to: 2f, name: "between");

options.Series.AreaOpacity = 0.4f;

4. Animation

Everything is live, so animation is writing a value each frame. To animate a curve, keep what Plot returned and swap its function; it keeps its name, colour and legend row:

ChartCurve? wave = null;
var elapsed = 0f;

// in Start
wave = chart.Plot(x => MathF.Sin(x), color: new Color(0, 158, 150), name: "wave");

// in Update
elapsed += (float)time.Elapsed.TotalSeconds;
var k = 1.75f + 1.25f * MathF.Sin(elapsed * 0.9f);
wave.SetFunction(x => 1.5f * MathF.Sin(k * x));

A trajectory is a curve that grows one point at a time - the path of something moving. Add a point per frame; points outside the ranges are clipped, so the trail runs to the edge and resumes where the path comes back:

var trail = chart.AddTrajectory(capacity: 900, name: "throw");

// in Update, after moving the body
trail.Add(new Vector3(position.X, position.Y, 0f));

trail.Clear();       // start again, keeping the buffers
trail.Break();       // lift the pen: the next Add starts a new run

With rollOver: true a full trail drops its oldest point instead of ignoring the new one, which gives an oscilloscope trace. Count and Capacity say how full it is.

Options animate the same way. Panning is writing the range, glow is writing a number:

chart.Options.Range.XMin += speed * dt;      // the axes, grid, labels and curves follow
chart.Options.Range.XMax += speed * dt;

chart.Options.Series.Glow = 6f + 4f * MathF.Sin(elapsed);
chart.Options.Grid.Visible = !chart.Options.Grid.Visible;

With Range.FollowCamera the chart's ranges follow what the camera sees and tick steps are re-picked as you zoom, so the 2D controller's drag and wheel pan and zoom an endless chart:

chart.Options.Range.FollowCamera = true;

A cursor readout follows the mouse; Chart.CursorPosition is the same point in chart units, for your own code to read a value against:

chart.Options.Cursor.Visible = true;
chart.Options.Cursor.Format = "0.00";

if (chart.CursorPosition is { } p)
{
    // p.X, p.Y in chart units
}

5. 3D

A chart is 3D when its Z range has a spread. It gains a Z axis, its clipping becomes a box, and its grid can cover the floor and the back wall as well:

game.AddGraphicsCompositor(clearColor: new Color(16, 18, 28));
camera = game.Add3DCamera().Get<CameraComponent>();
game.Add3DCameraController();
game.AddDirectionalLight();

var options = ChartOptions.Glow3D();

options.Range.ZMin = -3f;
options.Range.ZMax = 3f;
options.Grid.Planes = ChartGridPlanes.XY | ChartGridPlanes.XZ;
options.Grid.Visible = true;
options.Axes.ZTitle = "z";

chart = new Chart(game, options);
chart.Root.Transform.Position = new Vector3(0f, 3f, 0f);
chart.Root.Scene = scene;

chart.FrameCamera(camera, padding: 0.12f);    // back the camera off until the whole chart fits; the default padding is 0.05

Curves at z = 0 draw exactly as they do flat; the third dimension is for a parametric curve or a trajectory that uses z:

chart.PlotParametric(
    t => new Vector3(2.5f * MathF.Cos(t), 2.5f * MathF.Sin(t), t / MathUtil.TwoPi - 2f),
    0f, 5f * MathUtil.TwoPi, name: "helix", samples: 400);

The Glow3D preset's halo is Series.Glow pixels wide at Series.GlowStrength of the stroke's brightness, added to the scene (Series.AdditiveGlow). Widen it for a showcase, set it to 0 for none. A curve that leaves the chart plane, such as the helix or a 3D trail, is a mesh rather than a stroke, so its glow is an emissive tint under bloom. Everything else is a stroke.

6. Sharing the batch, and cleaning up

The chart draws into a depth-tested ShapeBatch it creates for itself. To draw your own shapes into the same batch, create one and pass it; what you submit after Update sits over the curves:

var batch = game.AddShapeBatch(depthTest: true);

// in Update
chart.Update(camera, batch);
batch.DrawDisc(new Vector3(ball.X, ball.Y, 0.05f), Vector3.UnitZ, 0.12f, new Color(40, 40, 40));

chart.Remove(series) takes one series off, chart.Clear() all of them, and chart.Dispose() frees everything the chart owns, including its batch. Remove chart.Root from the scene first.

Options at a glance

Group What it holds Notes
Range XMin..ZMax, TickStep, MinorDivisions, FollowCamera A Z spread makes the chart 3D, decided when it is built
Axes XColor..ZColor, Width, TickLength, TickWidth, XTitle..ZTitle Pixels
Grid Visible, Planes, Color, Width, MinorColor, MinorWidth Pixels; Planes matters in 3D only
Labels Visible, Mode, FontSize, Height, Color, Format Screen uses FontSize, World uses Height
Legend Visible Top left, one row per series
Title Text, FontSize, Height Above the top edge
Series CurveWidth, Glow, GlowStrength, AdditiveGlow, MarkerSize, MarkerWidth, AreaOpacity, Palette Defaults for series without a style; live for those
Cursor Visible, Format, Radius, Glow Mouse readout; Chart.CursorPosition

Every width, length, size and glow is in pixels on a 100% display and scales with the display scale. Ranges are in chart units; scale chart.Root to change the chart's size in the world.