Lattice Grid Buy a licence

developer guide

Presentation Mode for a JavaScript Data Grid

Presentation mode scales the grid for a room rather than a desk, with roomier density, the chrome out of the way, and a saved view applied so the table opens on what you came to talk about.

Developer guidePresenting and saved views › Presentation Mode for a JavaScript Data Grid

Presentation mode

Renders the grid for a room: full-screen, application chrome hidden, everything enlarged. The data stays live and queryable throughout, so a question from the audience is answered by filtering in front of them rather than promised as a follow-up.

Starting and stopping

grid.presentation.start();                  // 1.5x by default
grid.presentation.start({ scale: 2 });
grid.presentation.start({ chrome: ['statusBar'] });   // keep some chrome

grid.presentation.nudge(1);                 // live, or Ctrl/Cmd +
grid.presentation.setScale(1.8);
grid.presentation.stop();                   // or Escape
KeysDoes
EscapeLeave, restoring the grid exactly as it was.
Ctrl/Cmd + = / -Enlarge or reduce live, a laptop on a call and a projector at the back of a room are different problems.
Ctrl/Cmd + 0Back to the default enlargement.

The scale multiplies your density, it does not replace it. A grid built at spacious presented at 1.5x is still recognisably that grid, half as big again, which is what makes a presentation look like the product rather than like a different one. Virtualisation follows the enlargement, so rows are positioned at the size they are drawn.

Full-screen is the maximiser, not a second implementation. A grid the user had already maximised stays maximised when the presentation ends: leaving it would be undoing something the presentation did not do. Chrome hidden on entry is recorded and put back, so an element the host had already hidden is not revealed on exit.

Events are presentation:started, presentation:ended, presentation:scale and presentation:changed: colon-separated like every other grid event rather than the camelCase the original brief used, so a host subscribing to them does not have to remember which family a name belongs to.

Views are the slides

grid.presentation.start({ views: ['escalations', 'at-risk', 'margin-watch'] });
grid.presentation.step(1);          // or an arrow key, space, Page Down
grid.presentation.goTo(0);          // or Home / End
grid.presentation.reset();          // or R: back to the view as saved
KeysDoes
→ ↓ Space PageDownNext view.
← ↑ PageUpPrevious view.
Home / EndFirst or last.
RPut the current view back as saved, discarding anything sorted or filtered since arriving at it.

They are the saved views you already have. A view captures the column set, order, widths, filters, sorts, grouping and density; stepping applies each through the ordinary views.apply, as a single undo entry. Nothing about presenting changes what a view means.

The stepping keys only bind when there is a sequence, and never while something is being typed into. Without a deck those keys belong to the grid, a presenter with no slides still expects Page Down to scroll, and a quick filter answering a question from the room must not advance the deck on the space bar.

Stepping past either end sits there. It does not wrap: a presenter who sees the first slide again thinks the deck has restarted.

Transitions are a cross-fade, not continuous row motion. Rows are pooled and virtualised, so an element holding a row before a view change may hold a different row after it: only rows visible in both states could be animated between positions, and half a movement draws the eye to whichever rows happened to survive rather than to the change itself. prefers-reduced-motion removes it; a projected fade is far larger than one on a laptop, so someone who asked for less motion meant it.

Spotlight, redaction and unattended cycling

// light one row across two columns; everything else recedes
grid.presentation.setSpotlight({ keys: ['R42'], colIds: ['margin', 'utilisation'] });
grid.presentation.setSpotlight(null);        // after the point is made

// a wall display cycling saved views with nobody at the keyboard
grid.presentation.start({ views: [...], autoAdvance: 15000 });

// keep some chrome
grid.presentation.start({ chrome: ['statusBar'] });

Spotlight dims what it is not on, rather than lighting what it is. Rows and columns combine as an intersection, so naming both lights the cells where they meet. The dimming stops at 0.28 rather than going further: the audience has to see that there is more data and roughly what shape it is, or the spotlight reads as a filter and the room believes the other rows are gone. It is opacity alone, so a dimmed sparkline keeps its colours instead of flattening to grey.

