api reference
Charts, statistics and units
The charts module and createChart, grid.statistics and shadow columns, process capability, and unit systems of your own.
API reference › Charts, statistics and units
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 |
| 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 |
| 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.
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. |
| series | string | Splits the measure into one series per distinct value. |
| measures | object[] | {col, fn, type, axis}: several measures at once, each reduced by any totals kernel. |
| 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. |
| 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. |
| 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. |
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 point:click, point:hover and 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.
chart.on('point:click', ({ point }) => {
grid.filters.set({ col: 'region', op: 'eq', value: point.x });
});
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. Country outlines are yours to supply through shapes, because a world atlas is larger than the whole library and this package fetches nothing at runtime.
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.
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.
grid.statistics
What the grid knows about its own numbers, and about how they have changed since the page loaded. Every figure is computed over the filtered rows, through the same column handles the totals row uses, so a median here and a median in the footer are the same number, by the same definition (R type 7).
grid.statistics.profile('margin');
// { column, rows, present, missing, distinct, min, max, mean, median,
// q1, q3, iqr, stddev, outliers, histogram: [{ from, to, count }, …] }
grid.statistics.reduce('margin', 'p95'); // any registered kernel
grid.statistics.correlation('spend', 'revenue'); // Pearson's r, clamped to [-1, 1]
grid.statistics.weightedAverage('price', 'qty');
grid.statistics.covariance('spend', 'revenue');
grid.statistics.regression('spend', 'revenue'); // { slope, intercept, r2, stdError, n }
grid.statistics.spearman('spend', 'revenue'); // rank; one outlier cannot drag it
grid.statistics.kendall('spend', 'revenue'); // tau-b, null past 5,000 rows
grid.statistics.weightedQuantile('price', 'qty'); // the median by default
grid.statistics.series('price', { by: 'date', periodsPerYear: 252 });
// { volatility, annualisedVolatility, growth, maxDrawdown, maxDrawdownFrom,
// maxDrawdownTo, autocorrelation, upDays, downDays, … }
grid.statistics.capability('mm', { baseline: 20 });
// { cp, cpk, pp, ppk, sigmaWithin, sigmaOverall, outOfSpec, defectRate,
// limits: { centre, upper, lower, sigma }, violations: [{ index, rule }] }
grid.statistics.shadow('price', 'delta', 'R42'); // one row's shadow value
grid.statistics.rebase('price'); // "mark all": today's values become the baseline
grid.statistics.tracking(); // { columns, rows, forgotten }
The statistics tool panel is the end-user half of profile(): a column picker, the twelve figures and a histogram of the column's shape, all following the filters. Add it with toolPanel: { panels: ['columns', 'statistics'] }.
It shows the twelve one-pass figures, then Shape (skewness, kurtosis, Jarque-Bera), Robust (trimmed and winsorized means, MAD, robust outliers), Concentration (Gini, HHI, entropy, evenness, top-3 share) and Capability where the column declares a spec. A section whose reductions all return null is left out rather than shown as a column of dashes.
Or put it in your own page. mountPanel takes no dock and does not create one: toolPanel may be off entirely, so a statistics readout can sit beside a chart, in your own sidebar, or in a settings dialog, at whatever size you give it. It repaints on the same events the rail does, so it stays in step with filters, edits and saved views without you subscribing to anything.
import { mountPanel } from '@toclocoinc/lattice-grid';
const stats = mountPanel({ grid, panel: 'statistics', container: sidebar });
stats.refresh(); // for a change the grid does not announce
stats.destroy(); // yours to call: the element belongs to your page
Any built-in panel works: columns, filters, views, quick, formatting, statistics, as does a constructor of your own.
The reductions
Forty-one, all available to a totals row, to reduce() and to the profiling panel. Names are the same everywhere and the labels come from the message catalogue, so a grid in Polish reads in Polish.
| Group | Names |
|---|---|
| Basic | sum, avg, min, max, count, countValues, first, last, distinct, mode, range |
| Spread | variance, varianceP, stddev, stddevP, iqr, mad, sumSquares |
| Quantiles | median, p25, p75, p90, p95, p99 |
| Shape | skewness, kurtosis, jarqueBera: above 5.99 the column is not plausibly normal |
| Means | geomean, harmean, weightedAvg, trimmedMean, winsorizedMean |
| Outliers | robustOutliers: by the modified z-score, which an outlier cannot hide inside the way it inflates an ordinary one |
| Concentration | hhi, entropy, evenness, top3Share, top10Share, gini, the only group that reads a text column, because "how concentrated is this" is a question about categories |
| Positional | argmin, argmax |
Process capability
Declare the customer's tolerance on the column, and the capability figures, a control chart and any rule marking an out-of-tolerance cell all read the same limits.
columns: [{ field: 'mm', type: 'number', spec: { lower: 9.5, upper: 10.8, target: 10 } }]
Cp and Cpk use short-term variation, estimated from the moving range; Pp and Ppk use the overall standard deviation. The gap between them is the point: Cpk well above Ppk means the process drifted. Cp above Cpk means it is precise and aimed wrong, which needs a different fix from being too variable.
baseline fixes the control limits over the first N readings. Without it the limits are computed over everything (including whatever the process did wrong) so a step change pulls the centre line between the two levels and both halves land outside three sigma. Technically true, and useless for finding when it moved.
Seeing it
The statistics have chart types to match, in modules/charts. Each takes its numbers from this namespace rather than recomputing, so a coefficient in a matrix and the same one from the API cannot drift apart.
| Type | What it shows |
|---|---|
| correlogram | Every numeric column against every other, on a ramp centred at zero so the sign reads first. method: 'spearman' ranks instead; where the two disagree, the pair is related but not linearly. |
Sample quantiles against normal ones. Jarque-Bera says a column is not normal; this shows how, a heavy tail bends the ends, a skew bows the whole line. The reference runs through the quartiles, as R's qqline does, because a fitted line is dragged by the very tails you are inspecting. | |
| ecdf | The share at or below each value, as a step. No bins, so its shape is not partly a choice, and two overlay cleanly where two histograms fight. |
| lorenz | The curve a Gini is read off, against the diagonal a perfectly even column would trace. |
| control | An individuals chart: control limits from the moving range, the specification, and points breaking a Western Electric rule. The control limits are the process talking and the specification is the customer talking: conflating them is the classic error, so they are drawn differently. |
| histogram | curve: true overlays a kernel density estimate, which has no bin edges and so separates what is in the data from what is in the binning. |
| scatter | fit: true draws least squares per series with R² beside it. |
Values the grid maintains for you
Three of the ideas in this section are not standard grid vocabulary, so it is worth saying what they have in common before the detail. Each of them is a value the grid keeps up to date from data you already have, declared once rather than maintained by hand.
The alternative, in every application that needs one of these, is a parallel structure in the host: a copy of what each row looked like a moment ago, a rank recomputed on every tick, a cumulative total that has to be redone whenever the sort changes, and a dashboard panel running its own query beside the table. That code works for a while and then produces a number nobody can account for, usually because one part of it noticed a filter and another did not.
| Concept | What it is | Reach for it when |
|---|---|---|
| Shadow column | An extra column, declared against a real one, holding something the grid works out about it: how it has changed, or where it sits among the others. | You want “what was this an hour ago”, “how many places has it moved”, or “which decile is it in” as a column you can sort and filter on. |
| Running column | A cumulative value: the total, or the share of the total, by the time you reach this row. | You want a running balance, a cumulative percentage, or a Pareto curve down the page. |
| Derived grid | A whole second grid whose rows are built from the first: grouped, unnested, filtered, ranked or profiled. | You want a top-five panel, a breakdown by region, an exceptions list or a statistics summary beside the table, and it must never disagree with it. |
The line between a shadow and a running column is the sort order. A shadow is a function of the column: a row’s own history, or where its value sits among the others. Sort the grid differently and a rank is still the same rank. A running total is the opposite : it answers “how much by the time we reach this row”, and by the time is the order the rows are in, so re-sorting changes every value in the column. That is why they are declared separately rather than as two kinds of one thing.
A derived grid is a different scale of the same idea. A shadow adds a column to the rows you have; a derivation produces different rows altogether: one per sales person rather than one per sale. Because it is a source rather than a special kind of grid, the result sorts, filters, totals, themes and exports like any other, and can itself be the source of another.
All three read the rows the grid is currently showing, so a filter applied to the table moves the ranks, the running totals and every derived panel together. That is the property worth having: not that any one of them is clever, but that they cannot disagree.
Shadow columns
A shadow column is declared against another column and maintained by the grid. It has no field in the data and it is not a pure computed column either, because its value depends on what happened before. It is a real column throughout: sortable, filterable, totalled, grouped, exported, saved into a view, which is what makes "show me every circuit repriced more than twice this session, most-changed first" one gesture rather than a report.
columns: [
{ field: 'price', type: 'number' },
{ id: 'moved', title: 'Change', shadow: { of: 'price', kind: 'delta' } },
{ id: 'churn', title: 'Updates', shadow: { of: 'price', kind: 'updates' } },
{ id: 'run', title: 'Streak', shadow: 'streak' }, // shorthand: shadows the column beside it
]
| Kind | Value |
|---|---|
| updates | How many times the row's value has changed. Arrival is not a change, so a freshly loaded grid reads zero rather than one. |
| updatedAt | When it last changed, as a Date. |
| sinceUpdate | Milliseconds since it last changed. |
| delta | Current value minus the baseline. |
| deltaPercent | The same as a percentage. A change from nothing has no percentage and reads null rather than infinity. |
| rate | Change per second, from the last two readings. |
| history | The recent readings, oldest first. depth sets how many; the default is 20. |
| firstValue | The baseline itself. |
| streak | Consecutive moves in one direction, signed. It resets on a turn, because "seven rises" means something and "seven changes" does not. |
A second family answers where the row sits among the others rather than what it did before. They share the same declaration and the same state, the tracker already holds every row's current value and its baseline, which is exactly what a rank and a rank change need.
| Kind | Value |
|---|---|
| rank | Competition rank, largest first: ties share the better rank and the next value skips, so two firsts are followed by a third. |
| rankAsc | The same ranking read from the other end. |
| rankChange | Places climbed since the baseline. Positive means climbed, even though the rank number itself falls: this is the "top movers" column. |
| percentile | The share of rows at or below this one, 0 to 100. |
| quartile | 1 to 4, agreeing with percentile: the 60th percentile is in the third quartile. |
| zScore | Deviations from the mean. A column with no spread reads null rather than zero. |
| shareOfTotal | The value over the column's total, as a percentage. A total of zero (a column of offsetting positions) reads null rather than a division by it. |
Running totals
A running column answers “how much by the time we reach this row”: a balance that accumulates down the page, or the share of the total accounted for so far. It is the column a Pareto chart is made of, and the one a finance report opens with.
It is declared separately from a shadow column, and the sort order is the reason. Every shadow is a function of the column (of a row's own history, or of where its value sits among the others) so it reads the same however the rows are arranged. A running total does not: sort the grid differently and every value changes, because the question is "how much by the time we reach this row", and by the time is the sort order.
columns: [
{ field: 'amount', type: 'number' },
{ id: 'cum', title: 'Running', running: { of: 'amount', kind: 'total' } },
{ id: 'share', title: 'Cumulative %', running: { of: 'amount', kind: 'percent' } },
]
Computed in one pass over the display rows and cached against that ordering, so a hundred thousand rows are walked once per sort rather than once per cell. A running column is not sortable. Sorting on one asks the sort to depend on its own output (the value is defined by the display order) so the column does not offer a sort unless its definition asks for one, and the query layer refuses such a sort with a warning rather than computing it. Sort by the column it runs over instead. Group headings and totals rows are skipped, a running total that counted a subtotal would double everything below it, and a row with no value carries the figure forward unchanged rather than resetting it.
Positional kinds rank over every tracked row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. Pass scope: 'filtered' on the shadow spec to rank within what the filters left instead: both answers are legitimate, which is why it is a choice rather than a default.
Shadow state is keyed by row key, never by index: after any sort an index-keyed history would report one row's past against another row's present, and the wrong number would be sortable. Memory is capped at 200,000 tracked rows per column; past that the oldest are dropped and tracking().forgotten says how many, rather than a smaller number being reported as though it were the truth.
The statistic block
createStat draws the tile a dashboard opens with: a label, a value, its change
against a baseline, and a line saying what the comparison was. Two things make it worth using
rather than writing. It reads the grid, so it cannot disagree with the table
beneath it, a tile saying £4.2M above a table filtered to £1.8M is worse
than no tile, and that is what a hand-built tile does the first time somebody adds a filter.
And it formats through the column's own type: a stat over a
seconds column reads 42.1 ms, over a money column with an auto ladder
£1.2M, with nothing declared.
import { createStat } from '@toclocoinc/lattice-grid';
createStat({
grid, container: '#mrr',
title: 'Monthly recurring revenue',
of: 'mrr', fn: 'sum',
baseline: lastMonth,
footer: 'vs. last month',
});
| Key | Type | Description |
|---|---|---|
| grid | Grid | The grid to read. |
| container | Element | string | Required. An element, or a selector resolved against the grid's document. |
| title | string | The label above the value. Hidden when absent rather than left blank. |
| of | string | The column to reduce. Omit for count. |
| fn | TotalName | Any of the totals-row kernels: sum, avg, median, p95, distinct, gini and the rest. sum by default. |
| show | string | Report this column from the row holding the extreme, rather than the extreme itself: { of: 'sales', fn: 'max', show: 'rep' } is the name of the best rep. Needs min or max; no single row holds an average, so any other reduction is refused with a warning. |
| value | unknown | fn | A literal value (numeric or otherwise) or a function of the grid, instead of a reduction. |
| footer | string | fn | Text under the value, or a function of it. |
| baseline | number | fn | What the value is compared against. A zero baseline reports the absolute change and no percentage, because “up infinity per cent” is not a reading anyone can act on. |
| goodWhen | 'up' | 'down' | 'neither' | Whether a rise is good news, which decides the colour. up by default. Revenue up is green and error rate up is red; a tile that paints every rise green is misleading on half a dashboard. |
| scope | 'filtered' | 'all' | 'selected' | Which rows feed the value. filtered by default; all for a tile that is deliberately a constant, such as the denominator a filtered number is a share of. |
| live | boolean | false stops the tile following the grid. refresh() still works, so a caller can drive it. |
| format | (value, grid) => string | Override the formatting the column's type would apply. |
| empty | string | Shown when there is no value. An em dash by default. |
| decimals | number | Fraction digits for a value whose reduction changed the unit. 2 by default. |
The column's formatter is borrowed only where the reduction leaves the unit alone. A Gini
coefficient over a money column is a ratio between 0 and 1, and rendering it as
$0.34 says it is thirty-four cents; counts, ratios and variances, which are
in units squared: fall back to a plain number.
Returns { element, value, refresh, destroy }. A misconfigured tile returns an inert
handle rather than throwing, so a dashboard with one bad tile still renders the other eleven.
Units of your own
Twenty-six unit systems ship: length, mass, pressure, data, bitrate, angle, temperature, flow and the rest. A family of your own takes three things, and all three are yours to set: the symbols, where the symbol sits relative to the number, and how the rungs relate to each other.
import { registerUnitSystem, defineUnit, createUnitType } from '@toclocoinc/lattice-grid';
// `factor` is how many base units one of these is. Exactly one must be 1.
registerUnitSystem('distance', [
defineUnit('mm', 0.001, ['millimetre', 'millimetres']),
defineUnit('cm', 0.01, ['centimetre', 'centimetres']),
defineUnit('m', 1, ['metre', 'metres']),
defineUnit('km', 1000, ['kilometre', 'kilometres']),
]);
createGrid(el, {
dataTypes: {
distance: createUnitType({ system: 'distance', unit: 'm', display: 'auto' }),
},
columns: [{ field: 'span', type: 'distance' }],
});
The factor values are the relationship between the rungs: there is no separate ladder to declare, and no ordering to get right, because the ladder is sorted by factor at load. A hand-ordered list of thirty units is one transposition away from an auto display that walks backwards, and that mistake is invisible in review.
| Option | Description |
|---|---|
| system | A built-in system, or one registered with registerUnitSystem. |
| unit | What the column stores. It need not be the system's base: the same ladder with unit: 'km' stores kilometres, and 50mm typed in becomes 0.00005. |
| display | 'auto' walks the ladder for the most readable rung, or name a symbol to fix it. |
| placement | 'prefix' puts the symbol in front: $1,200, and applies to input as well as display. Defaults to a suffix. |
| decimals | Fixed fraction digits; or minDecimals / maxDecimals, or significantFigures. |
| locale | Separators and grouping. Follows the grid's locale when unset. |
A unit given { auto: false } stays off the display: 'auto' ladder while remaining accepted on input and available as an explicit display. That is how imperial units sit beside metric ones without an auto readout jumping between the two.
The stored value is always a plain number in the column's own unit. Sorting, filtering, grouping, totals and the pivot all read that number and never the text, which is why 250mm sorts below 1.5 cm correctly rather than 1 sorting before 9. registerUnitSystem is global and throws on a duplicate name, so register each system once at startup rather than inside a component that may mount twice.