api reference
Renderers, formatting and charts
The built-in renderers and editors, grid.formatting and rules, in-cell charts, highlighting, and styling with CSS custom properties.
API reference › Renderers, formatting and charts
Built-in renderers and editors
Both are addressable by name. Anything you register through components is
addressable the same way, and a name you register wins over a built-in one.
Cell renderers, for cell.render:
| Name | Draws |
|---|---|
| area | A filled sparkline over a series. |
| bullet | A value against a target and qualitative bands. |
| checkbox | A boolean, optionally as a switch. |
| colour | A colour swatch with its value. |
| column | A column sparkline. |
| delta | Movement since the last value, with direction. |
| detailExpander | The master-detail chevron. Generated; not usually named directly. |
| donut | A donut chart from a series. |
| gauge | A value on an arc against a range. |
| group | The group and tree label, with its expander and indent. |
| icon | An icon chosen from the value. |
| image | A picture from a URL. Selected automatically for type: 'image'. |
| line | A line sparkline. |
| link | An anchor, with the text and href drawn from the row. |
| pie | A pie chart from a series. |
| pill | A status chip carrying a semantic variant. |
| progress | A progress bar with an optional label. |
| qrcode | A QR code of the value. |
| range | A span between a low and a high value. |
| rating | A star rating. |
| skeleton | A loading placeholder for a row not yet arrived. |
| stacked | A stacked proportion bar. |
| twoline | A primary value with a secondary line beneath it. |
| winloss | A win/loss sparkline of signed values. |
Editors, for edit.editor. Each column type already selects a
sensible one, so naming an editor is for overriding that choice:
| Name | Edits |
|---|---|
| checkbox | A boolean. |
| code | Source text, in a monospace field. |
| colour | A colour. |
| date | A calendar date. |
| datetime | A date and a time together. |
| duration | A length of time. |
| iconPicker | One icon from a set. |
| ipaddress | An IPv4 or IPv6 address. |
| multiSelect | Several options, as chips. |
| number | A number, with the column's constraints. |
| objectPicker | A record chosen from a list. |
| password | A masked secret. |
| radix | A value in its own base. |
| rating | A star rating. |
| segmented | One of a few options, as a segmented control. |
| select | One option from a list. |
| slider | A number on a track. |
| text | A single line. The default. |
| textarea | Several lines. |
| time | A time of day. |
| treeSelect | A value from a hierarchy. |
| unit | A quantity with a unit. |
grid.formatting
Conditional formatting rules the grid holds as runtime state, so an end user can change them. Rules travel in a saved view and undo like any other change. A scope is a column id, or '*' for every column; grid-wide rules are evaluated first, then the column's own, as one ordered list in which the first match wins.
This is distinct from compileRules() feeding cell.style, which compiles at configuration time and is what you want for rules a user should not be able to change. Both work at once: a runtime rule layers over whatever cell.style produced, winning only for the properties it names.
createGrid(el, {
formatting: { // optional seed
margin: [{ when: { op: 'lt', value: 0 }, style: { background: '#fbeceb' } }],
},
});
grid.formatting.add('margin', { when: { op: 'lt', value: 0 }, style: { background: 'red' } });
grid.formatting.add('*', { when: { op: 'blank' }, style: { background: '#f1f3f5' } });
grid.formatting.move('margin', ruleId, 0); // order is meaning
grid.formatting.update('margin', ruleId, { enabled: false });
grid.formatting.remove('margin', ruleId);
grid.formatting.clear('margin'); // or clear() for everything
| Method | Returns | Description |
|---|---|---|
| list(scope?) | Rule[] | The rules for one scope, in evaluation order. |
| all() | object | Every rule keyed by scope, the shape a saved view carries. |
| scopes() | string[] | Every scope holding at least one rule. |
| add(scope, rule, opts?) | Rule | null | Appends, or inserts at opts.at. Returns the rule with its generated id. |
| remove(scope, idOrIndex) | boolean | By id or position. |
| update(scope, idOrIndex, patch) | Rule | null | Merges fields. The id is identity and cannot be reassigned. |
| move(scope, idOrIndex, to) | boolean | Reorder, which can change which rule wins. |
| set(scope, rules) | Rule[] | Replace one scope. |
| replaceAll(rules) | void | Replace every scope at once. |
| clear(scope?) | void | One scope, or all of them. |
| styleFor(colId, value) | object | null | What the rules alone would paint, for an export or a preview. |
A rule held here must be JSON: style may not be a function, because the rules are serialised into views and undo slices. Config-time cell.style still accepts one. Group rows are not formatted, matching the way decoration is dropped for them.
Rules that describe the data, not a threshold
gt: 100 needs somebody to know that 100 is the interesting number. Often nobody does, the interesting cells are the top decile, or the outliers, and where those fall is a property of the data rather than of the rule. These operators say that directly, and the grid works out the threshold from the column itself, over the filtered rows.
grid.formatting.add('margin', { when: { op: 'outlier' }, style: { background: '#fbeceb' } });
grid.formatting.add('qty', { when: { op: 'topPercent', value: 10 }, style: { bold: true } });
grid.formatting.add('score', { scale: { from: 'quantile', colours: ['#f8f9fa', '#1a6bc7'] } });
grid.formatting.distribution('margin'); // { n, min, max, mean, stddev, median, q1, q3, iqr }
grid.formatting.restat(); // re-derive every threshold from the data as it stands
| Operator | value | Marks |
|---|---|---|
| topPercent | 10 or 0.1 | The top tenth of the column. Written either way; both mean the same thing. |
| bottomPercent | 10 or 0.1 | The bottom tenth. |
| topN | 5 | The five largest, ties included: three rows sharing second place in a top three all take the colour. |
| bottomN | 5 | The five smallest. |
| aboveMean / belowMean | , | Either side of the mean. |
| aboveMedian / belowMedian | , | Either side of the median, which is the one to reach for on a skewed column. |
| zAbove / zBelow | 2 | That many standard deviations from the mean. A column with no spread marks nothing rather than everything. |
| outlier | 1.5 | Outside Tukey's fences at that many IQRs, the same definition a box plot draws, so the marked cells are the ones its whiskers exclude. |
A colour scale can take its bounds the same way, with from in place of min and max: 'minmax' spans the data, 'quantile' spans low to high (5th to 95th percentile by default), 'stddev' spans deviations either side of the mean. The quantile form is the better default on real data, one mistyped order of magnitude otherwise compresses every real value into the first swatch.
Thresholds are pinned when the rules compile and do not move on their own. That is deliberate: a boundary that re-derived itself as rows were filtered would repaint cells whose values had not changed, and nobody comparing two screenshots could tell which of the two things had moved. grid.formatting.restat() is how you move it, and a "recalculate" control is the natural place to put it.
grid.highlight
One mechanism for two jobs: the flash a changed cell makes, and a marker you paint deliberately. A target is a cell ({key, colId}), a row ({key}, or a bare row key) or a column ({colId}). Cell beats row beats column, so a specific highlight is never hidden by a broad one laid over it.
createGrid(el, {
highlightOnChange: { colour: '#ffe08a', duration: 1200 }, // or just true
});
grid.highlight({ key: 'r1', colId: 'cap' }, { colour: 'green', duration: 800 });
grid.highlight({ key: 'r3' }, { colour: '#fdeaea', duration: 0 }); // 0 = until cleared
grid.highlight({ colId: 'margin' }, { colour: '#e7f1fd', duration: 0 });
grid.highlight.clear({ key: 'r3' });
grid.highlight.clear(); // everything
| Method | Returns | Description |
|---|---|---|
| highlight(target, opts?) | boolean | colour (or color) and duration in milliseconds. duration: 0 stays until cleared. |
| clear(target?) | boolean | One target, or every highlight when called with nothing. |
| list() | object[] | Every active highlight and its remaining duration. |
| colourFor(key, colId) | string | null | What a given cell is painted, after precedence. |
A highlight belongs to the row, not the element. Rows are recycled as you scroll, so highlights are reapplied after every paint, they survive scrolling, sorting, filtering and paging without any of them knowing highlights exist.
Conditional formatting
Rules compile into the function cell.style already takes, so a compiled rule set
installs exactly like a hand-written style function.
import { compileRules } from '@toclocoinc/lattice-grid';
{ field: 'margin', cell: { style: compileRules([
{ when: { op: 'lt', value: 0 }, style: { background: '#fdecea', colour: '#b91c1c' } },
{ when: { op: 'between', value: 0, value2: 5 }, style: { background: '#fdf3e0' } },
{ scale: { min: 0, max: 100, colours: ['#f8f9fa', '#1a6bc7'] } },
]) } }
| Key | Description |
|---|---|
| when | { op, value, value2 }, using the same operators as filters: eq, ne, gt, gte, lt, lte, between, outside, contains, notContains, startsWith, endsWith, blank, notBlank, true, false. |
| style | A style object, or a function of the cell params. |
| scale | { min, max, colours }, a colour scale. Two or more stops, reached evenly. |
| stopIfTrue | Default true. false lets a later rule add to this one. |
| enabled | false skips the rule without removing it. |
Rules are evaluated in order and the first match wins, as in a spreadsheet: "red if overdue, amber if due this week" reads top to bottom and stops. A blank cell satisfies no comparison, so an empty cell is not swept into "less than 100".
A scale's min and max are required rather than
derived from the data. A scale that rescaled as rows were filtered would change a cell's colour
without its value changing.
In-cell charts
Eleven chart renderers for a cell. Each is a single SVG whose path data is the only thing a repaint writes, so they cost the same as any other cell as rows recycle.
| Name | Shows | Reads |
|---|---|---|
| line | Trend across a series. | An array |
| area | Trend, with the area beneath filled. | An array |
| column | A bar per point, drawn from zero. | An array |
| winloss | One equal mark per point, up or down. | An array |
| pie | How a set of numbers divides. | An array |
| donut | The same, with a hole. | An array |
| bullet | One measure against a target, over bands. | A number |
| stacked | How one row's total divides, across the cell. | An array |
| range | The span a set of values covers, middle marked. | An array |
| gauge | One value as a dial. | A number |
| delta | Direction and movement over a sampling interval. | A number |
{ id: 'trend', field: 'readings', cell: 'line' }
{ id: 'spend', field: 'monthly', cell: { render: 'column', props: { min: 0, max: 100 } } }
{ id: 'mix', field: 'split', cell: { render: 'donut', props: { hole: 0.55 } } }
{ id: 'sla', field: 'uptime',
cell: { render: 'bullet', props: { target: 80, bands: [60, 85], max: 120 } } }
// When the series lives on another property than the cell's value.
{ id: 'trend', field: 'latest', cell: { render: 'line', props: { series: 'readings' } } }
| Prop | Applies to | Description |
|---|---|---|
| series | sparklines | Property name holding the array, when it is not the cell's value. |
| min / max | all | Pin the scale so several columns compare like for like. |
| label | all | false hides the number beside the chart. |
| marker | line, area | false hides the dot on the last point. |
| hole | donut | Inner radius as a fraction, default 0.55. |
| target | bullet | Draws the target marker. |
| bands | bullet | Edges of the qualitative bands, e.g. [60, 85]. |
| interval | delta | Milliseconds between samples. Default 1000. |
| mode | delta | 'change' (default) or 'against'. |
| against | delta | Property to compare with in against mode. |
| show | delta | 'both', 'arrow' or 'delta'. |
Entries that are not numbers are gaps rather than zeroes: a line breaks
across them and a bar is omitted. Pin min and max when comparing
columns, a sparkline scaled to its own data fills its cell whatever the magnitude.
The chart is aria-hidden and the cell carries a text summary,
so a screen reader is told "12 points, 9 to 20, ending 18" rather than each value in turn.
Built-in names
Every registry accepts a custom entry under the same name, which then wins over ours.
Data types
Seven built-in, and seventy-four in the extended catalogue. Inference only ever reaches the built-in
names: the candidates are tried in registration order, and every string settles on
text and every number on number before an extended type is reached. So a
column asks for an extended type by name.
Editors
Cell renderers
Filters and aggregations
Icons
Inline SVG sprites, overridable by name through registerIcon(name, def).
Styling and your page's CSS
Forced colours. In Windows High Contrast Mode the grid translates state that is normally a background tint into borders and system colours: selection takes the system's own selection colours, pinned regions swap their shadow for a rule, and diff states are told apart by border style rather than by hue. A colour swatch and a collaborator's presence colour keep their own colour, because there the colour is the information.
Every selector is namespaced under .lattice, so the grid cannot restyle your
page. Since 1.4.0 the reverse holds too: the elements the grid builds are given a floor for the
properties a host page commonly sets on a bare tag: margin, padding, border, radius,
background, shadow, text transform, letter spacing, and type and colour on form controls. A rule
such as section { padding: 5.5rem 0 } no longer reaches inside the grid.
No !important is involved. The reset is specificity (0,1,1); every rule that
dresses a grid element is (0,2,0) or higher, and so is any rule of yours aimed at a Lattice
class. Deliberate overrides work exactly as before: only bare-tag rules are shut out. The reset
covers box model and decoration only, never display, position or any
dimension.