A spotlight does not survive a view change. It belongs to the point being made, not to the deck: carried forward, it leaves the audience looking at a lit row that no longer means anything.

Redaction travels in views and undo. It is part of grid state, so a saved view carries its own masking and a view that redacts salary redacts it every time it is shown. Toggling is a tracked action, so it undoes like any other change.

Auto-advance wraps, unlike a keypress. An unattended display that stopped on the last view would show one screen for the rest of the day.

Escape ends the presentation, not just full screen. A presentation runs full screen with its chrome hidden, so leaving full screen without ending it would drop a chrome-less enlarged grid back into the page with no control left to turn it off. The maximiser stays the only listener on the key and the presentation follows it, which keeps one Escape doing one thing: an open editor or menu still closes first. A grid built with maximise: false binds the key directly instead.

Capturing a still

const blob = await grid.capture({ scale: 2 });
await grid.capture({ scale: 3, fileName: 'q3-margins.png' });   // and save it

grid.on('presentation:captured', (e) => {
  console.log(e.width, e.height, e.bytes, e.mimeType);   // 1800 600 41030 'image/png'
});

Mounting the bar elsewhere needs the grid's class. Every rule that styles the prompt bar is scoped under .lattice, and every colour token is declared there, so a bar mounted into your own chrome through ai.element arrives unstyled. Add class="lattice" to the container, and the same data-theme the grid carries, if you have set one, and it picks up the theme.

It photographs the browser's own rendering. The grid is cloned, every computed style is inlined onto the clone, and the result is wrapped in an SVG foreignObject and drawn to a canvas, so the picture is what the browser drew, not a second renderer's guess at it. That matters here more than usual: every decoration, sparkline and pill the cell layer produces comes out right without being reimplemented.

Virtualisation makes it cheap. Only the rows on screen exist in the DOM, so capturing a million-row grid clones the thirty rows a camera could have seen anyway. A full-screen capture at scale: 2 takes around a second.

Cross-origin images are refused before the work starts. They taint the canvas, and a tainted canvas fails at the very last step with a SecurityError that names nothing, so the check runs first and the error names the offending URL. Serve the image same-origin, inline it as a data: URL, or hide the column.

Two further limits, both inherent to the technique: web fonts need embedding to appear (Lattice's default system-ui stack is unaffected), and CSS pseudo-elements are not captured.

Drawing over the grid

grid.annotate.use('pen');                    // pen · arrow · rect · highlight
grid.annotate.use('arrow', { colour: '#e0245e' });
// A durable text label anchored to a cell, seeded or added.
grid.annotate.add({ type: 'text', text: 'Q3 spike', points: [{ x: 232, y: 72 }], background: '#fffbe6' });
grid.annotate.undo();
grid.annotate.clear();
grid.annotate.use(null);                     // hand the grid back

It never touches data. Nothing in the layer reads a row or writes one. A grid with annotations sorts, filters and exports exactly as one without them.

It is inert unless a tool is chosen. The canvas is not even created until the first use(), and carries pointer-events: none whenever no tool is active, so scrolling, selection and editing pass straight through. A presenter who has finished drawing must not discover the grid has stopped responding.

Marks are anchored to the data, not the screen. They are stored in content coordinates and redrawn with the scroll offset subtracted, so a circle drawn round a cell travels with that cell rather than hanging over whatever scrolled underneath it.

They are transient. Marks annotate a moment, so they are cleared when the presentation ends. A capture taken while they are on screen includes them, the canvas bitmap is carried into the still deliberately, because cloneNode copies a canvas element and not one pixel of what was drawn on it.

No tool shortcuts are bound. The keys a presenter would want are already taken by stepping and by the grid itself, and a shortcut that silently shadows Page Down is worse than one the host chooses. Bind your own to use().