api reference
Presentation and timeline: JavaScript Data Grid API
grid.presentation, grid.annotate, grid.timeline, grid.maximise, and grid.overlay.
API reference › Presentation and timeline
grid.presentation
Enlarges the grid, drops the chrome and steps through saved views, for a screen share or a room. State only at this level: full screen and the pixels are the DOM layer's, so a headless grid can still be put into presentation state and asked about it.
grid.presentation.start({ scale: 1.5, views: ['q3', 'q4'] });
grid.presentation.step(1); // next view
grid.presentation.setSpotlight({ colIds: ['revenue'] });
grid.presentation.nudge(1); // a little larger
grid.presentation.stop(); // or Esc
Esc ends the presentation, not just full screen: leaving one without the other would strand an enlarged, chrome-less grid in the page with no control left to turn it off. An open editor or menu still closes first.
| Method | Returns | Description |
|---|---|---|
| active | boolean | True while a presentation is running. |
| scale | number | The current enlargement. |
| start(options?) | boolean | Begin presenting. scale, views, chrome keep-list, interval for auto-advance. |
| stop() | boolean | Stop and put the grid back as it was. |
| setScale(value) | number | Set the enlargement, clamped to 0.5-4. |
| nudge(steps?) | number | Move the enlargement by steps, for the live keyboard adjustment. |
| options | object | The options the running presentation started with. |
| views | string[] | The view ids being stepped through. |
| index | number | Position in the sequence, -1 when there is none. |
| viewId | string | null | The view id currently shown. |
| step(by?) | number | Step forward or back through the sequence. |
| goTo(index) | number | Show a numbered position. |
| spotlight | object | null | What is currently lit. |
| setSpotlight(target?) | boolean | Light rows, columns or their intersection and let the rest recede. Call with nothing to clear. |
| reset() | boolean | Put the current view back as it was saved, discarding what the presenter has sorted or filtered since. |
Enlargement is a CSS scale factor multiplied into the same tokens density uses, so text, rows, padding and controls grow together rather than the grid being zoomed as an image. Font size is damped against it: type that scaled linearly with a 2× row height reads as shouting.
grid.annotate
The drawing layer over the grid: pixels on a transparent canvas, never data. A presenter picks a tool (pen, arrow, rect, highlight) and draws; the layer is inert until one is chosen, so scrolling and selection pass straight through otherwise. Marks are stored in content coordinates, so a circle drawn round a cell stays on that cell as the grid scrolls and resizes rather than hanging over the viewport.
Marks can also be seeded and added without drawing, which is what lets a host ship a pre-drawn callout or restore one from storage. A mark descriptor is { type, points, colour? } - type is freehand, arrow, rect, highlight or text (pen is accepted as an alias for freehand); points are {x, y} in content coordinates (a trail for freehand, the two endpoints for an arrow or rectangle, a single anchor for text). Seeded and added marks are durable: they survive a presentation ending, unlike a live-drawn mark, and they round-trip through getState and a saved view.
A text mark is a label anchored at one content point, carrying its text string and a basic style: colour, an optional fontSize in content pixels (default 14, scaled with a presentation), and an optional background colour drawn behind it. Like every mark it is held in content coordinates, so the label tracks the cell it annotates through scroll and resize.
// Seed a mark at construction - rendered on first paint, the way redaction seeds.
createGrid(el, {
columns, rows,
annotate: true,
state: { annotations: [
{ type: 'arrow', points: [{ x: 40, y: 120 }, { x: 220, y: 80 }], colour: '#e0245e' },
{ type: 'text', text: 'Q3 spike', points: [{ x: 232, y: 72 }], colour: '#1a6bc7', background: '#fffbe6' },
] },
});
// Or add one durably at runtime - no synthesised pointer input.
grid.annotate.add({ type: 'rect', points: [{ x: 40, y: 100 }, { x: 260, y: 160 }] });
grid.annotate.add({ type: 'text', text: 'review', points: [{ x: 48, y: 108 }], fontSize: 16 });
// Persist and restore: seeded and added marks come back out of the state.
const marks = grid.getState().annotations; // [{ type, points, colour }, …]
grid.state.apply({ annotations: marks }); // re-seed a fresh grid
annotate.add adds to the model and paints - it never synthesises pointer events, so a mark is exactly what the descriptor says. undo() removes the most recent mark and clear() removes them all, as before; annotation:changed still fires on every change. A presentation ending clears the presenter's live-drawn marks but keeps the durable ones, which are view state a host means to persist.
grid.timeline
Moves the grid back through recent data changes: what a row held a minute ago, before the number moved. It reads the change log rather than the undo history: history records what you did, and on a live grid the question is what the data did.
Nothing is scrubbable until attach(). What a value used to be is not recoverable after the fact, and reading a row per key on every change is real cost on a busy feed, so recording is off until you ask for it and the window fills from that moment.
grid.timeline.attach(); // start recording; a scrubber appears
grid.timeline.seek(5); // stand five changes back
grid.timeline.step(-1); // one further back
grid.timeline.at(); // the moment being shown
grid.timeline.toLive(); // return, applying everything stepped over
grid.timeline.detach(); // stop recording; the scrubber goes
| Method | Returns | Description |
|---|---|---|
| attached | boolean | Whether the scrubber is recording. |
| live | boolean | True when the grid is showing the present. |
| position | number | How many steps back the grid is standing. Zero is live. |
| depth | number | How many steps back it is possible to go. |
| attach() | void | Start recording what changes replace. |
| detach() | void | Stop recording and return to the present. |
| seek(steps) | number | Stand a number of steps back, 0 being live. Clamped, not refused, at both ends. |
| step(by) | number | Move relatively; negative goes back in time. |
| toLive() | number | Return to the present, applying everything stepped over. |
| at() | number | null | The timestamp being shown. |
| span() | object | null | { from, to }, the range the scrubber can move over. |
Cells whose value moved during a seek are marked and stay marked until the next seek, in --lattice-timeline-changed. On a wide row the change you are hunting for is easy to scroll past, and a flash you can miss helps nobody. Marking compares rendered column values rather than raw fields, so a computed column that moved because its inputs moved is marked too. Chart columns redraw as you scrub, like any other cell.
Value changes reverse; row additions and removals do not, a window containing them scrubs over the value changes and leaves the row set alone. While scrubbed back the grid is not live: changes keep being recorded but are not applied, and returning to the head applies everything missed. The delta renderer is the one cell type to keep off a scrubbed grid, because it samples on a wall-clock timer and reads a seek as a real movement.
grid.maximise
Fills the browser window with the grid, and puts it back. The rail's last button is
this; grid.maximise is the same thing, so an application can bind its own
control or keyboard shortcut. Esc restores.
grid.maximise.toggle(); // what the rail button calls
grid.maximise.enter();
grid.maximise.active(); // true while it fills the window
grid.maximise.exit();
| Method | Returns | Description |
|---|---|---|
| enter() | boolean | Fill the window. false when the host element is not in the document. |
| exit() | boolean | Back to the page. false when it was not maximised. |
| toggle() | boolean | Whether the grid is maximised afterwards. |
| active() | boolean | Whether it is filling the window now. |
The host element is moved to <body> and pinned to
the viewport, then moved back between the same two siblings. A position: fixed element is
positioned against the nearest ancestor carrying a transform, filter,
contain or will-change (any card, animated panel or sticky shell) so styling
alone fills the window on one page and lands in a small box on the next. A hidden placeholder holds the
vacated space at the size the grid had, so the page behind neither reflows nor loses its scroll position.
Geometry is applied as inline styles and every displaced property is handed back
exactly as it was found, because the element being restyled is yours. While maximised the element
carries .lat-maximised and <body> carries .lat-maximised-host,
as hooks for your own CSS.
Putting your own toasts, dialogs and popups above a maximised grid.
The grid sits at the --lattice-maximise-z token - 1000 by default, deliberately
far below the old near-ceiling value, so your chrome does not have to win a z-index arms race. Place the
toast or dialog one layer above it and it covers the grid:
.my-toast { z-index: calc(var(--lattice-maximise-z) + 1); }
Or lower the grid itself - per grid with maximise: { zIndex: 500 }, or site-wide by
overriding the token (.lattice { --lattice-maximise-z: 500; }). Either way the grid's own
popups - the column menu, the cell editor, the filter popup and the cell tooltip - keep opening above the
maximised grid, because they are inside it or read the same token one step above it. maximise:
{ container: el } fills el instead of the viewport, for a host that wants maximise to
mean its own modal or panel; the element should be position: relative so the grid fills it.
const { createTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { document } = createTestDom({ width: 800, height: 400 });
const host = () => { const el = document.createElement('div'); el.style.cssText = 'width:400px;height:200px'; document.body.appendChild(el); return el; };
// `container` fills a host element rather than the viewport; `zIndex` sets the layer.
const panel = document.createElement('div');
panel.style.position = 'relative';
document.body.appendChild(panel);
const h1 = host();
const one = createGrid(h1, { rowKey: 'id', rows: [{ id: 1, name: 'a' }], columns: [{ field: 'name' }], maximise: { zIndex: 500, container: panel } });
flushFrames();
one.maximise.enter();
const a = `z ${h1.style.zIndex} pos ${h1.style.position} in-panel ${panel.contains(h1)} active ${one.maximise.active()}`;
one.maximise.exit();
// The default is the --lattice-maximise-z token (1000) and fills the viewport.
const h2 = host();
const two = createGrid(h2, { rowKey: 'id', rows: [{ id: 1, name: 'a' }], columns: [{ field: 'name' }], maximise: true });
flushFrames();
two.maximise.enter();
const b = `z ${h2.style.zIndex} pos ${h2.style.position} in-body ${h2.parentNode === document.body} active ${two.maximise.active()}`;
two.maximise.exit();
two.destroy();
return `${a}; ${b}`;
grid.overlay
| Method | Returns | Description |
|---|---|---|
| show(kind, message?) | void | 'loading' or 'empty'. |
| hide() | void |
An emptyMessage that tells an empty dataset from one the filters emptied
const { createTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const dom = createTestDom({ width: 400, height: 200 });
const grid = createGrid(dom.root, {
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'region' }],
rows: [{ id: '1', region: 'north' }, { id: '2', region: 'south' }],
emptyMessage: (ctx) => ctx.reason === 'filtered' ? 'No rows match those filters' : 'No rows yet',
});
flushFrames();
const before = dom.root.querySelector('[data-overlay="empty"]') ? 'showing' : 'hidden';
grid.filters.set({ col: 'region', op: 'eq', value: 'nowhere' });
flushFrames();
const emptyEl = dom.root.querySelector('[data-overlay="empty"]');
const filtered = emptyEl ? emptyEl.querySelector('.lat-overlay-message')?.textContent : undefined;
grid.destroy();
return `before ${before} | filtered ${filtered}`;