api reference
The charts module
createChart, the chart types and their options, the axes, annotations and tooltips, and the geographic packs.
API reference › The charts module
All 13 pages Everything on one page → Developer guide →
The charts module
modules/charts draws thirty-five chart types from the grid's own data. It is optional and imports nothing from the grid (the grid is handed in) so the bundle carries the drawing and none of the grid, and a page that never charts never loads it.
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
const chart = createChart({
grid, // the grid to read
container: '#revenue', // an element or a selector
type: 'bar',
x: 'region', // the category column
y: 'revenue', // the measure column
});
A chart reads the grid's filtered rows. Filter, sort or edit the grid and every chart bound to it redraws on the next frame: there is nothing to subscribe to and nothing to keep in step. A chart of rows the user cannot see would be describing a different data set.
The types
| Family | Types | Takes |
|---|---|---|
| Cartesian | line, step, area, rangeArea, bar, horizontalBar, waterfall, scatter, bubble | x, y, optional series |
| Two axes | combo, pareto | x, measures |
| Distribution | histogram, boxplot | y alone |
| Matrix | heatmap | x, y, series |
| Part to whole | pie, donut, sunburst, treemap | x, y; or the grid's grouping, see below |
| Specialist | radar, gauge, funnel, candlestick | varies; candlestick takes four measures in open, high, low, close order |
| Geographic | geomap | x as an ISO code, y as the value |
| Flow | sankey, chord, network | source, target, y; a network also takes nodes - see Network diagrams |
| Over time | stream, marimekko, violin, gantt | varies; gantt takes label, start, end |
A chart given data it cannot draw (a candlestick with three measures rather than four) says so on the chart rather than drawing nothing, because a chart that silently draws nothing is indistinguishable from one that is broken.
Hierarchical data. The part-to-whole types read nested input from the grid's own grouping, not from the spec: with grid.columns.group(['region', 'product']) in place, the tree is that grouping, one level per grouped column in that order, and x is not consulted; depth caps how many levels are read. On a flat grid, x is the single level. What each type draws of that tree: a pie or donut draws the top level; a sunburst draws every level as a ring, each segment its share of the segment inside it, and names a segment on any ring where the name fits, leaving it unnamed where it does not; a treemap nests: children inside their parent's tile, each branch with a header band naming it and padding round its children, to the depth the tree has. A child whose tile would be smaller than a line of text is not drawn and its parent's tile stands for it, so the levels drawn are the levels that can be read; a small group at the top level is always drawn, as a labelled tile with no children inside it. drill descends the tree on click, on either: clicking any tile or arc, at any depth, makes that node the root - a tile nested two levels down, or a segment on the outer ring, not only the top level - and the drill event carries the full path of labels from the root to it. ascend() comes back out a level at a time along the same path.
The spec
| Key | Type | Description |
|---|---|---|
| grid | Grid | Required. The grid to read. |
| container | Element | string | Required. Where to draw. |
| type | string | One of the thirty above. |
| x / y | string | Category and measure columns. x is one column id: an array there is not a nesting instruction and warns once, naming the option and what it accepts; nest by grouping the grid instead (see Hierarchical data above). |
| series | string | Splits the measure into one series per distinct value. |
| measures | object[] | {col, fn, type, axis}: several measures at once, each reduced by an aggregation. fn is one of sum, avg (alias mean), min, max, count, countValues, first, last; it defaults to sum. An fn that is none of these is a mistake, not a silent sum: it warns once, naming the value and the supported set, and falls back to sum so the chart still draws. |
| title | string | Drawn above the plot. |
| scheme | string | string[] | A named scheme or your own colours. schemeNames() lists the built-in ones, including a colour-blind-safe palette. |
| legend | boolean | object | position, and isolate so a click shows only that series, which is what a reader with eight series wants, and what plain toggling makes them do in seven clicks. |
| labels | boolean | object | Values beside each mark. position, format, minGap. A label that would overlap one already placed is dropped rather than drawn over it. |
| axis | object | Titles, tick density and formatting per axis. |
| reference | object[] | Horizontal lines: {value, label}. |
| multiples | string | One chart per distinct value of this column, on a shared scale. |
| buckets | number | Histogram bins. Twelve by default. |
| canvas | boolean | number | Draw to canvas past this many points, for a dense scatter. |
| subtitle | string | A second line under the title. |
| footnote | string | A note under the plot, a source, a caveat, a unit. |
| emptyText | string | What to show when the binding produces nothing. Said rather than left blank, because an empty plot and a broken one look identical. |
| fit | boolean | 'line' | A least-squares line through a scatter or bubble chart, one per series. true draws it with its R²; 'line' draws the line alone. Only where the x axis is numeric: on a band scale a slope would be a slope through the order the categories happened to be listed in. |
| trend | boolean | string | ChartTrend | array | Trend and forecast overlays: a linear least-squares line, a movingAverage, or exponential smoothing, one per series. true draws a single linear trend; a method name or a { method, forecast, window, kind, alpha, beta } object configures one; an array draws several. The maths matches the core stats engine to the last digit (a parity test asserts it) but is computed locally to keep the charts bundle lean. For the linear method, forecast: n projects the line n steps past the data as a dashed forecast; a moving average and a smoothed level have no slope to project, so forecast is ignored for them and the fact is said in the accessible description. The trend layer (the fitted line, the forecast line, its band and the R² label) may extend past the plot into the chart's margin, but it is bounded by the chart box, the chart's own <svg>: a forecast that reaches past the chart's edge is cut off there rather than painted over the content beside it. |
| error | boolean | object | Whiskers showing the uncertainty in each mark, computed from the readings the chart can see behind it. { of } takes a symmetric margin from a column instead; { confidence } sets the level. A mark the chart sees only one value for gets none, and the chart says so. |
| stack | boolean | Stack the series rather than drawing them side by side. |
| curve | boolean | Overlay a kernel density curve on a histogram, which shows which features are in the data and which are in the binning. |
| diverging | boolean | Colour a heatmap outward from zero rather than along a single ramp. |
| downsample | number | Reduce to at most this many points per series before drawing, keeping the extremes so a spike is not lost. |
| tooltip | boolean | false turns the hover tooltip off. |
| selection | boolean | Draw the grid's selected rows emphasised, and follow the selection as it changes. |
| drill | boolean | Clicking a group drills into it. |
| filterOnClick | boolean | Clicking a mark filters the grid to it. |
| size / maxRadius | string / number | Bubble charts: the column driving the radius, and the largest it may be drawn. |
| min / max | number | Fix the measure axis rather than taking it from the data. |
| code / codeProperty | string | A geomap's ISO code column, and the property carrying the code in your shapes. |
| columns / method / values | string[] / string / boolean | Correlogram: which columns to correlate, by pearson, spearman or kendall, and whether to print the coefficients in the cells. |
| iterations | number | Network layouts: how many relaxation passes to run. |
| nodes | ChartNode[] | A network's nodes, named by you rather than inferred from the rows: { id, label, icon, x, y }. id matches a value in the source or target column; icon is any name in the grid's icon registry; x/y are fractions of the plot (0 to 1) and pin the node there, out of the force simulation. A node listed here that appears in no row is still drawn. See Network diagrams. |
| icon | string | The default glyph for a network node that names none of its own. Unset, an undeclared node is a plain disc. |
| linkWidth | number | Fix a network link's stroke width in pixels. Unset, width follows the link's value as a share of the heaviest link. |
| spec / baseline / rules / confidence | object / number / string / number | Control and capability charts: a tolerance overriding the column's own spec, how many leading readings fix the control limits, which rule set judges the violations (westernElectric or nelson), and the level for the capability interval. |
A reduction over no readings is a gap, not a zero.
sum, avg/mean, min, max,
first and last all answer null for a category whose
rows carry no value to reduce, so the line breaks and the bar is absent rather than dropping
to zero - a zero is a real reading, and drawing one where the data reported nothing
would show a plunge that never happened. count and countValues are
the deliberate exception: count tallies rows and countValues tallies
the values actually present, so both are honestly zero when that is the true answer.
countValues is the one to ask for when “none arrived” is the reading
you want drawn as zero rather than as a break in the line.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { bindSeries } = await import('../packages/modules/charts/bind.js');
// 'a' has a reading; 'b' has a row, but the reading itself is absent.
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'day', type: 'text' }, { field: 'sales', type: 'number' }],
rows: [
{ id: 1, day: 'a', sales: 10 },
{ id: 2, day: 'b', sales: null },
],
});
// avg over no readings is null: 'b' is a gap in the line, not a zero.
const gap = bindSeries(grid, { x: 'day', y: { col: 'sales', fn: 'avg' } });
// countValues asks "how many arrived": honestly 0 for 'b', not null.
const none = bindSeries(grid, { x: 'day', y: { col: 'sales', fn: 'countValues' } });
grid.destroy();
return [gap, none].map((bound) => bound.series[0].points.map((p) => String(p.y)).join(',')).join(' | ');
A rolling axis.x.window that has aged past its data shows the empty
state, not a picture drawn off-plot. The window's domain ends at the wall clock
(see window under the axis options below), so a
feed that has gone quiet for longer than the window's span would otherwise have every mark
fall outside the plot, with the axes and legend still drawn as if the chart were healthy.
The chart shows its empty state instead and warns once per chart instance,
naming the span and how old the newest reading is, so a dead feed reads as no data rather
than as a chart that quietly stopped moving.
The x scale comes from the column's type, and nothing else. A temporal type - date, datetime,
timestamp or dateString - draws a time axis; a numeric type
draws a linear axis whatever its distinct count; every other type draws bands, one
per distinct value. A declared numeric or temporal column is therefore never demoted to a
band, which it used to be below thirteen distinct values - silently turning off
fit, band and everything else that needs a continuous x. Where the
column's type is not what you want, pin the scale with
axis: { x: { scale: 'band' | 'linear' | 'time' } }: 'band' is how a
numeric code column (a quarter, a rating, a star count) asks for its bands back, and
'time' or 'linear' lifts a column the grid types as
text onto a continuous axis. That last case is worth knowing about: a grid built
with no rows infers text and does not revisit it when rows
arrive, so a streamed date column binds bands where the same column beside a populated grid
binds time. The chart warns once when it bands a column whose values all read as dates or as
numbers, naming the column, its type and the option that overrules it. A band scale always
draws: a line, area or step on one is drawn through the band centres, and its labels are
thinned to the pitch the font can be read at rather than one per row
(axis.x.every overrides the count).
The margin a chart leaves for its labels is measured from the labels. Where a chart names its rows down the left - a
correlogram's columns, a horizontalBar's categories, a
gantt's tasks, a forest's coefficients, a heatmap's
rows - the gutter is the widest name it will draw, capped at two fifths of the
chart. Past that cap a name is ellipsised and keeps the whole of itself as
aria-label, so a screen reader announces the real name and the glyphs still stop
inside the chart; no label is ever cut mid-glyph, and the gutter is re-measured whenever the
chart is resized. Widening the chart therefore gives the names more room, which is the thing
a fixed margin could not do. Where the left-hand gutter holds a measure axis the
room is estimated from five digits instead, because the numbers on it are not known until
after the plot has been laid out. margin: { left } is added to whatever the
labels need rather than competing with it.
The chart
| Method | Returns | Description |
|---|---|---|
| update(spec) | void | Change any part of the spec and redraw. Keys you omit keep their values. |
| draw() | void | Redraw now, for a change the grid does not announce. |
| data() | object | What the chart last bound: series, categories and the rows behind them. |
| on(event, fn) | function | Returns its own unsubscribe. |
| ascend(levels?) | void | Up one level on a drillable hierarchy. |
| toSVG(opts?) | string | The chart as markup. |
| toPNG(opts?) | Promise<Blob> | scale: 2 for a retina still. |
| toCSV() | string | The bound data, for a reader who wants the numbers. |
| destroy() | void | Yours to call: the element is in your page, not the grid's. |
| element | SVGElement | The chart's own root. |
Clicking a chart
A chart emits click, hover, leave, focus, draw, drill, brush and legend - there is no point:click, point:hover or series:toggle. The common use is filtering the grid from a mark, which makes the pair two views of one selection rather than a chart beside a table. The click and hover payload is flat - { label, category, column, value, series, rowKeys, native, preventDefault }, with no point wrapper - where column is the grid column the mark filters on and category the value to filter it to.
chart.on('click', ({ column, category }) => {
grid.filters.set({ col: column, op: 'eq', value: category });
});
Simplest of all, set filterOnClick: true in the spec and the chart applies exactly that filter itself on the clicked mark. The click event still fires first, so a handler that calls preventDefault() on the payload takes the click over instead.
Chart a selected range
A user who drags out a block of cells - a text column and the numbers beside it - is asking a question a spreadsheet answers with one gesture: chart this. chartRange is that gesture. It reads the selected range, derives the chart from its shape, and returns the same live Chart createChart does, so nothing about it is a second kind of chart.
- The leading text column becomes the categories, and the numeric columns beside it become the measures. One number is a bar chart; several are a grouped bar. A block of pure numbers charts against the row position.
- Hidden and unreadable columns are never charted. The range is resolved against the columns the reader can actually see, so a chart never carries a value the grid itself would not show.
- It is bound to the range's own rows, filtered as the grid is filtered - the band the rectangle covers, not the whole sheet.
- The type is a sensible default you can change:
chart.update({ type: 'line' }), or passtypeup front.
The derivation is pure, so a menu can ask what a range would chart as - the type, the dimension, the measures - before anyone draws it. deriveRangeSpec answers that, and chartRange then draws exactly it. This block proves the shape rule on a real grid; drawing needs a container, shown below it.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { deriveRangeSpec, canChartRange } = await import('../packages/modules/charts/index.js');
let charted = null;
const grid = createHeadlessGrid({
columns: [
{ field: 'region', type: 'text' },
{ field: 'revenue', type: 'number', total: 'sum' },
{ field: 'cost', type: 'number', total: 'sum' },
],
rows: [
{ id: 1, region: 'EMEA', revenue: 300, cost: 120 },
{ id: 2, region: 'AMER', revenue: 500, cost: 240 },
{ id: 3, region: 'APAC', revenue: 200, cost: 90 },
],
rowKey: 'id',
// Opt in, with the handler that draws - the page's link to modules/charts.
// The menu and Alt+F1 call it with the grid and the selected range.
rangeChart(grid, range) { charted = range; },
});
// The rectangle the user dragged: three rows, the text column and both numbers.
const range = { startRow: 0, endRow: 2, columns: ['region', 'revenue', 'cost'] };
// The leading text column is the dimension; the two numeric columns are the
// measures, so the default is a grouped bar (a combo of bar marks).
const plan = deriveRangeSpec(grid, { range });
// What the menu action does when the reader picks "Chart selection".
const handler = grid.get('rangeChart');
if (canChartRange(grid, { range })) handler(grid, range);
return [plan.x === 'region' ? 'Region' : plan.x,
plan.measures.join(','),
plan.type,
charted === range ? 'drawn' : 'no'].join('|');
Drawing is one more call. chartRange takes the container, derives the spec and returns the live chart - or null when the range has no number to plot. The cell menu offers Chart selection, and Alt+F1 triggers it from the keyboard, when the grid is configured with rangeChart. Because the charts module is optional and the grid draws no charts itself, the config carries the handler - a function, or { onChart }, called (grid, range) - which is where a page wires the two together:
import { chartRange } from '@toclocoinc/lattice-grid/modules/charts';
createGrid(el, {
columns, rows,
rangeChart(grid, range) {
// A grouped bar by default; pass `type` to draw it as something else.
const chart = chartRange(grid, { container: '#chart', range });
if (chart) chart.update({ scheme: 'colourblind' });
},
});
Regression diagnostics
A regression is not finished when it has coefficients; it is finished when the residuals
have been looked at. regressionPlots turns a fitted model - the one
grid.statistics.regressionModel returns - into ready chart specs, so the
diagnostic pictures are one call rather than a hand-assembled spec each. It reimplements no
charting and no statistics: the fit line’s confidence band is the module’s own ribbon
primitive fed by the model’s own interval, and the multicollinearity plot is the existing
correlogram paired with the model’s VIF.
The presets that map onto grid columns are returned as drawable specs: the fit with its
band, residuals-vs-fitted (over the fitPredicted and fitResidual
shadow columns), a QQ plot of the residuals, the multicollinearity correlogram, and - over the
fitStdResidual, fitLeverage and fitCooksD columns - residuals-vs-leverage, a bubble sized by Cook's distance. Scale-location
(√|standardised residual| vs fitted) is drawn from explicit points computed off the model,
since its y is a transform no column holds; the coefficient forest plot draws one
row per coefficient - its estimate with a confidence whisker and a line at zero - through the
explicit-bound error-bar primitive. A preset a given model cannot support (no multicollinearity
for one predictor, no band for several) is returned as a null spec carrying a machine-readable
reason rather than silently dropped.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { regressionPlots } = await import('../packages/modules/charts/index.js');
// x=1..5, y=2,4,5,4,5. Shadow columns carry the model's per-row diagnostics -
// fitted value, residual, standardised residual, leverage and Cook's D - so a
// diagnostic chart is a plain chart over columns.
const model = { predictors: ['x'], response: 'y' };
const grid = createHeadlessGrid({
columns: [
{ field: 'x', type: 'number' },
{ field: 'y', type: 'number' },
{ id: 'yhat', title: 'Fitted', shadow: { kind: 'fitPredicted', model } },
{ id: 'resid', title: 'Residual', shadow: { kind: 'fitResidual', model } },
{ id: 'sresid', title: 'Std residual', shadow: { kind: 'fitStdResidual', model } },
{ id: 'lev', title: 'Leverage', shadow: { kind: 'fitLeverage', model } },
{ id: 'cook', title: "Cook's D", shadow: { kind: 'fitCooksD', model } },
],
rows: [
{ id: 'r1', x: 1, y: 2 }, { id: 'r2', x: 2, y: 4 }, { id: 'r3', x: 3, y: 5 },
{ id: 'r4', x: 4, y: 4 }, { id: 'r5', x: 5, y: 5 },
],
rowKey: 'id',
source: { mode: 'memory', columnarBelow: 0 },
});
const { plots } = regressionPlots(grid, {
spec: model, fitted: 'yhat', residual: 'resid', stdResidual: 'sresid', leverage: 'lev', cooksD: 'cook',
});
return [
plots.fit.spec.type, // scatter, with fit:true and band
plots.fit.spec.band.points.length, // a band point per row
plots.residualsFitted.spec.type, // residual vs fitted, a scatter
plots.qq.spec.type, // a QQ plot of the residuals
plots.residualsLeverage.spec.type, // bubble, over the diagnostic columns
plots.residualsLeverage.spec.size, // sized by Cook's D
plots.scaleLocation.spec.type, // scatter, from explicit points off the model
plots.coefficientForest.spec.type, // forest, estimate + whisker per coefficient
].join('|');
Maps
A geomap takes an ISO code from one column and a value from another. Alpha-2, alpha-3 and numeric codes are all accepted, and continent codes draw a continent map without any outline data. Codes that match nothing are counted and reported on the chart rather than dropped, a map missing half its data looks exactly like a map of a world where half the data is zero. The full code tables are in CHART-CODES.md.
Real outlines: geometry packs
A pack is an optional module you import only if you draw that map. Real boundaries are tens to hundreds of kilobytes, so none of them are in the charts bundle and the grid still fetches nothing at runtime: you import the pack you want, exactly as you import an extension chart type, and hand it to shapes. Each pack is TopoJSON - quantised and delta-encoded, decoded by the chart - generated from the published source below by tools/build-geo-packs.mjs, and it carries its own provenance: the source URL, the version, the date it was retrieved, the licence, and the attribution line that licence requires.
import { pack } from '@toclocoinc/lattice-grid/modules/geo-world-110m';
createChart({ grid, container: '#map', type: 'geomap', code: 'iso', y: 'revenue',
shapes: pack }); // Equal Earth, fitted to the pack
| Module | Regions | Source and licence | Size (gzipped) | Attribution required |
|---|---|---|---|---|
| modules/geo-world-110m | 177 countries | Natural Earth 1:110m Admin 0, via world-atlas - public domain | 39 KB | None |
| modules/geo-world-50m | 241 countries | Natural Earth 1:50m Admin 0, via world-atlas - public domain | 225 KB | None |
| modules/geo-us-states | 50 states + DC | US Census cartographic boundaries, via us-atlas - public domain | 36 KB | None |
| modules/geo-europe-nuts | NUTS 0-2 | Eurostat GISCO NUTS 2021 1:20m - free re-use with attribution | 95 KB | © EuroGeographics for the administrative boundaries |
| modules/geo-uk | 9 regions, 361 local authorities, 650 constituencies | ONS Open Geography, generalised clipped - Open Government Licence v3.0 | 292 KB | Contains OS data © Crown copyright and database right 2026; Source: Office for National Statistics licensed under the Open Government Licence v.3.0 |
Joining. A pack is keyed by the code its source is published under and by the codes that source also knows: the world packs by ISO alpha-2, with alpha-3 and numeric accepted; geo-us-states by the two-letter USPS abbreviation, with the FIPS code accepted; geo-europe-nuts by NUTS id (DE, DE1, DE11); geo-uk by ONS code (E12000007). A region the pack does not know is reported as unmatched exactly as before.
Choosing a grain. geo-uk ships three layers in one module because they share a coastline: layer: 'regions' (the default), 'local-authorities' or 'constituencies'.
Two notes from the data, not from us. Natural Earth at 1:110m leaves out the micro-states - Singapore, Malta, Monaco have no outline at that scale - so bind country-level data to geo-world-50m if those matter. And geo-us-states places Alaska and Hawaii at their true longitudes rather than in the insets an Albers USA composite uses, so a map of all 51 spans the Pacific; the five US territories are left out of the pack for the same reason.
Projections
Every map names a projection, and a pack declares the right one for itself, so shapes: pack alone gives a sensible map. projection overrides it; projectionOptions passes parallels and centre to the two that take them. The drawn geometry is then fitted to the panel, so a map of the UK fills its box rather than sitting inside the whole globe's.
| Name | What it is | Use it for |
|---|---|---|
| equalEarth | Equal-area (Šavrič, Patterson & Jenny 2018) | A world map - the default. Areas are honest and the shapes are recognisable. |
| robinson | Compromise, tabulated | A world map where the poles matter more than area. |
| mercator | Conformal, cut at ±85.05° | Matching a web-map basemap. |
| albers | Conic equal-area, two standard parallels | A country or continent in the mid-latitudes - the US and Europe packs default to it. |
| transverseMercator | Conformal about a central meridian | A tall, narrow country. The UK pack defaults to it through 2°W, which is what stands Britain upright. |
| equirectangular | Longitude and latitude straight onto x and y | Back-compatibility: the projection every map here drew before 1.63. |
createChart({ grid, container: '#uk', type: 'geomap', code: 'lad', y: 'claims',
shapes: ukPack, layer: 'local-authorities' }); // transverse Mercator
createChart({ grid, container: '#us', type: 'geomap', code: 'state', y: 'sales',
shapes: usPack, projection: 'albers',
projectionOptions: { parallels: [29.5, 45.5], centre: [-96, 37.5] } });
The antimeridian is handled in the chart. A country whose outline crosses ±180° - Russia, Fiji, New Zealand's Chathams - is split there before it is projected, so it draws as the parts it is rather than as a band running the wrong way across the map. Antarctica is cropped until it carries a value, by its country code AQ as well as the continent code AN.
The module imports nothing from the grid: createChart is handed a grid rather than importing one. That is what keeps the charts bundle to the drawing, and it is why the grid must be created first, and why a chart cannot outlive it.
Network diagrams - icon nodes, links coloured by their value
A network draws the grid's rows as a graph: source and target name the two endpoint columns and y carries the value on the link between them. Three things make it a picture of your network rather than a generic hairball, and each is a fact only you have.
Nodes you name. nodes: [{ id, label, icon, x, y }] gives a node a glyph from the grid's own icon registry - a built-in name, or one you registered - and a label drawn beneath it. A node that appears in the rows but not in nodes takes the chart's icon default (a plain disc when there is none) and its own id as its label. A node listed in nodes that appears in no row is still drawn: a device with no links is a fact worth seeing. The glyphs are SVG paths from the same registry the cells paint from, so they are sharp at any chart size and take the chart's theme colours.
Positions you choose. x and y are fractions of the plot, measured from its top-left. A node giving both is pinned there and takes no part in the force simulation; everything else is laid out around it by the same deterministic relaxation as before, so “core on top, regions below” needs no hand-placed SVG. Half a position is not a position: a node with only x is laid out. A fraction outside 0 to 1 clamps to the edge of the plot rather than drawing where nobody can see it. Pinning one node never reshuffles the others - the layout's seeding draws for every node, pinned or not, precisely so that it cannot.
Colours from the rules you already wrote. Each link's stroke comes from the value column's own conditional-formatting rules, through grid.formatting.styleFor(col, value) - the colour order is background, then backgroundColor, then color; a gradient (a data bar, an icon set) is not a colour and is not read. A link no rule matches keeps the chart's default link colour. There is no chart-level threshold option, deliberately: a second place to say “red above 80” is a second place for the chart and the cell to disagree. The legend lists the rules that actually fired, with their own labels and their own swatches; a rule that matched nothing is not advertised. Change a rule and the links recolour on the next frame without the layout re-running, so nothing moves.
Links are undirected, and parallel links stay parallel. There are no arrowheads, and A,B is the same pair as B,A - a cable has two ends and no direction. Several rows between the same pair are drawn as several lines, side by side, offset perpendicular to the pair by 4 px and symmetric about it, in row order, each with its own value and its own colour. They are not summed: three circuits between two sites are three readings, and one line carrying 120% would be a number nothing measured. The tooltip on any one line names both endpoints and that line's own value, and the pointer picks out the line you are actually over rather than the pair. Width follows the value unless linkWidth fixes it.
Linked like every other chart. The graph is drawn from the grid's filtered rows and follows filter and sort. With selection: true, clicking a link selects its row and clicking a node selects every row it is an end of; the grid's selection then lights those links and nodes and dims the rest. A click handler still fires first and can preventDefault().
The picture, executed
Two core routers pinned across the top, three regional routers pinned below, two circuits between every pair, and one rule set on the load column doing all the colouring. Thirteen rows, thirteen lines, three colours, five glyphs, and a legend that names the three rules.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createChart } = await import('../packages/modules/charts/index.js');
const { document, root } = createTestDom({ width: 640, height: 420 });
// Two circuits between every core and every region: six pairs, twelve rows.
const rows = [];
for (const core of ['core-1', 'core-2']) {
for (const site of ['emea', 'amer', 'apac']) {
for (const [n, load] of [['a', 18], ['b', 92]]) {
rows.push({ id: `${core}/${site}/${n}`, from: core, to: site, load });
}
}
}
rows.push({ id: 'core-1/core-2/x', from: 'core-1', to: 'core-2', load: 61 });
const grid = createGrid(root, {
rowKey: 'id',
selection: 'multiple',
columns: [{ field: 'from' }, { field: 'to' }, { field: 'load', type: 'number' }],
rows,
// One rule set on the column. The cells and the links read it together.
formatting: {
load: [
{ id: 'ok', label: 'Healthy', when: { op: 'lt', value: 40 }, style: { background: '#107c41' } },
{ id: 'busy', label: 'Busy', when: { op: 'lt', value: 80 }, style: { background: '#f0b400' } },
{ id: 'hot', label: 'Saturated', when: { op: 'gte', value: 80 }, style: { background: '#a4262c' } },
],
},
});
const container = document.createElement('div');
container.rect = { width: 600, height: 400, top: 0, left: 0 };
root.appendChild(container);
const chart = createChart({
grid, container, type: 'network', source: 'from', target: 'to',
y: { col: 'load', fn: 'sum' }, selection: true,
nodes: [
{ id: 'core-1', label: 'Core', icon: 'square', x: 0.3, y: 0.15 },
{ id: 'core-2', label: 'Core', icon: 'square', x: 0.7, y: 0.15 },
{ id: 'emea', label: 'EMEA', icon: 'circleFilled', x: 0.2, y: 0.8 },
{ id: 'amer', label: 'AMER', icon: 'circleFilled', x: 0.5, y: 0.8 },
{ id: 'apac', label: 'APAC', icon: 'circleFilled', x: 0.8, y: 0.8 },
],
});
const find = (tag, cls) => [...container.querySelectorAll(tag)]
.filter((n) => (n.getAttribute('class') || '').includes(cls));
const at = (key) => find('circle', '__node').find((c) => c.getAttribute('data-node') === key);
const num = (el, name) => Number(el.getAttribute(name));
// The picture: two cores on one row, three regions on another below them.
const top = [at('core-1'), at('core-2')].map((c) => num(c, 'cy'));
const bottom = ['emea', 'amer', 'apac'].map((k) => num(at(k), 'cy'));
const rowsPinned = top[0] === top[1] && bottom.every((y) => y === bottom[0]) && bottom[0] > top[0]
&& num(at('core-1'), 'cx') < num(at('core-2'), 'cx');
// Every link its own line, coloured by the rule its value matched.
const edges = find('path', '__edge');
const stroke = (e) => ((e.getAttribute('style') || '').match(/stroke:\s*([^;]+)/) || [])[1];
const colours = [...new Set(edges.map(stroke))].sort();
// Two circuits between core-1 and emea, drawn side by side 4px apart.
const ends = (d) => d.match(/-?\d+(?:\.\d+)?/g).map(Number);
const pair = edges.slice(0, 2).map((e) => ends(e.getAttribute('d')));
const gap = Math.round(Math.hypot(pair[0][0] - pair[1][0], pair[0][1] - pair[1][1]));
// Five glyphs, drawn from the registry the host extended.
const glyphs = find('path', '__node-icon').length;
// The legend names the rules that fired, not a palette.
const legend = [...container.querySelectorAll('button')]
.filter((b) => (b.getAttribute('class') || '').includes('__legend-item'))
.map((b) => b.textContent).join(',');
chart.destroy();
grid.destroy();
return `${edges.length} links, ${colours.join('/')} | pinned ${rowsPinned} | gap ${gap} | ${glyphs} icons | ${legend}`;
Extension chart types - pay only for what you draw
The base charts bundle draws the built-in TYPES and nothing else. A new chart type is a separate, opt-in module a caller imports only if they use it, on the slim-core seam. Importing it self-registers the type with the base module through registerChartType; the base Chart consults that registry for any type it does not draw natively. Because the base never imports the extension, the base bundle does not grow for a type a caller never uses.
import '@toclocoinc/lattice-grid/modules/charts'; // the base
import '@toclocoinc/lattice-grid/modules/chart-ridgeline'; // opt in to one type
createChart({ grid, container: '#dist', type: 'ridgeline', x: 'segment', y: 'value' });
An extension declares { draw, bind?, freeform?, labelled? }. Its draw(ctx) receives the same context a built-in drawer gets - plot, bound, groups, scheme, typography, labels, grid, spec - plus ctx.helpers, the base's own toolkit (element factory, scales, axis drawers, mark pool, distribution kernels). So an extension imports nothing heavy from the base: it receives the toolkit and ships only its own geometry. registeredChartTypes() lists what is registered. Ridgeline (drawRidgeline) is the first: one kernel-density ridge per category, stacked and overlapping, over the distribution of a measure - the reading for "how did this distribution change across segments".
const charts = await import('../packages/modules/charts/index.js');
const ridge = await import('../packages/modules/chart-ridgeline/index.js');
// Importing the module self-registered the type against the base registry.
const registered = charts.registeredChartTypes().includes('ridgeline');
// registerChartType is idempotent by name, so re-registering is safe.
charts.registerChartType('ridgeline', { draw: ridge.drawRidgeline });
return [
registered,
charts.registeredChartTypes().includes('ridgeline'),
typeof ridge.drawRidgeline,
].join(' | ');
More lead-pick types ship the same way, each in its own opt-in module - the base bundle stays flat as they are added. Calendar heatmap (drawCalendar, type: 'calendar') lays a measure out value-by-day, GitHub-style, from a date column. Scatter-plot matrix (drawSplom/bindSplom, type: 'splom') crosses every pair of numeric columns. Hexbin (drawHexbin/bindHexbin, type: 'hexbin') bins a scatter into count-shaded hexagons so a million rows read as a density field. Each reads the grid through the public row API, so it follows the grid's filters and sort.
const charts = await import('../packages/modules/charts/index.js');
const cal = await import('../packages/modules/chart-calendar/index.js');
const splom = await import('../packages/modules/chart-splom/index.js');
const hex = await import('../packages/modules/chart-hexbin/index.js');
// Each import self-registered its type; the base bundle carries none of them.
const types = charts.registeredChartTypes();
return [
['calendar', 'splom', 'hexbin'].every((t) => types.includes(t)),
typeof cal.drawCalendar,
typeof splom.drawSplom, typeof splom.bindSplom,
typeof hex.drawHexbin, typeof hex.bindHexbin,
].join(' | ');
The model-evaluation and time-series lead picks ship the same way. ROC / PR / calibration (drawRoc/bindRoc, type: 'roc', curve: 'roc' | 'pr' | 'calibration') evaluates a classifier from a label and a score column, with the AUC. Fan / forecast (drawFan/bindFan, type: 'fan') draws history, a point forecast, and a widening prediction interval from y/forecast/lower/upper. Decomposition panel (drawDecomposition/bindDecomposition, type: 'decomposition') stacks the observed/trend/seasonal/residual components on one x axis - the companion to the grid's own time-series shadow columns.
const charts = await import('../packages/modules/charts/index.js');
const roc = await import('../packages/modules/chart-roc/index.js');
const fan = await import('../packages/modules/chart-fan/index.js');
const decomp = await import('../packages/modules/chart-decomposition/index.js');
const types = charts.registeredChartTypes();
return [
['roc', 'fan', 'decomposition'].every((t) => types.includes(t)),
typeof roc.drawRoc, typeof roc.bindRoc,
typeof fan.drawFan, typeof fan.bindFan,
typeof decomp.drawDecomposition, typeof decomp.bindDecomposition,
].join(' | ');
The comparison and ranking family ships the same way. Slope (drawSlope, type: 'slope') connects each series across two periods; dumbbell (drawDumbbell/bindDumbbell, type: 'dumbbell') shows a start/end gap per category; bump (drawBump, type: 'bump') plots rank-over-time; diverging (drawDiverging, type: 'diverging') grows bars from a central zero; parallel coordinates (drawParallel/bindParallel, type: 'parallel') draws one polyline per row across several numeric axes.
const charts = await import('../packages/modules/charts/index.js');
const slope = await import('../packages/modules/chart-slope/index.js');
const dumbbell = await import('../packages/modules/chart-dumbbell/index.js');
const bump = await import('../packages/modules/chart-bump/index.js');
const diverging = await import('../packages/modules/chart-diverging/index.js');
const parallel = await import('../packages/modules/chart-parallel/index.js');
const types = charts.registeredChartTypes();
return [
['slope', 'dumbbell', 'bump', 'diverging', 'parallel'].every((t) => types.includes(t)),
typeof slope.drawSlope,
typeof dumbbell.drawDumbbell, typeof dumbbell.bindDumbbell,
typeof bump.drawBump,
typeof diverging.drawDiverging,
typeof parallel.drawParallel, typeof parallel.bindParallel,
].join(' | ');
When parallel coordinates is given a colourBy column it colours each line by its category - but a colour with no key is a code, so the chart now emits a legend of those categories, one entry per category in the order the colours were assigned, exactly the shape every other coloured type returns. The base draws it and wires the click, so a click on a category toggles it off through the same hide-a-series gesture the rest of the module has, and the drawer skips a hidden category's lines. With no colourBy there is nothing to key and no legend is drawn. The categories the key is built from are the ones bindParallel returns as groups:
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { bindParallel } = await import('../packages/modules/chart-parallel/index.js');
const cats = ['red', 'green', 'blue'];
const rows = Array.from({ length: 9 }, (unused, i) => ({ id: `R${i}`, a: i, b: i % 4, grp: cats[i % 3] }));
const grid = createHeadlessGrid({
columns: [{ field: 'a', type: 'number' }, { field: 'b', type: 'number' }, { field: 'grp', type: 'text' }],
rows, rowKey: 'id',
});
// The colourBy categories, in colour order - each becomes a legend entry whose
// index picks the same colour its lines use.
const bound = bindParallel(grid, { columns: ['a', 'b'], colourBy: 'grp' });
grid.destroy();
return `key ${bound.groups.join(',')}`;
A least-squares forecast on the trend overlay (trend: { method: 'linear', forecast: n }) no longer draws a bare dashed line: it shades the uncertainty band around the projection. By default that is the Student-t prediction band (a future observation); band: 'confidence' shades the narrower mean-response band the fitted line's own doubt describes, and band: false leaves the bare line. The band widens as the line runs further past the data - the honest shape, since a projection is least certain where it reaches furthest - and confidence (default 0.95) sets its level. It is the exact interval the core forecast kernel reports for the linear method, computed locally in the charts bundle (never imported, for the bundle reason the trend maths already is) and asserted equal to the engine's to the last digit:
const { forecast } = await import('../packages/core/src/index.js');
const { linearTrend } = await import('../packages/modules/charts/trendline.js');
const ys = [2, 5, 6, 9, 11, 12];
const pairs = ys.map((y, i) => ({ x: i, y }));
// The trend overlay's forecast band, three steps ahead at 95%…
const overlay = linearTrend(pairs, 3, { confidence: 0.95 });
// …is the core forecast kernel's linear prediction band, to the last digit.
const engine = forecast(ys, { method: 'linear', horizon: 3, confidence: 0.95 });
const b = overlay.band.points[3];
const e = engine.points[2];
return `match ${b.lower === e.lower && b.upper === e.upper}; conf ${overlay.band.confidence}`;
The hierarchy, flow and geographic remainder ships the same way - treemap, sunburst, funnel, radar, sankey, chord and network are already built in, so the new opt-in modules are: icicle (drawIcicle, type: 'icicle', drawn from the grid's group tree), waffle (drawWaffle, type: 'waffle'), alluvial (drawAlluvial/bindAlluvial, type: 'alluvial'), arc diagram (drawArc/bindArc, type: 'arc'), bubble map (drawBubbleMap/bindBubbleMap, type: 'bubblemap') and hexbin map (drawHexMap/bindHexMap, type: 'hexmap'). The two maps place lon/lat directly, so they need no outline data and fetch nothing.
const charts = await import('../packages/modules/charts/index.js');
const icicle = await import('../packages/modules/chart-icicle/index.js');
const waffle = await import('../packages/modules/chart-waffle/index.js');
const alluvial = await import('../packages/modules/chart-alluvial/index.js');
const arc = await import('../packages/modules/chart-arc/index.js');
const bubblemap = await import('../packages/modules/chart-bubblemap/index.js');
const hexmap = await import('../packages/modules/chart-hexmap/index.js');
const types = charts.registeredChartTypes();
return [
['icicle', 'waffle', 'alluvial', 'arc', 'bubblemap', 'hexmap'].every((t) => types.includes(t)),
typeof icicle.drawIcicle, typeof waffle.drawWaffle,
typeof alluvial.drawAlluvial, typeof alluvial.bindAlluvial,
typeof arc.drawArc, typeof arc.bindArc,
typeof bubblemap.drawBubbleMap, typeof bubblemap.bindBubbleMap,
typeof hexmap.drawHexMap, typeof hexmap.bindHexMap,
].join(' | ');
Map markers - a figure per location, coloured by its own rule
modules/chart-markermap registers markermap: one marker per row, placed by lon/lat over a geometry pack's outlines, showing the row's label and its value beside the dot. Two things come from the grid rather than from the chart, and that is the whole point of the type. The number is the value column's own formatted cell text, so a percentage, a currency or a unit reads on the map exactly as it reads in the table. The colour is whatever grid.formatting.styleFor(valueColumn, value) returns for that row - the rule's background, or its color where it sets no background - so a red / amber / green availability wall is one rule set on one column plus one chart configuration. There is deliberately no chart-level thresholds option and no colour column: the rules are the one source, and a legend lists the rules that actually fired, with each rule's own swatch.
With shapes it draws the pack's regions underneath, through the pack's own projection, and pans and zooms exactly as a geomap of that pack does; without shapes the markers fall back to the projection alone. A row whose coordinates are absent, non-numeric or outside ±180 / ±90 draws no marker and is counted in chart.data().unplaced, which the map also writes under itself. Labels are deconflicted by trying four positions in a fixed order - right of the dot, then left, then above, then below - and a label with nowhere to go is dropped rather than overprinted; labels: false turns them all off on a dense map and leaves the tooltip, which carries the name, the value, the coordinates and the status. With selection: true a click on a marker selects that row in the grid, and the grid's selection emphasises the marker.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { document, root } = createTestDom({ width: 700, height: 460 });
const panel = document.createElement('div');
panel.rect = { width: 700, height: 460, top: 0, left: 0 };
root.appendChild(panel);
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { createChart } = await import('../packages/modules/charts/index.js');
// Importing the module registers `markermap`; its drawer is drawMarkerMap.
const markermap = await import('../packages/modules/chart-markermap/index.js');
const { pack } = await import('../packages/modules/geo-world-110m/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [
{ field: 'site', type: 'text' }, { field: 'lng', type: 'number' },
{ field: 'lat', type: 'number' }, { field: 'avail', type: 'number', format: '0.00%' },
],
rows: [
{ id: 'ldn', site: 'London', lng: -0.13, lat: 51.5, avail: 0.9995 },
{ id: 'syd', site: 'Sydney', lng: 151.2, lat: -33.87, avail: 0.9991 },
{ id: 'fra', site: 'Frankfurt', lng: 8.68, lat: 50.11, avail: 0.9962 },
{ id: 'nyc', site: 'New York', lng: -74.0, lat: 40.71, avail: 0.9805 },
],
});
// Three rules on the column. Nothing below repeats a threshold or a colour.
grid.formatting.add('avail', { when: { op: 'gte', value: 0.999 }, style: { background: '#1b7f3b' }, label: 'Healthy' });
grid.formatting.add('avail', { when: { op: 'gte', value: 0.99 }, style: { background: '#c8a415' }, label: 'Watch' });
grid.formatting.add('avail', { when: { op: 'lt', value: 0.99 }, style: { background: '#c0392b' }, label: 'Breached' });
const chart = createChart({
grid, container: panel, type: 'markermap',
lon: 'lng', lat: 'lat', label: 'site', value: 'avail', shapes: pack,
});
// What was painted: a fill per marker, and the text beside each dot.
const fills = [];
const labels = [];
const walk = (node) => {
for (const child of node.children || []) {
const cls = String(child.getAttribute('class') || '');
if (cls.includes('markermap-dot')) fills.push(child.getAttribute('fill'));
if (cls.includes('data-label')) labels.push(child.textContent);
walk(child);
}
};
walk(chart.element);
// The binder is public too, for a host that wants the placed rows itself.
const bound = markermap.bindMarkerMap(grid, { lon: 'lng', lat: 'lat', label: 'site', value: 'avail' });
chart.destroy();
return `${fills.join(' ')} | ${labels.join(' / ')} | unplaced ${bound.unplaced}`;
Type reference
Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.
The charts module
ChartTypeDefinition
The definition an extension chart type registers. `draw` receives the base drawing context - `plot`, `bound`, `groups`, `scheme`, `typography`, `fontSize`, `labels`, `grid`, `spec`, `doc` - plus `ctx.helpers`, the base's own toolkit of primitives (element factory, scales, axes, mark pool, distribution kernels), and appends its marks to the layer groups. `bind` optionally supplies the bound data (default: the by-series binder); `freeform` lays the chart out without axis gutters; `labelled` declares that `labels` applies.
| Property | Type | Description |
|---|---|---|
| freeform | boolean | Set it when the type lays itself out across the whole frame instead of drawing inside the axis gutters. (optional) |
| labelled | boolean | Whether the spec's `labels` option applies to this type. Default false. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| draw | (ctx: object) => object | ctx: object | => object | Draws the type. It is handed the same context a built-in drawer gets - the plot rectangle, the bound data, the SVG groups, the scheme, the typography and the spec - plus `helpers`, the base's own element, scale, axis and pool primitives, and returns what it drew. |
| bind | (grid: Grid, spec: ChartSpec) => object | grid: Gridspec: ChartSpec | => object | Turns the grid and spec into the bound data `draw` receives. Omitted, the base's by-series binder is used. (optional) |
Chart
A live chart.
Properties
| Property | Type | Description |
|---|---|---|
| element | HTMLElement | The chart's root element - the wrapper the chart built inside the container, which holds the heading, the SVG, the legend and the accessible table. The `<svg>` is a descendant of it, not this element. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| draw | (): void | - | void | Redraw now. |
| update | (spec: Partial<ChartSpec>): void | spec: Partial<ChartSpec> | void | Change the spec and redraw; unnamed keys keep their values. |
| data | (): object | null | - | object | null | The data the chart last bound. |
| ascend | (levels?: number): void | levels?: number | void | Go up one level, on a drillable hierarchy. |
| on | (event: ChartEventName, handler: (payload: ChartEventPayloads[ChartEventName]) => void): () => void | event: ChartEventNamehandler: (payload: ChartEventPayloads[ChartEventName]) => void | () => void | Register an event handler; returns a function that unsubscribes. A handler that throws is reported to the console and the rest still run. What each event carries is {@link ChartEventPayloads}; the handler is declared with the widest of them, so narrow on the name inside it. |
| emit | (event: ChartEventName, payload?: object): object | event: ChartEventNamepayload?: object | object | Fire an event at the subscribers and at the matching `on<Event>` in the spec, and return the payload the handlers saw - which is how a caller reads back what a handler changed. |
| toSVG | (opts?: object): string | opts?: object | string | The chart as standalone SVG markup, empty string before the first draw. Pass `{ inlineStyles: true }` to copy the computed styles onto a clone, which is what an SVG loaded as an image needs to look like the chart on screen. |
| toPNG | (opts?: { scale?: number; background?: string }): Promise<Blob | null> | opts?: { scale?: number; background?: string } | Promise<Blob | null> | Rasterise the chart through the browser, so the picture is the one it drew. Styles are inlined first, and a white background is painted unless `background` says otherwise; `scale` defaults to the device pixel ratio. Resolves to null where there is no canvas or `Image`. |
| toCSV | (): string | - | string | The numbers the chart is drawing, as CSV: one column per series, one row per category (or label and total, for a hierarchy). Empty string before the first draw. |
| destroy | (): void | - | void | Stop following the grid, disconnect the resize observer, stop any rolling-window timer, remove the chart's element and drop every listener. Calling it twice is harmless. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| click | A mark was clicked, before the chart filters, drills or selects on it; cancellable. | ChartClickEvent | yes |
| hover | The pointer moved onto a mark and its tooltip was shown. | ChartDatumEvent | no |
| leave | The pointer left every mark and the tooltip was hidden. | ChartEvent | no |
| focus | The keyboard moved onto a mark, which has just been described for a screen reader. | ChartFocusEvent | no |
| draw | A draw finished, at the settled plot size; a draw that showed the empty state raises nothing. | ChartDrawEvent | no |
| drill | The chart descended into a hierarchy, or `ascend()` came back up. | ChartDrillEvent | no |
| brush | A range was dragged out on an axis, before the chart zooms or filters on it; cancellable. | ChartBrushEvent | yes |
| legend | A legend entry was clicked and the hidden set changed. | ChartLegendEvent | no |
ChartSpec
| Property | Type | Description |
|---|---|---|
| grid | Grid | The grid the chart draws. It reads the grid's filtered rows and redraws when they change, so the chart follows the table without being told to. |
| container | Element | string | Where to draw: an element, or a CSS selector resolved against the grid's document. The chart builds its own root inside it. |
| type | ChartType | Which chart to draw. A name the base bundle does not know is looked up in the extension registry, so an opt-in chart module's type works here once imported. |
| x | string | The category column. (optional) |
| y | string | The measure column, for the types that take one. (optional) |
| series | string | Splits the measure into one series per distinct value. (optional) |
| rows | object[] | ((grid: Grid) => object[]) | The exact rows to chart, overriding the grid's own walk - an array, or a function returning one at draw time. `chartRange` uses it to bind a chart to the band of rows a selected range covers rather than the whole grid. (optional) |
| measures | ChartMeasure[] | Several measures at once, for combo and candlestick. (optional) |
| source | string | Endpoints, for sankey, chord and network. (optional) |
| target | string | The column naming the link's destination, beside `source`. (optional) |
| label | string | Row label and dates, for gantt. (optional) |
| start | string | The start-date column, for a gantt chart. (optional) |
| end | string | The end-date column, for a gantt chart. (optional) |
| title | string | A heading above the plot, drawn in the figure's caption alongside any `subtitle`. (optional) |
| scheme | string | string[] | A named scheme, or an array of colours. (optional) |
| legend | boolean | { position?: 'top' | 'bottom' | 'left' | 'right'; isolate?: boolean } | Show the series legend. The object form places it, and `isolate` lets a click on a legend entry show that series alone. (optional) |
| labels | boolean | ChartLabels | Print the value beside each mark. `true` takes the defaults; the object form sets the position, the format and the minimum gap. Only the chart types that support labels honour it. (optional) |
| axis | { x?: string | ChartAxis; y?: string | ChartAxis; y2?: string | ChartAxis; right?: string | ChartAxis; } | Per-axis configuration. Each side is a title string or an object of `{ title, min, max, ticks, format, grid, labels }`. `y2` (or `right`) configures the second measure axis of a dual-axis or combo chart; a dual-axis chart labels both axes by default so it cannot silently mislead. (optional) |
| brush | boolean | 'filter' | 'zoom' | 'select' | { mode: 'filter' | 'zoom' | 'select'; axis?: 'x' | 'y' | 'y2' } | Dragging across the plot. `true` or `'filter'` writes a range condition into the grid; `'zoom'` changes only this chart's own domain; `'select'` selects the rows under the drag. The object form names which axis the drag acts on - `axis: 'y'` or `'y2'` brushes a value axis, which on a dual-axis chart must say which one it means. (optional) |
| font | object | The type scale, as a base size in pixels or an object naming any of the roles (`size`, `small`, `title`, `axisTitle`, `family`, `weight`). Anything left out is derived from the base, so setting one size rescales the chart rather than leaving one label out of step. The base defaults to 12. (optional) |
| margin | number | { top?: number; right?: number; bottom?: number; left?: number } | Space in pixels around the plot: a number for every side, or an object for the ones it names. It is the floor the axis-label gutters are added to, so widening the left margin buys room beyond what the labels already needed. Defaults to 8. (optional) |
| fit | boolean | 'line' | A least-squares line through a scatter or bubble chart, one per series. `true` draws the line and its R²; `'line'` draws the line alone. Only where the x axis is numeric: on a band scale the positions are categories in an arbitrary order, and a slope through them would be a slope through the order they happened to be listed in. (optional) |
| trend | boolean | ChartTrendMethod | ChartTrend | Array<ChartTrendMethod | ChartTrend> | Trend and forecast overlays: a least-squares line, a trailing moving average, or exponential smoothing, drawn over a line, area or scatter chart. `true` draws a single linear trend; a method name or a {@link ChartTrend} object configures one; an array draws several. The maths matches the core stats engine to the last digit - the same least-squares fit, rolling window and exponential recursions - but is computed locally in the charts module rather than imported, because the in-tree bundler does not tree-shake and the import would inline the whole statistics closure; a test asserts the parity. A `forecast` count projects the linear line that many steps past the data, drawn dashed so it never reads as a reading; a moving average and a smoothed level have no slope to project, so `forecast` is ignored for them and the fact is stated in the accessible description rather than faked. (optional) |
| band | (RegressionBand & { line?: boolean }) | null | A pointwise confidence band, drawn as a varying-width ribbon beneath the fit line. Fed by a fitted model's own interval - the `band` from {@link StatisticsApi.regressionModel}, or as produced by {@link regressionPlots} - so the ribbon and the diagnostics report the one computation rather than a slope redrawn here. `line: false` suppresses the band's own centre line, for a chart that already draws the fit with `fit`. Only where the x axis is numeric, for the same reason `fit` is. (optional) |
| points | { x: number | string; y?: number; label?: string; size?: number; lower?: number; upper?: number; key?: string; }[] | An explicit point set, bypassing the by-column binder: a cartesian chart whose values are not a grid column - a scale-location plot's √|standardised residual|, a coefficient forest's per-coefficient estimate - hands its points in directly. Each is `{x, y}` with an optional `label`, `size` (a bubble's third channel) and `lower`/`upper` (interval bounds the error-bar primitive reads). Numeric `x` throughout gives a continuous axis. (optional) |
| error | boolean | { of?: string; confidence?: number } | Whiskers showing the uncertainty in each mark. `true` computes a confidence interval from the readings behind the mark; `of` takes a symmetric margin from another column instead. (optional) |
| reference | { value: number; label?: string; axis?: 'left' | 'right' }[] | Horizontal reference lines. On a dual-axis bar or line chart (see {@link ChartMeasure.axis}) a line naming `axis: 'right'` is placed on the right-hand scale, so it means what the right axis says rather than landing at the same number on the scale it does not belong to. (optional) |
| annotations | ChartAnnotation[] | The declarative annotation layer: reference and target lines, shaded bands and callouts, each naming the axis it reads and each described into the accessible table as a sentence. A value may be a constant or `compute`d from the data it annotates, so it follows the chart as the grid is filtered. (optional) |
| buckets | number | Bins for a histogram; the default is twelve. (optional) |
| diverging | boolean | A diverging colour ramp, for heatmap and geomap. (optional) |
| shapes | unknown | Country outlines, for a geomap drawing countries rather than continents. Either GeoJSON, an object of code to SVG path data, or a geometry {@link GeoPack} imported from an optional `modules/geo-*` package - as the pack itself, or as `{ pack: id }` once its module has been imported and registered. (optional) |
| codeProperty | string | Which GeoJSON feature property carries the region code that the rows are matched against. Defaults to `iso_a2`. Unused when `shapes` is a map of code to path data. (optional) |
| lon | string | The longitude column, for the types that place a row by where it is rather than by a code: `markermap`, `bubblemap` and `hexmap`. Degrees east, -180 to 180; a row outside that, or with no reading, is left off the map and counted. (optional) |
| lat | string | The latitude column, beside {@link ChartSpec.lon}. Degrees north, -90 to 90, on the same terms. (optional) |
| value | string | The measure a `markermap` writes beside each dot and colours it by. Its text is the column's own formatted cell text and its colour is whatever the column's conditional-formatting rules give that value, so a map and the table beside it say the same thing about the same number. (optional) |
| layer | string | Which layer of a multi-layer geometry pack to draw - the UK pack, for instance, ships `regions`, `local-authorities` and `constituencies` together. Ignored for a single-layer pack. (optional) |
| projection | MapProjection | ((lon: number, lat: number) => [number, number]) | The map projection a geomap draws through: `'equalEarth'` (the default for a world), `'robinson'`, `'mercator'`, `'equirectangular'`, `'albers'`, `'transverseMercator'`, or a projection function of the caller's own `(lon: number, lat: number) => [number, number]`. Left unset, a geometry pack draws through the projection it declares. (optional) |
| projectionOptions | { parallels?: [number, number]; centre?: [number, number] } | Parameters for the projections that take them: `parallels` and `centre` for `albers`, `centre` for `transverseMercator`. (optional) |
| graticule | boolean | { step?: number } | A lon/lat reference grid under a geomap's regions, off by default part 2). Only drawn over a geometry pack's fitted projection - the schematic continents have no fitted projection to draw one against. `step` is the spacing between lines in degrees (default 30). (optional) |
| multiples | string | One chart per distinct value of this column. (optional) |
| canvas | boolean | number | Draw to canvas past this many points. (optional) |
| downsample | number | How many points to reduce a dense line or scatter series to before drawing. Reduction keeps the first point, the last, and the ones that carry the outline. Unset, the target is the plot's width in pixels; a series is only reduced once it exceeds half again that target. (optional) |
| emptyText | string | What to show when there is nothing to draw. Defaults to the grid's own translated empty-chart message. (optional) |
| subtitle | string | A second line under the title. (optional) |
| footnote | string | A note under the plot, a source, a caveat, a unit. (optional) |
| tooltip | boolean | `false` turns the hover tooltip off. (optional) |
| selection | boolean | Draw the grid's selected rows emphasised, and follow the selection. (optional) |
| drill | boolean | Clicking a group drills into it. (optional) |
| filterOnClick | boolean | Clicking a mark filters the grid to it. (optional) |
| stack | boolean | Stack the series rather than drawing them side by side. (optional) |
| curve | boolean | Overlay a kernel density curve on a histogram. (optional) |
| measure | string | An alias for `y`, where "the measure" reads better than "the y axis". (optional) |
| size | string | Bubble charts: the column driving the radius, and the largest it may be. (optional) |
| maxRadius | number | The largest bubble radius in pixels. Clamped to between 6 and 28, and 22 when unset; the smallest bubble is always 3. (optional) |
| min | number | Fix the measure axis rather than taking it from the data. (optional) |
| max | number | Fix the top of the measure axis rather than taking it from the data. (optional) |
| code | string | A geomap's ISO code column. An alias for `x`. (optional) |
| columns | string[] | Correlogram: which columns to correlate, how, and whether to print them. (optional) |
| method | CorrelationMethod 'pearson' | 'spearman' | 'kendall' | Which correlation a correlogram computes. `spearman` uses the rank coefficient; anything else - including `kendall`, which is not implemented - uses Pearson. The figures come from the grid's own statistics, so the matrix cannot disagree with them. (optional) |
| values | boolean | Print each coefficient inside its correlogram cell. On by default, and dropped anyway where the cells are too small for the text to fit; set `false` to leave the matrix as colour alone. (optional) |
| iterations | number | Network layouts: how many relaxation passes to run. (optional) |
| nodes | ChartNode[] | The nodes of a `network`, named by the host rather than inferred from the rows: an icon per device, a label, and a position the layout must honour. A node listed here that appears in no row is still drawn. A node in the rows that is not listed here takes the chart's `icon` default and its own id as its label. (optional) |
| icon | string | The default glyph for a `network` node that names none of its own: any name in the grid's icon registry (see {@link Grid.icons}). Unset, a node with no icon is a plain disc. (optional) |
| linkWidth | number | A `network` link's stroke width in pixels, fixed. Unset, width follows the link's value as a share of the heaviest link, as it always has. (optional) |
| spec | { lower?: number; upper?: number; target?: number } | Control and capability charts: a tolerance overriding the column's own `spec`, how many leading readings fix the control limits, which rule set the violations are judged against, and the level for the capability interval. (optional) |
| baseline | number | How many leading readings fix a control chart's limits. The rest of the series is then judged against them, which is how a step change shows up as a run of violations instead of dragging the centre line to the middle. Unset, the whole series sets the limits. (optional) |
| rules | ControlChartRuleSet 'westernElectric' | 'nelson' | Which run-rule set a control chart's violations are judged against. Defaults to `westernElectric`. (optional) |
| confidence | number | The level of the capability interval printed with a capability chart, as a fraction (0.95 for 95%). (optional) |
RegressionModel
A fitted multi-predictor linear model and its diagnostics.
| Property | Type | Description |
|---|---|---|
| method | string | Which fit produced the model: `ols`, `wls` or `robust`. |
| coefficients | RegressionCoefficient[] | The fitted coefficients, the intercept first, then one per predictor in the order given. |
| r2 | number | The share of the response's variance the model accounts for, clamped to 0..1 - weighted for a weighted fit. 1 when the response has no variance at all. |
| adjR2 | number | R² penalised for the number of predictors, so adding a predictor that earns nothing lowers it. |
| n | number | How many rows the fit used. A row missing the response, any predictor, or (for a weighted fit) a positive weight is left out of the fit. Fewer rows than coefficients gives no model at all rather than a fitted one. |
| df | number | Residual degrees of freedom, n − p. |
| sigma2 | number | Residual variance, RSS ÷ df. |
| fitted | number[] | The model's prediction for each row used, in the order of `rows`. |
| residuals | number[] | Observed minus fitted for each row used - what the model did not explain. |
| leverage | number[] | Hat-diagonal leverage per row. |
| cooksD | (number | null)[] | Cook's distance per row; null where it cannot be computed. |
| vif | number[] | Variance-inflation factor per predictor; Infinity when exactly collinear. |
| heteroscedasticity | Heteroscedasticity | null | The Breusch-Pagan test of whether the residual spread depends on the predictors. Null when the auxiliary regression could not be fitted. |
| band | RegressionBand | null | The pointwise confidence band for the mean response. Present only for a single-predictor fit - the charted fit-line case - and null otherwise. |
| weights | number[] | null | Per-row weights actually used (robust/WLS), or null for OLS. |
| predictors | string[] | The predictor column ids the model was fitted on, in the order the coefficients follow. |
| response | string | The column id the model predicts. |
| rows | number[] | The physical rows the diagnostics are aligned to, in order. |
RegressionCoefficient
One fitted coefficient, with the uncertainty around it.
| Property | Type | Description |
|---|---|---|
| name | string | `(intercept)` or the predictor's column id. |
| estimate | number | The fitted coefficient itself. |
| stdError | number | The standard error of the estimate - the square root of the coefficient's variance, from the residual variance and the design matrix. |
| t | number | estimate ÷ standard error. |
| p | number | Two-sided Student-t p-value; a number with a documented method, not a verdict. |
| lower | number | null | The Wald confidence interval at the model's confidence level - the whiskers a coefficient forest plot draws. Null when there is no residual degree of freedom to form a critical value. |
| upper | number | null | The top of the Wald interval, null on the same terms as `lower`. |