developer guide
Audit Mode and Diff View for a Data Grid
Audit mode shows the grid as a record of change: the previous value beside the current one, and rows that were deleted still visible rather than quietly absent, so a review has something to look at.
Developer guide › Collaboration and comments › Audit Mode and Diff View for a Data Grid
Audit mode
Give the grid a prior snapshot and every row reports whether it was added, removed or changed, and which cells moved.
Before and after
diff: { snapshot: lastApprovedVersion },
grid.diff.summary(); // { added, removed, changed, unchanged }
grid.diff.statusOf('CIR-100042'); // 'changed'
grid.diff.changedColumns('CIR-100042');
grid.diff.before('CIR-100042', 'capacity');
Changed rows get a band down the leading edge and changed cells a tint with the prior value on
the cell as data-before. The row band and the cell tint are deliberately different
devices: "which rows moved" and "what changed in this row" are different questions and one
highlight cannot serve both.
A network map: icon nodes, links coloured by their value
A network chart draws the rows as a graph - two columns name the endpoints
and a third carries the value on the link. Until 1.64 every node was a plain circle, every
edge the same grey, and the layout went wherever the simulation put it, so the picture a
network team actually draws - core on top, regions below, an icon per device, a colour
per circuit - could not be expressed. Three options change that, and every one of them
carries a fact that only the host has.
One nodes list, one rule set, one chart config
createGrid(el, {
columns: [{ field: 'from' }, { field: 'to' }, { field: 'load', type: 'number' }],
rows, // one row per circuit
selection: 'multiple',
// The rules live on the column. The cells and the links read the same ones.
formatting: {
load: [
{ label: 'Healthy', when: { op: 'lt', value: 40 }, style: { background: '#107c41' } },
{ label: 'Busy', when: { op: 'lt', value: 80 }, style: { background: '#f0b400' } },
{ label: 'Saturated', when: { op: 'gte', value: 80 }, style: { background: '#a4262c' } },
],
},
});
createChart({
grid, container: '#topology', type: 'network',
source: 'from', target: 'to', y: { col: 'load', fn: 'sum' },
selection: true,
nodes: [
// x/y are fractions of the plot: both given pins the node there.
{ id: 'core-1', label: 'Core', icon: 'router', x: 0.3, y: 0.15 },
{ id: 'core-2', label: 'Core', icon: 'router', x: 0.7, y: 0.15 },
{ id: 'emea', label: 'EMEA', icon: 'hub', x: 0.2, y: 0.8 },
{ id: 'amer', label: 'AMER', icon: 'hub', x: 0.5, y: 0.8 },
{ id: 'apac', label: 'APAC', icon: 'hub', x: 0.8, y: 0.8 },
],
});
The icons come from the grid, not from the chart. icon is a
name in the grid’s own sprite registry - a built-in, or one you registered - and the chart reads it through grid.icons, the grid it is bound to. That is
deliberate and it is the only route that works: the charts module ships as its own bundle, so
an import of the registry there would hand the chart a second, empty
copy, and a glyph you registered would be invisible to it. One registry, reached through the
one object both sides already share. Unknown names warn once and draw a plain disc rather
than nothing.
Pinning steers the layout without replacing it. A node with both
x and y is a fixed body: it still pushes its neighbours apart and
still pulls on its links, and the integration step skips it. Everything unpinned settles
around it by the same deterministic relaxation as before - and pinning one node does
not reshuffle the others, because the layout’s seeding draws for every node whether it
is pinned or not, precisely so that it cannot. Half a position is not a position: a node with
only x is laid out.
Parallel links are not summed. Three rows between the same pair are three
lines, offset 4 px apart, symmetric about the pair’s own line, in row order. One
line carrying their total would be a number nothing measured - three circuits at 40% do
not make one at 120% - and the pointer picks out the line you are over rather than the
pair, so each one’s own value is readable. Links are undirected: no arrowheads, and
A,B is the same pair as B,A.
There is no chart-level threshold option, on purpose. The colour comes
from grid.formatting.styleFor(col, value) - background, then
backgroundColor, then color; a gradient is not a colour and is not
read. A second place to say “red above 80” is a second place for the chart and the
cell to disagree. The legend lists only the rules that fired, with their own labels and
swatches, and changing a rule recolours the links on the next frame without the layout
re-running, so nothing moves.
Clicking acts on rows, because a link is a row. 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 marks and dims the
rest. The types whose marks are aggregates are deliberately untouched: selecting the
forty rows behind a bar is not what a click on a bar means.
The glyphs those nodes draw are yours to supply. icons on the grid configuration
registers SVG sprites by name, before the first paint, into the same registry the built-in
chevrons and sort arrows live in - so a name you register is usable anywhere a glyph
name is: a column’s icon decoration, a rail action’s
icon, a network node’s icon. Registering a built-in name
overrides it, which is how the expander chevron becomes your own mark. A sprite is a view box
and its path data; one filled path on the house 16×16 box is the common case, so a bare
path string is read as exactly that. grid.icons reads the registry back.
Registering your own glyphs, executed
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { root } = createTestDom();
const { createGrid } = await import('../packages/dom/src/index.js');
const grid = createGrid(root, {
rowKey: 'id',
rows: [{ id: 'core-1', load: 41 }],
columns: [{ field: 'id' }, { field: 'load', type: 'number' }],
// Your own sprites, by name, alongside the built-in set.
icons: {
router: { viewBox: '0 0 16 16', paths: ['M2 6h12v6H2Z', 'M5 6V3h6v3'], paint: 'stroke' },
hub: 'M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Z', // one filled path is enough
},
});
// Read back what the registry holds: the normalised glyph, whichever form registered it.
const router = grid.icons.get('router');
const hub = grid.icons.get('hub');
grid.destroy();
return `router ${router.paths.length} ${router.paint} | hub ${hub.paths.length} ${hub.paint}`;
Showing what was deleted
A deletion is a change too
diff: { snapshot: lastApprovedVersion, removedRows: 'pinned' } // or 'data'
The row is gone from the data and still in the snapshot. That is the only place it exists, and it is what these rows are built from, so they carry the values they had when the snapshot was taken, not any current ones.
One option answers both questions: whether to show it, and whether it
counts. 'pinned' puts it beneath the rows, struck through and dimmed, outside the
row set, so rows.count(), exports and selection all pass over it.
'data' appends it to the set instead, and all three include it. Omitted, nothing
is shown and the grid is the one you already had.
Neither sorts or filters it among the live rows. A removed row's values
are yesterday's; ordering them among today's presents two data sets as one, and lets a filter
written for current values decide the fate of historical ones. Neither permits an edit either,
a write aimed at a removed row is refused and returns 0, rather than being
counted as applied against a record that is not there.