Skip to content

Chart and Data Visualization with Bamboo

bamboo (scenerystack/bamboo) is SceneryStack's charting library — it renders datasets as line/bar/scatter/area plots, draws axes and gridlines, and does it all through one shared coordinate object every piece of a chart is built around. This page is a subsystem-level tour of how the pieces fit together; each linked API page documents one class in depth.

ChartTransform: the object everything else is built on

Every bamboo Node — every plot type, every axis, every gridline set — takes a ChartTransform in its constructor and rebuilds itself whenever that transform's changedEmitter fires. ChartTransform is not itself a Node — it's a plain object holding the chart's model-coordinate ranges (modelXRange, modelYRange) and view-coordinate size (viewWidth, viewHeight), plus the conversion math between the two:

ts
import { ChartTransform, ChartRectangle, LinePlot, AxisArrowNode } from 'scenerystack/bamboo';
import { Orientation } from 'scenerystack/phet-core';
import { Range, Vector2 } from 'scenerystack/dot';

const chartTransform = new ChartTransform( {
  viewWidth: 400,
  viewHeight: 200,
  modelXRange: new Range( 0, 10 ),
  modelYRange: new Range( -1, 1 )
} );

const chartRectangle = new ChartRectangle( chartTransform, { stroke: 'gray' } );
const xAxis = new AxisArrowNode( chartTransform, Orientation.HORIZONTAL );

const dataSet: ( Vector2 | null )[] = [];
for ( let x = 0; x <= 10; x += 0.1 ) {
  dataSet.push( new Vector2( x, Math.sin( x ) ) );
}
const linePlot = new LinePlot( chartTransform, dataSet, { stroke: 'blue' } );

// A resize (or panning/zooming the chart) is one call, and every attached Node redraws together.
chartTransform.setViewWidth( 600 );

This is the single most important thing to internalize about bamboo: a chart is a loose collection of independent Nodes that happen to share one ChartTransform, not one monolithic "Chart" class you configure. Adding a second plot type to the same chart is just constructing another Node against the same chartTransform — there's no chart-level API to register plots with.

The plot Node family

Each plot type is a distinct Node subclass specialized for one data shape — pick based on what the dataset actually represents, not by look alone:

PlotDataset shapeUse it for
LinePlot(Vector2 | null)[], connected by straight segmentsA sampled continuous function or time series; null entries create gaps for missing/discontinuous data
AreaPlotSame shape as LinePlotThe same kind of data as LinePlot, shaded down to a baseline — cumulative quantities, or emphasizing magnitude rather than just the curve's shape
BarPlotVector2[], one bar per point, numeric x onlyDiscrete quantities at discrete x-values, growing from a shared baseline
ScatterPlotVector2[], one circle per pointUnconnected discrete data points — measurements, samples — where connecting them with lines would imply a relationship that isn't there
LinearEquationPlotA slope m and intercept b, no dataset at allA reference/threshold/best-fit line spanning the chart's entire model range, not a curve through sampled points

All five extend either scenery's Path or Node and rebuild their drawn geometry in an internal update(), called on construction and whenever chartTransform.changedEmitter fires — so once a plot is constructed against a transform, updating the transform (a resize, a pan) or calling the plot's own setDataSet() is all that's needed to keep it visually correct; nothing needs to be manually re-laid-out.

Axes and gridlines: decoration, not structure

AxisLine/AxisArrowNode draw the x/y axis lines themselves (plain or arrow-tipped), and GridLineSet, TickMarkSet, and TickLabelSet draw the repeating reference lines, tick marks, and numeric tick labels behind or alongside the data, all at a fixed model-coordinate spacing. All of these are, like the plots, independent Nodes constructed against the same ChartTransform — a chart with no axis or gridline Nodes at all is still a perfectly valid, if sparse, bamboo chart; they're additive decoration, not something a plot depends on to render correctly.

Composing a full chart

A typical bamboo chart is a Node (or the ScreenView itself) with several bamboo Nodes as children, all built from one ChartTransform, layered in the usual scenery stacking order — background/gridlines first, data plots in the middle, axes and labels on top:

ts
import { GridLineSet } from 'scenerystack/bamboo';

const verticalGridLines = new GridLineSet( chartTransform, Orientation.VERTICAL, 1, {
  stroke: 'lightGray'
} );

const chartNode = new Node( {
  children: [
    chartRectangle,       // background + border
    verticalGridLines,    // reference gridlines, behind the data
    linePlot,             // the data itself
    xAxis                 // axis line/arrow on top
  ]
} );

Panning or zooming the whole chart is then a single chartTransform.setModelXRange( newRange ) (or setViewWidth/setViewHeight for a resize) — every child Node listening to changedEmitter redraws itself in response, with no coordination code needed between them.

Large datasets: CanvasLinePlot instead of LinePlot

Every plot type above extends an ordinary scenery Path/Node, which means a dataset large enough (many thousands of points, redrawn every frame) can start to show up as real overhead — one scenery drawable's bookkeeping cost, multiplied by every point. CanvasLinePlot is bamboo's answer for that specific case: it paints the same kind of (Vector2 | null)[] dataset as LinePlot, but directly to a CanvasRenderingContext2D via a shared ChartCanvasNode, rather than building a scenery Shape. Reach for it only once profiling (see Performance and Profiling) actually shows LinePlot's per-point overhead as the bottleneck — for anything smaller, LinePlot's automatic redraw wiring and ordinary Path semantics (hit-testing, easy composition with other Nodes) are simpler to work with.

Dispose a chart's Nodes the same as any dynamically-created view

Every bamboo Node holds a listener on its ChartTransform's changedEmitter, which its own dispose() removes — but if a chart itself is created and destroyed dynamically (not for the sim's lifetime), the usual Dispose and Memory Management discipline still applies: dispose each plot/axis Node (and the ChartTransform itself, via its own dispose()) when the chart is torn down, the same as any other transient view.

Where to go next