Lattice Grid Buy a licence

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.

MethodReturnsDescription
activebooleanTrue while a presentation is running.
scalenumberThe current enlargement.
start(options?)booleanBegin presenting. scale, views, chrome keep-list, interval for auto-advance.
stop()booleanStop and put the grid back as it was.
setScale(value)numberSet the enlargement, clamped to 0.5-4.
nudge(steps?)numberMove the enlargement by steps, for the live keyboard adjustment.
optionsobjectThe options the running presentation started with.
viewsstring[]The view ids being stepped through.
indexnumberPosition in the sequence, -1 when there is none.
viewIdstring | nullThe view id currently shown.
step(by?)numberStep forward or back through the sequence.
goTo(index)numberShow a numbered position.
spotlightobject | nullWhat is currently lit.
setSpotlight(target?)booleanLight rows, columns or their intersection and let the rest recede. Call with nothing to clear.
reset()booleanPut 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
MethodReturnsDescription
attachedbooleanWhether the scrubber is recording.
livebooleanTrue when the grid is showing the present.
positionnumberHow many steps back the grid is standing. Zero is live.
depthnumberHow many steps back it is possible to go.
attach()voidStart recording what changes replace.
detach()voidStop recording and return to the present.
seek(steps)numberStand a number of steps back, 0 being live. Clamped, not refused, at both ends.
step(by)numberMove relatively; negative goes back in time.
toLive()numberReturn to the present, applying everything stepped over.
at()number | nullThe 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();
MethodReturnsDescription
enter()booleanFill the window. false when the host element is not in the document.
exit()booleanBack to the page. false when it was not maximised.
toggle()booleanWhether the grid is maximised afterwards.
active()booleanWhether 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

MethodReturnsDescription
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}`;