api reference
The grid
Constructing a grid, every configuration property, the column definition, each grid.* sub-API, the sources rows come from, and every event the grid raises.
API reference › The grid
Prefer a narrower walkthrough? The grid's own surface is also split into a page per namespace, each a shorter guided read than the full reference below: Rows, columns and data sources , Filtering, sorting and quick filter , Editing, selection and history , Renderers, formatting and charts , Statistics and units , Export, import, state and saved views , Presence, comments and redaction , Presentation, timeline and maximise , Events , The model layer, diagnostics and licence , Accessibility .
All 13 pages Everything on one page → Developer guide →
Construction
Two files are all you need: a stylesheet and a script. Nothing is fetched at runtime (no CDN, no font, no icon sprite) however the two files themselves got there.
<!-- Script tag, from your own build. Everything is on one global. -->
<link rel="stylesheet" href="dist/lattice-grid.min.css">
<script src="dist/lattice-grid.min.js"></script>
<script>
const grid = LatticeGrid.createGrid(document.getElementById('grid'), config);
</script>
Or straight from jsDelivr, no npm install, no bundler, no local copy at all:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/lattice-grid.min.js"></script>
<script>
const grid = LatticeGrid.createGrid(document.getElementById('grid'), config);
</script>
Or as modules, importing the bundle by path: your own build, npm, or jsDelivr:
// With a renderer, in a browser. Any of:
import { createGrid } from './dist/lattice-grid.esm.js';
// import { createGrid } from '@toclocoinc/lattice-grid';
// import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/lattice-grid.esm.min.js';
const grid = createGrid(document.getElementById('grid'), config);
// Headless: the same API without a renderer. Data, filters, sort,
// grouping, totals, formatting and export all work; grid.element is
// null and the DOM-only chrome is simply absent.
// Runs in Node, for tests and server-side export.
import { createHeadlessGrid } from '@toclocoinc/lattice-grid';
const grid = createHeadlessGrid(config);
jsDelivr mirrors every version published to npm at
cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/<file>. @1 pins the
major: a page in production picks up fixes within 1.x and never a breaking release, where
@latest would. To freeze a page on one exact build, replace @1 with the
full version getVersion() reports. The same convention reaches any module:
.../modules/htmx.esm.min.js, .../modules/react.esm.min.js, and so on.
Configuration properties
Every key is settable at runtime through grid.set(key, value). Keys marked
dom are read by createGrid and ignored by a headless grid.
Data and structure
| Property | Type | Default | Description |
|---|---|---|---|
| columns | (Column | ColumnGroup)[] | , | Column definitions. Groups may nest. |
| columnGroups | ColumnGroup[] | , | Header grouping declared separately from the columns. |
| rows | unknown[] | , | Row objects. The array is copied on ingest; the row objects in it are not. The grid keeps a shallow copy of the array you pass here (and of source.rows, and of the array given to rows.load()), so your array is never written to: after rows.apply, a sort, a group or an edit it holds exactly what it held when you passed it, and two grids built from one array are independent. The objects inside it are still yours - row.data is the object you supplied and rows.data() returns those same objects, so identity round-trips (see ingest.retainSource). rows.apply({ update }) does not write through either: it merges into a new object, which replaces that slot in the grid's copy only, so row.data for an updated row is a new object and the one you passed is untouched. An in-place cell edit does: edit.setCells, or typing in a cell, writes the new value into the shared object, which is the other face of row === sourceObject. ingest.retainSource: false and ingest.dropSourceRows opt out of sharing altogether. |
| rowKey | string | (row) => string | , | Stable row identity. Without it the grid assigns a key per row object and warns: enough for sorting, filtering, selection and copying within a session, but change tracking, streaming dedupe, selection persistence and remote reload all switch off, because new objects are new rows. If it is configured but resolves to nothing for some rows - a field absent, or present on some rows only - those rows collapse onto one key and the grid warns once, naming the field(s) and how many rows were affected, on rows.load() as well as at construction. |
| source | SourceConfig | memory | Where rows come from: memory, paged, remote or stream. See Sources. |
| ingest | IngestConfig | , | { retainSource, dropSourceRows }. How rows enter the column store. retainSource defaults to true: the caller's row objects are held by reference so rows.data() returns them unchanged and row === sourceObject holds. Set it false to stop the store retaining them and reconstruct a row on demand - but the source layer and grid config still hold the array, so the resident footprint does not actually fall. dropSourceRows: true closes that gap: it releases the objects from the source layer too, so the packed columns become the only copy and the footprint drops by roughly an order of magnitude at scale. Either way rows.data() then returns freshly reconstructed objects, so identity checks and row.sourceObject no longer hold and equality becomes value-based. Cell values are unchanged. |
| tree | TreeConfig | , | { path } or { parentKey }, plus label, orphans. Rows form a hierarchy. See Tree data. |
| detail | DetailConfig | , | { rows, config, render, isMaster, height, cacheLimit, target }. A master row expands into a nested grid, inline or into an element you supply. See Master-detail. |
| context | unknown | , | Arbitrary value passed to every callback, so formatters and renderers need no closures over app state. |
Defaults and registries
| Property | Type | Description |
|---|---|---|
| columnDefaults | Column | Merged under every column before its own definition. |
| columnPresets | Record<string, Column> | Named bundles applied with preset: 'money'. |
| dataTypes | Record<string, DataType> | Custom types. Registered ahead of the built-ins, so a name here overrides one of ours. |
| sampleSize | number | Values read per undeclared column when inferring its type. Default 100. |
| targetSize | 'default' | 'large' TargetSize | Raises every interactive target to a comfortable size for touch, leaving the type alone. Applied automatically on a coarse pointer; 'default' opts out of that. |
| components | Record<string, Ctor> | Renderers, editors and filters addressable by name. |
| pipes | Record<string, fn> | Template pipes for cell.template. |
| totalFns | Record<string, TotalFn> | Custom aggregations, addressable from column.total. |
| variants | Record<string, VariantDefinition> | Semantic colour tokens for decorations. |
Behaviour
| Property | Type | Default | Description |
|---|---|---|---|
| selection | SelectionConfig | 'single' | 'multiple' | 'none' | , | Object form adds checkbox (a pinned column of row checkboxes), headerCheckbox (tri-state select-all in its heading), checkboxOnly (only that column may change selection - for a row with its own click action), groupSelectsChildren, ranges, fillHandle, fill. See Selection and ranges. |
| edit | EditConfig | boolean | , | { enabled, mode: 'cell' | 'row', start: 'single' | 'double' | 'key', enterMovesDown, undoDepth, commit, confirm, pendingTimeout, pastePreview }. commit/confirm/pendingTimeout turn on optimistic writes; pastePreview (default off) shows a confirm/cancel diff before a bulk paste commits. |
| pagination | PaginationConfig | boolean | , | Local or remote paging. |
| quickFilterText | string | , | Initial quick-filter term. Equivalent to grid.filters.quick(text). |
| hostFilter | { active(), passes(row) } | , | An application-level predicate composed with the grid's own filters. |
| pivot | object | , | { enabled, groupTotals, totalsLabel, maxColumns, separator }. groupTotals: 'before' | 'after' adds a column group totalling every value column across all pivot values, at the near or far edge; omitted, it adds none. totalsLabel heads it, defaulting to Total. maxColumns defaults to 500, counts the totals group, and fails with a message rather than locking the browser. |
| grandTotalRow | boolean | 'bottom' | false | true puts it inline at the end of the rows; 'bottom' pins it above the status bar. Maintained incrementally on a memory source: see Grouping, totals and pivot. |
| pinnedTopRows | object[] | , | Rows held above the scrolling body. Rendered through the ordinary column pipeline, but not part of the data: not counted, sorted, filtered, grouped, selectable or exported. See Pinned rows. |
| pinnedBottomRows | object[] | , | As pinnedTopRows, held below the body instead. Sits under the grand total when both are shown. |
| fullWidth | { when, render } | , | Draw matching rows as one band across every column instead of dividing them into columns, a section banner, a note, a “load more” affordance. when(row) picks them, render(params) fills them. Still ordinary data rows in every other respect. See Full-width rows. |
| groupRenderer | (params) => string | Node | void | , | Draw the group row yourself - a section header with a chevron, a rollup, a count, a progress bar - instead of the grid's expander-and-label. The row is drawn as one band across every column and no ordinary cells are mounted underneath it. Return an HTML string, a node, or write into params.element. A string is inserted as markup here, unlike fullWidth.render, because a group heading is synthesised by the grid and has no data row: the string can only be your own template, the same contract the board's cardRenderer has. The renderer is handed the group key, the grouped column id, the value, the level, the expanded state, leafCount, the group's totals and leaves() for the rows themselves. Mark any element in your markup data-lat-group-toggle to make it expand and collapse the group. See Group rows you draw yourself. |
| groupDefaultExpanded | boolean | number | (group) => boolean | , | Which groups start open before anyone has touched one. true (the default) opens every group, false closes every group, a number opens the first N levels (0 closes everything, a negative opens every level), and a predicate answers per group - the current sprint open while the rest start closed. It is handed { key, column, value, level, path }. Only ever consulted for a group nobody has expanded or collapsed: once the user or your code decides, that decision stands. See Group rows you draw yourself. |
| groupFooter | boolean | false | A closing total row per group. |
| totalFilteredOnly | boolean | true | Totals reduce the filtered set. false totals the whole dataset, group totals included. See Grouping, totals and pivot. |
| totalOnlyChangedColumns | boolean | false | Reduce only the totalled columns an edit actually changed. Off by default, it asserts that each total depends on nothing but its own column. See Grouping, totals and pivot. |
| showTotalInHeader | boolean | true | Under grouping or pivot, a totalled column's heading names its reduction on a line above the column name. See Grouping, totals and pivot. |
| aggregateChooser | boolean | false | Let the user pick a column's reduction from the column menu. On, the totalling entry becomes an Aggregate submenu offering only the aggregates the column's type says are meaningful (sum, average, min, max, count and so on - never sum on a category column), with the current one ticked and a None to stop totalling; it is keyboard-operable through the standard menu and drives grid.columns.setTotal(), reusing the existing reduction model. Off by default and non-breaking: the menu keeps its plain Total this column toggle. See Grouping, totals and pivot. |
| allowUnsafeTemplates | boolean | false | Off by default. Templates are escaped unless this is explicitly set. When set, an interpolated value may contain presentational markup, but script is still removed from it: <script>, <iframe> and the other executable tags, on* handler attributes, and javascript: URLs. The flag permits markup, not code. |
| licence | string | , | Signed licence key. Removes the trial watermark; unlocks nothing, because nothing is locked. |
Presentation
| Property | Type | Default | Description |
|---|---|---|---|
| messages | object | en-GB | Replaces the grid's own text: labels, menus and screen-reader announcements. A partial catalogue laid over the built-in British English one, so anything you leave out stays in English. Twenty catalogues are bundled: EN_GB, EN_US, FR_FR, FR_CA, IT_IT, ES_ES, PT_BR, DE_DE, NL_NL, SV_SE, DA_DK, NB_NO, FI_FI, PL_PL, CS_CZ, HU_HU, RO_RO, UK_UA, EL_GR, JA_JP and AR. They are exports of the package, not separate files, so importing one does not reduce what is bundled. EN_US is a partial overlay carrying only what differs from British English. AR_SA is an alias for AR: the Arabic catalogue is pan-Arabic, and a region appears in a name only where two variants ship. resolveCatalogue(tag) finds the catalogue for any tag, so resolveCatalogue('es-MX') returns the Spanish one. Every key is listed in MESSAGE_KEYS; auditCatalogue() reports what a catalogue of your own is missing. |
| locale | string | runtime | BCP-47. Drives every formatter and one shared Intl.Collator. |
| direction | 'ltr' | 'rtl' | 'auto' Direction | auto | Writing direction. Left unset, it follows the element's computed dir and then the locale, so locale: 'ar' renders right to left without further configuration. Set it explicitly to override both. |
| theme | 'light' | 'dark' | 'high-contrast' | 'terminal' Theme | , | Stamped as data-theme on the grid's root. Unset follows the viewer's prefers-color-scheme. See Theming. |
| density | 'compact' | 'standard' | 'comfortable' | 'spacious' | number Density | 'compact' | One scale that every geometry token derives from: row height, spacing, decoration sizes, and type at a damped rate. Row heights are 23.8 / 28 / 42 / 56px. A number scales 28px, so 1.4 gives 39.2px for anything between the presets. Virtualisation follows it; an explicit rowHeight overrides it. |
| rowHeight | number | (row) => number | 28 | A function enables variable-height rows. |
| headerHeight | number | 32 | Per header row. |
| title | string | , | A caption drawn above the column headings. Inside the grid rather than an element placed above it, so it scrolls with the grid, sits in the region a screen reader announces, and is kept by image capture and print. |
| showHeader | boolean | true | Draw the column headings at all. false removes the row, and removes it from the accessibility tree rather than only from view. Distinct from showColumnFunctions, which keeps the headings and drops only their sort, filter and menu controls. |
| overscan | number | 4 | Rows rendered beyond the viewport. |
| autoHeight | boolean | 'visible' | , | Size rows to their content: cells wrap instead of ellipsising, and each row takes the height its tallest cell needs. Only rendered rows are measured either way, the difference is that true stops measuring above 10,000 rows and returns to fixed heights, while 'visible' keeps measuring at any size and accepts a scrollbar that shifts as rows are measured on the way past. |
| columnVirtualisationAbove | number | 30 | Column count above which columns virtualise too. |
| state | GridState | , | Restore a saved view at construction. |
Performance
| Property | Type | Default | Description |
|---|---|---|---|
| useWorker | boolean | true | Compute column distributions off the main thread. Sorting, filtering and grouping run on the main thread. |
| workerThreshold | number | 50000 | Row count above which a distribution is sent to the Worker. |
| workerUrl | string | , | External worker file, for a CSP that forbids blob:. Settled when the Worker is built; changing it rebuilds one. |
| sharedMemory | boolean | false | Pass columns to the worker in a SharedArrayBuffer instead of copying them, where the page is cross-origin isolated. Retains a shared copy of each column that crosses. |
Chrome dom
| Property | Type | Description | |
|---|---|---|---|
| statusBar | boolean | { panels } | Composable panels along the bottom. Default set: rowCount, selectedCount, aggregation, comments, updates, progress. Each is silent when it has nothing to report. | |
| maximise | boolean | true | false removes the rail button and grid.maximise, for an application with its own full-screen mode. |
| toolPanel | boolean | object | Side dock. panels: columns, filters, views, quick, formatting, statistics, regression. side: 'left' makes it the icon rail, which also turns on actions (undo, redo, pause, restore, maximise, then the export group: export, excel, clipboard, print: nine in all, and an array takes these names rather than the button labels) and icons. An explicit array replaces that list rather than extending it; a bare '-' in it renders a divider between groups. exportName names the CSV. annotate is a three-state option, not a boolean flag. annotate: true adds the native annotation tools - pen, arrow, rect, highlight - to the rail as toggle buttons (pressed while in use, pressed again to exit), and they stay put whether or not a presentation is running. annotate: false opts OUT: none of the four tools are ever added, presentation or not. Omitting annotate keeps the default: the tools are off until a presentation starts, appear for its duration, and leave when it ends. | |
| groupPanel | boolean | object | A drag-and-drop group-by strip above the column header - the row-group panel. Drag a heading into it to group by that column; the active groups show as removable, reorderable chips, and reordering the chips changes the nesting order. It is keyboard-operable - arrows move between chips, Shift with an arrow reorders, Delete ungroups, and an add control groups any column - and every change is announced through the live region. Off by default and non-breaking; it drives the same model as grid.columns.group() and reimplements nothing. The object form takes hint, the placeholder shown while nothing is grouped. | |
| kpis | StatConfig[] | A built-in KPI/stat strip: a labelled band of stat tiles the grid places for you above the column header. Each entry is a createStat spec - of, fn, title, interval, footer, format and the rest, minus grid and container, which the grid supplies - so a strip tile and a hand-placed one are the same object. The tiles follow the grid's filters, recomputing on every change like a stand-alone stat does. Off by default and non-breaking; it reuses createStat and reimplements no compute. | |
| timeZone | string | An IANA zone every date column formats and parses in, so a grid shows one zone whatever the viewer's machine says. Individual columns may override it. | |
| formulaFunctions | object | Your own functions, added to the formula language by name. The built-in list is closed on purpose; this is the one way in, and a function you add is called exactly as a built-in is. | |
| formatting | object | Conditional formatting rules to seed, keyed by column id or '*'. The same shape grid.formatting.all() returns, so a saved view can be handed straight back. | |
| facets | boolean | object | Header histograms that double as a filter. collapsed, height, and per-column strategy and buckets. | |
| updates | object | How a live feed behaves: batching, the queue that holds while paused, and the highlight a changed cell flashes. | |
| comments | object | Threaded cell comments: storage, the current author, and whether the indicator shows on an unread thread. | |
| presence | object | Live cursors, selections and edit locks. Carries intent and never values; see grid.presence. | |
| environment | function | Extra fields for the diagnostics bundle: build number, tenant, region. Called when a bundle is taken, never on the render path. | |
| contextMenu | boolean | (p) => MenuItem[] | Right-click menu. The function form is (params, defaults) => items: see custom items. false suppresses it: what a read-only grid wants, since the default menu offers Paste, Clear and Fill down. A column takes its own contextMenu (also accepting a bare MenuItem[], which is appended after the grid-level items), which composes onto this one as a chain and outranks it on suppression. | |
| columnMenu | boolean | (p) => MenuItem[] | The header's 3-dot menu, and a right-click on a column heading. The function form is (params, defaults) => items, with params carrying colId, column and grid: see custom items. false suppresses it. | |
| rangeChart | fn | { onChart } | boolean | Off by default. Offers Chart selection in the cell menu and binds Alt+F1 when a selected range has a number to plot. The DOM layer draws no charts, so the handler you give - a function, or { onChart }, called (grid, range) - is where the page wires in chartRange from the charts module. | |
| shortcuts | boolean | true | The ? keyboard shortcut overlay. false suppresses it, for a host that wants ? for itself. See Keyboard. |
| find | boolean | FindConfig | true | The in-grid find bar: Ctrl+F (Cmd+F) with focus in the grid opens it; typing highlights every matching cell in place without filtering a row away; Enter / Shift+Enter step through the matches. { shortcut, debounce }: shortcut: false keeps the bar reachable through grid.find.open() only; debounce is the typing quiet period in ms (120). false removes the bar and the binding; grid.find(text) still searches. See Find. |
| rowReorder | boolean | { column } | , | Let a user reorder rows by dragging a handle or with Alt+Shift+arrows. The handle goes in the first visible column unless column names another. Refused, with a reason announced, while a sort, filter or grouping is active. See Row reorder. |
| rowTransfer | boolean | { send, receive, mode, group } | , | Let rows be dragged between grids. Off by default. send and receive are both on when present, so one-way is { receive: false } or { send: false }. mode: 'copy' leaves the row behind; group restricts which grids may exchange. See Moving rows between grids. |
| alignedGrids | Grid[] | , | Other grids to stay column-aligned with. Widths, order, visibility, pinning and horizontal scroll are shared; sort, filters, selection and rows stay independent. Declare it on the grid created last. See Aligned grids. |
| stickyGroupHeaders | boolean | number | { depth } | false | Keep the enclosing group headings pinned above the viewport while scrolling inside a group. Off by default; true turns it on and stacks at most two, a number sets the cap, and each costs a row of viewport. See Sticky group headings. |
| gridLines | boolean | 'both' | 'horizontal' | 'vertical' | 'none' | 'horizontal' | Which rules are drawn between cells. Horizontal is what the grid has always drawn; vertical rules are additive. 'rows' and 'columns' are accepted aliases. Only the rules between data are affected, the header underline and pinned seams are structure. |
| cornerRadius | boolean | number | string | , | Round the grid's outer corners. true adopts the theme's radius, a number is pixels, a string is used as written. |
| stripedRows | boolean | false | Shade alternate data rows (zebra striping). Strictly opt-in, so an existing grid is unchanged on upgrade. Parity follows each row's logical index, so a stripe survives a scroll; group headings, footers and the grand total are never striped; selection and hover still win. Uses the theme's --lattice-surface-alt, so dark, high-contrast and terminal come for free. |
| verticalAlign | 'top' | 'middle' | 'bottom' VAlign | , | Vertical alignment of cell content within a row, as a default for every column - the vertical counterpart to the per-column align. A column's own verticalAlign (or cell.verticalAlign) overrides it. Omitted, the grid keeps its historical placement (centred in a fixed-height row, top in an autoHeight row), so an existing grid is unchanged on upgrade. Setting a value aligns every column uniformly, including auto-height rows, unless a column opts out. See Vertical alignment. |
| tooltip | TooltipConfig | , | { delay, maxWidth } - grid-level defaults for the rich cell tooltip. delay is how long the pointer or the keyboard cursor must rest on a cell before anything is built, 400ms by default; maxWidth is how wide the tooltip may grow (a number is pixels, a string is used as written). Defaults only: it switches nothing on, and a grid whose columns declare no cell.tooltip has no tooltips whatever is set here. See Rich cell tooltips. |
| scrollbars | 'auto' | 'always' | 'custom' | { x, y } | 'auto' | How the scroll viewport's scrollbars are drawn. 'auto' is the platform's native behaviour, where overlay scrollbars fade when idle; 'always' keeps that native bar shown whether or not the pointer is over the grid; 'custom' makes the grid draw its own bar instead - always visible, the same in every browser, and sized by the --lattice-scrollbar-* tokens rather than by the platform, for a target bigger than a 7px overlay ribbon. Scrolling itself is unchanged in every mode. The object form { x, y } sets each axis on its own, so { y: 'always' } keeps the vertical bar while the horizontal one stays native; note that 'custom' on one axis hides the native bar on both, and the grid warns once when the two disagree. Omitted, the grid is unchanged on upgrade. See Always-visible and grid-drawn scrollbars. |
| columnTagFilter | boolean | { multiple, label } | , | A bar above the headings for showing only the columns carrying a chosen tag. Draws nothing unless some column has tags. See Column tags. |
| rowTemplate | string | { template, cardsPerRow, maxCardWidth, gap, className, role, itemRole } | , | Draw each row with a template instead of dividing it into columns, a card list, a feed, a search-result list. Compiles once; binds with {{data.field}}. cardsPerRow or maxCardWidth puts several on a line. The pipeline underneath is unchanged. See Cards, lists and feeds. |
| gallery | boolean | { template, tileWidth, tileHeight, cardsPerRow, gap, className, role, itemRole } | , | Present rows as a gallery of tiles, laid out by the same 2-D virtualisation the grid runs. true generates a tile per row from the columns; tileWidth sizes them and the count across follows the container, or cardsPerRow fixes it. Presentation only - sort, filter, group and export are unchanged. See Cards, lists and feeds. |
| recordCard | boolean | { template, cardHeight, className, role, itemRole } | , | Present each row as a record card - a form of label/value pairs, one line per column in display order, showing the same text the table shows. true generates the form from the columns. A card list underneath, so it inherits the virtualisation and every card interaction. Presentation only. See Cards, lists and feeds. |
| board | boolean | { template, laneWidth, cardHeight, laneGap, gap, className, role, itemRole } | , | Present rows as a board - a kanban of grouped lanes of cards. The top-level group becomes a lane and every leaf under it becomes a card stacked in it; group the grid to give the board its lanes. true generates a card per row from the columns. Both axes are virtualised, the lanes across and the cards down each, so a board of many long lanes draws only what is on screen. A board card is still a row: it clicks, selects and drags through the grid's own handlers. Presentation only - sort, filter, group and export are unchanged. See Cards, lists and feeds. |
| responsive | { maxWidth, template, rowHeight } | , | Collapse to cards when the container is at or below maxWidth (640 by default), and return to a table above it. Sorting, filtering and export keep working. Emits presentation:changed. See Cards, lists and feeds. |
| rowForm | boolean | { mode, load, fields, title, width, trigger, timeout, container } | , | Open a row for editing on a form. mode: 'drawer' (default) or 'dialog'; without load the fields are the grid's own columns. A field entry is { field, label, editor, type, props, lookup }: any editor, including your own. Takes double-click on the row unless trigger: false. A load that has not answered within timeout milliseconds (2000; false waits indefinitely) is reported as a failure with a retry. container builds the form in an element of your own instead of over the grid. See Editing a row on a form. |
| showColumnFunctions | boolean | true | false leaves each heading as its label, with no sort, filter or menu control. Those remain reachable through the API, the keyboard and the tool panel. |
| headerControls | 'hover' | 'always' | 'hidden' | 'none' HeaderControlsVisibility | 'hover' | When the per-column header controls - the sort arrow, the filter funnel and the menu button - are shown, as a default for every column. 'hover' reveals them on hover or keyboard focus (the historical behaviour); 'always' keeps them visible; 'hidden' draws none of them for a clean read-only heading, leaves them out of the tab order, but still shows a read-only sort badge on a column that is actually sorted; 'none' goes further and shows the title only, whatever the grid's state - not even 'hidden''s sort badge, though aria-sort keeps reporting the truth. A column's own headerControls overrides this default for that column. Distinct from showColumnFunctions: false, which also drops the furniture but keeps the functions reachable from the keyboard; 'hidden' and 'none' are the read-only choices. |
| significantFigures | number | , | On a unit column, render to this many significant figures rather than a fixed number of decimals, so precision is the same on every rung of the ladder. Rounding is applied before the unit is chosen. Set inside the unit configuration a data type is built from. See Units. |
| typeOptions | object | , | Per-column options a data type reads. ratio and percentRate use { weight } to name the column their average is weighted by. See Aggregate safety. |
| highlightOnChange | boolean | string | object | Flash a cell when its value changes. { colour, duration }; duration: 0 stays until cleared. | |
| rowClass | string | string[] | (p) => … | A class, or classes, for every row. Re-evaluated on each repaint. | |
| rowStyle | CellStyle | (p) => CellStyle | Inline styles for every row. Camel-case or hyphenated property names. | |
| views | object | saved, allowSave, storage. See grid.views. | |
| permissions | string | object | fn | Per-column access. See grid.permissions. | |
| diff | object | { snapshot } turns on audit mode. | |
| historyBar | boolean | object | A standalone undo/redo toolbar with a timeline. | |
| ai | object | { ask } mounts the prompt bar. Your ask receives { prompt, schema, schemaText, message, context } and returns the model's reply. | |
| dataTypes | object | Custom types by name. createRadixType and createUnitType are exported for building them. | |
| editBar | boolean | A spreadsheet-style input above the header. When on, it hosts the column's real editor and inline editing is suppressed. | |
| pagination | boolean | object | Renders the pager control: page size, a summary, first/previous/next/last, and a page number you can type into and press Enter to jump. |
Column definition
Everything is optional. A column with only field infers its type from sampled data and takes every default from there.
Inferring a Date, or an ISO string. Inference walks
boolean, number, date, dateString,
datetime, text, object and takes the first type that
matches every sampled value. A Date whose local wall clock reads exactly
00:00:00.000 infers as date and stores YYYY-MM-DD;
a Date carrying any time of day infers as datetime and stores
YYYY-MM-DDTHH:mm (with seconds when they are non-zero), because a
date column would discard the clock on ingest and nothing downstream could
recover it. An ISO string follows the same rule: a date-only string
(YYYY-MM-DD) infers as date; a string with a time part
(T plus a time, with or without a zone offset - '2026-09-12T14:30:00Z')
infers as datetime and keeps the time, and a column mixing both forms infers
datetime rather than falling back to text. A timestamp such as
'2026-09-12T14:30:00Z' therefore keeps its 14:30 on ingest, rather than being
read as the calendar day '2026-09-12' with nothing to say that a time had been
dropped. This is a heuristic with one stated blind spot: a genuine
timestamp that lands on exactly local midnight - a nightly batch stamped
00:00:00.000, or a bare '2026-09-12' string that really meant an
instant - is indistinguishable from a date-only value and is still inferred
as date, so its time of day is still discarded. Sub-second resolution is never
retained by datetime. If you group by such a column, declare it: an
undeclared Date column groups by the instant, which is one group per row, where a
type: 'date' column groups into day buckets. Date filters are unaffected either way - they compare on the day and return the same rows. Declare type and none of this applies:
'date' truncates on purpose, 'datetime' keeps a wall clock, and
'timestamp' keeps the instant to the millisecond.
rows.value() returns that stored form - and the same one on every
path: the rows you passed at construction, rows.load(),
rows.apply({ add }) and rows.apply({ update }), edit.setCells
and a typed cell edit, a stream chunk and a stream re-send, a store-backed or columnar store,
off-thread ingest, a bound KPI tile and a chart binding all answer the same shape for the same
instant, so a host can do arithmetic on it without testing what it got.
rows.text() is the formatted form, and row.data is always the raw
value you supplied, unconverted. For millisecond arithmetic use type: 'timestamp',
which answers the epoch number on every one of those paths. A column of ISO timestamps that
must stay a calendar day opts out with an explicit type: 'date' - no warning is
logged for that column, because the value is preserved (declared, not narrowed) and there is
nothing to disclose.
| Property | Type | Description | |
|---|---|---|---|
| id | string | Defaults to field. Required when there is no field. | |
| field | string | Dotted paths supported: 'site.address.postcode'. | |
| title | string | Header text. Defaults to a humanised field. | |
| type | TypeName | false | A data type bundles format, parse, compare, storage, editor, filter, renderer and Excel behaviour. false disables inference. 'image' treats the value as a URL and draws it: see image columns. | |
| preset | string | string[] | Named bundles from columnPresets. | |
| tags | string | string[] | , | Labels grouping columns together, used by the column tag bar. A bare string is accepted for one tag. |
| format | FormatSpec | string | Shorthand strings like 'percent:1' or 'date:dd MMM yyyy'. | |
| lookup | LookupSpec | Id-to-label mapping. Nested children are flattened, so a tree-shaped list resolves labels everywhere. | |
| value | ColumnValueSpec | Computed values and the value lifecycle. | |
| cell | ColumnCellSpec | string | A bare string is a renderer name. | |
| edit | ColumnEditSpec | boolean | string | A bare string is an editor name. | |
| sort | ColumnSortSpec | boolean | ||
| filter | ColumnFilterSpec | boolean | FilterName | ||
| group | object | boolean | { enabled, index, explode }. | |
| pivot | object | boolean | { enabled, index }. | |
| total | TotalName | TotalFn | One property drives the group row, the tree node, the pivot cell and the grand total. Split it per scope with groupTotal / grandTotal when the subtotals and the grand total should reduce differently. | |
| groupTotal | TotalName | TotalFn | The reduction for group subtotals - group footers, tree-node rollups and pivot cells - where it should differ from the grand total. Overrides total for those scopes only; omitted, total applies. | |
| grandTotal | TotalName | TotalFn | The reduction for the pinned grand-total row, where it should differ from the subtotals. Overrides total for the grand total only; omitted, total applies. | |
| layout | ColumnLayoutSpec | number | A bare number is the width. | |
| header | ColumnHeaderSpec | string | ||
| export | ColumnExportSpec | { lookup: 'label' | 'value' | 'columns', csv, excel }. | |
| allowGroup / allowPivot / allowTotal | boolean | Whether the tool panel offers the column for that zone. | |
| nullable | boolean | Affects storage choice and null ordering. |
Column sub-specs
value
| Key | Type | Description |
|---|---|---|
| compute | (deps, ctx) => unknown | Derived value. Receives only its declared dependencies. A pure result is computed at ingest and cached; it is re-run when its row is replaced by rows.apply({ update }) or rows.load(), when the grid a derived grid follows changes, and when the host asks with rows.refresh({ rows, columns, force: true }). An in-place cell edit to one of its deps does not currently re-run it. For an answer that arrives later (an id-to-name lookup, a rate table), return a placeholder, then call rows.refresh({ rows, columns: [id], force: true }) once it resolves - or declare pure: false. |
| deps | string[] | '*' | Declared dependencies. Cycles are caught at compile time, not at render. An edit to a column outside deps does not re-run a pure compute. |
| pure | boolean | Default true: the result is cached and served until a dependency changes or a refresh forces it. false guarantees the compute is re-evaluated on every read and every paint - never served from a cache - and is the right declaration for a value that depends on something the grid cannot see. A DEV-mode proxy flags pure computes that read outside their deps. |
| format | (p) => string | Overrides the type's formatter. |
| parse | (p) => unknown | Editor output to value. Always called, whatever the editor emitted. |
| apply | (p) => boolean | Writes the value back into the row object. |
| key | (p) => string | Group key override. |
| compare | Comparator | Overrides the type's comparator. |
| quickFilterText | (p) => string | What the quick filter matches against. |
cell
| Key | Type | Description |
|---|---|---|
| render | RendererName | RenderFn | Ctor | Renderer name or component. The built-in names are listed under built-in renderers. |
| props | object | Passed to the renderer. |
| decoration | DecorationName | spec | pill, bar, fill, dot, edge. |
| variant | VariantSpec | Maps a value to a semantic token: { map }, or { when: [...], default }. |
| template | string | Escaped unless allowUnsafeTemplates is set. |
| class | string | string[] | (p) => … | Classes for this column's cells. |
| classWhen | { [class]: (p) => boolean } | A class per predicate, re-evaluated as values change. |
| style / css | CellStyle | (p) => CellStyle | Inline styles, static or computed. |
| tooltip | string | (p) => string | ColumnTooltipSpec | A string or a function is the plain-text case and becomes the browser's own title. An object is a tooltip the grid draws itself - { render, mount, unmount } - which can carry structure, markup or live content, is shown on keyboard focus as well as hover, and can be dismissed with Escape. See Rich cell tooltips. |
| align | 'start' | 'center' | 'end' | 'left' | 'right' | Horizontal alignment; also accepted at the top level of the column. start, center and end are logical: they follow the writing direction, so an end-aligned number column sits on the right edge in a left-to-right grid and on the left edge in a right-to-left one (direction). left and right are physical: they name an edge and keep it in both directions. centre is accepted for center. Omitted, the column takes its data type's default (numbers end, booleans center, text start). The heading follows the cell unless header.align says otherwise. |
| wrap / autoHeight | , | Presentation flags. |
| spanColumns / spanRows | (p) => number | Spanned cells render in their own layer so row recycling cannot clip them. |
Custom CSS, by scope. Cells: cell.class,
cell.classWhen, cell.style and cell.css, all of which
may be functions of the cell. Columns: the same four, declared on the column, so they
apply to every cell in it; the header takes header.class. Rows:
rowClass and rowStyle on the grid.
All of them are re-evaluated on every repaint and remove what they added last time first. That is not caution: rows and cells come from pools, so an element that carried a class for one row will later carry a different row, and a class written once and left alone smears down the grid as the user scrolls.
edit, sort, filter, layout, header
| Spec | Keys |
|---|---|
| edit | enabled (boolean or predicate), editor, props, popup, validate |
| sort | enabled, direction, order, nullsFirst |
| filter | enabled, type, props |
| layout | width, fit, min, max, flex, pin, hidden, resizable, movable, lockVisible, lockPosition. fit: 'content' is the declarative form of columns.autoSize(): the column is sized to what it is showing on the first paint and measured again whenever the rows change, the columns are shown, hidden, reordered or pinned, or the grid is resized - but not as it scrolls, which would make the columns jitter. It measures the mounted rows and the heading, as autoSize() does, so it sizes to visible content rather than to the widest value in the dataset. A declared width outranks it, and so does a width the user drags to, a resize being recorded as a width; flex is resolved first and wins. width is a pixel number or a percentage string ('25%'): a share of the grid's inner width that follows the viewport - after the container changes size the column is re-resolved against the new width, clamped to its min/max, so a '50%' column is half of an 800px grid and half of the same grid at 400px. Percentages summing past 100 overflow and scroll rather than being scaled down. A pin of 'start' or 'end' holds the viewport edge while there is something to scroll; where the columns do not fill the grid, spare width falls beyond the last column rather than in front of it. |
| header | template, render, props, class, tooltip, align. render draws a custom heading and may be a function or a component (a class with a render method); the two forms are interchangeable and each may either append to the passed heading element itself (returning nothing) or return an Element (attached for you) or a string (used as the heading text). class adds a class to the heading cell; template is not read. |
Rich cell tooltips
cell.tooltip as a string or a function gives you the browser's own
title: one line of plain text, on the browser's schedule, unstyled, and invisible
to a keyboard user. The object form declares a tooltip the grid draws instead,
so a cell can show a related record, a small chart, a list of validation errors or an edit
history. The plain-text form is untouched and still becomes a
title, so an existing grid behaves exactly as it did.
render(params) returns one of four things, and the difference between the last two
is a security property rather than a matter of taste:
| Return | Rendered as |
|---|---|
an HTMLElement | Attached as it is. Your DOM, your responsibility. |
{ title, rows, note } | A TooltipSpec, drawn by the grid: a heading, label/value lines, and a closing note. Every field is written as text, so a spec built out of row values needs no escaping. |
{ html: '…' } | The only wrapper that inserts markup, scrubbed of script by the same rules the cell layer applies to allowUnsafeTemplates output. |
| a string | Always text, whatever it contains. A string holding <b>bold</b> shows those characters; it does not embolden. |
That last rule is the load-bearing one. The most natural tooltip anyone writes is
render: (p) => p.value, and a value comes from row data - data the developer
did not write and usually cannot audit. If a bare string were treated as markup, a name field
holding <img src=x onerror=…> would execute and nothing in the code
would have looked dangerous. Markup therefore has to be asked for in the source, where a
reviewer can see it, and no value arriving from data can promote itself.
mount(el, params) and unmount(el) carry live content. Inside
mount you call createChart or createKPI from a module
bundle your application loaded - the grid core never imports a module - and
unmount is called every time the tooltip closes, so nothing keeps running behind a
hidden box. One tooltip element is built and re-used for every cell.
Accessibility. Nothing is built until the pointer or the keyboard cursor has
rested on the cell for delay (400ms by default), so sweeping across the grid mounts
nothing. Focusing a cell shows the same tooltip after the same delay and the
cell points at it with aria-describedby; the tooltip can be hovered without
closing, and Escape dismisses it (WCAG 2.2 AA, 1.4.13). Escape is only consumed
while a tooltip is open, so it still reaches the editor, the menu and the maximised view.
It closes on scroll. Rows and cells are pooled and re-used, so a tooltip left open across a scroll would be anchored to a node that is now showing a different row. Closing is the honest answer and costs nothing: the browser re-hit-tests after a scroll, so a pointer parked over the grid simply gets a fresh tooltip for the row that is actually there. Content is resolved when the tooltip opens rather than when the pointer arrived, so it always names the row that node is showing at the moment it opens.
Grid-level defaults, and a column declaring a spec tooltip
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
tooltip: { delay: 250, maxWidth: 360 }, // the defaults for every tooltip
columns: [
{ field: 'name' },
{
field: 'owner',
cell: {
tooltip: {
// Returned as a spec: the grid writes every field as text.
render: (p) => ({ title: p.value, rows: [{ label: 'Row', value: p.key }] }),
},
},
},
],
rows: [{ name: 'a', owner: 'Ada' }],
rowKey: 'name',
});
const defaults = grid.get('tooltip');
const spec = grid.columns.get('owner').cell.tooltip;
const content = spec.render({ value: 'Ada', key: 'a' });
grid.destroy();
return `${defaults.delay}, ${defaults.maxWidth}, ${content.title}, ${content.rows[0].value}`;
Grid methods
Top-level members. Everything else hangs off a namespace.
| Member | Returns | Description |
|---|---|---|
| getVersion() | string | The version this grid came from, e.g. '1.70.0'. Also on the module as getVersion(), for when you have no grid to hand. |
| get(key) | unknown | Read any configuration key. |
| set(key, value) | void | Write one key. Every key is live; nothing needs a rebuild. |
| setAll(values) | void | Write several in one pass. Emits one config:changed for the batch, not one per key. |
| config() | GridConfig | The whole live configuration as a shallow copy. Pairs with setAll for a read-modify-write round trip. Nested objects are shared by reference, so treat it as read-only. |
| setPinnedRows(rows, opts?) | void | Pin rows outside the scrolling body. opts.edge is 'top' (the default) or 'bottom'. Pass a new array rather than mutating the previous one: array identity is the change signal. See Pinned rows. |
| getPinnedRows(opts?) | object[] | The objects pinned at one edge, as a copy. |
| on(event, handler) | () => void | Returns its own unsubscribe. '*' subscribes to everything; the handler still receives one event object, and reads event.type to tell which arrived. |
| rendererHost() | object | The host object a renderer reads: columns, rows, callbacks. Deliberately a plain bag rather than the grid itself, so a renderer cannot reach into core internals. You need this only when writing a renderer of your own. |
| once(event, handler) | () => void | |
| off(event, handler) | void | |
| emit(event, payload) | void | Emit on the grid's bus, for custom components. |
| attachRenderer(renderer) | void | Bind a renderer to a headless grid. |
| destroy() | void | Release listeners, workers and pooled buffers. |
| element | HTMLElement | null | The rendered root; null when headless. |
| ready | boolean | |
| destroyed | boolean |
grid.rows
Data usually arrives after the grid does. Build it with rows: [], then load
when your fetch resolves, the sort, filters, grouping and column layout you set up in the
meantime all survive, and apply to the new data.
const grid = createGrid(el, { columns, rowKey: 'id', rows: [] });
grid.overlay.show('loading');
const data = await fetch('/api/circuits').then(r => r.json());
grid.rows.load(data); // replaces whatever was there
grid.overlay.hide();
| Method | Returns | Description |
|---|---|---|
| load(rows) | void | Replaces the data. The view (sort, filters, grouping, column layout) is kept. Same as grid.set('rows', data). |
| get(index) | Row | By display index, after filtering, grouping and flattening. |
| byKey(key) | Row | By row key, whether or not it is on screen. |
| matchCount() | number | Data rows passing the filters, across every page. Excludes group headers, footers and totals, the numerator of "1,204 of 100,000". |
| coverage() | { covered, total, windowed } | How much of the data a figure computed from this grid covers - the question the counters above cannot answer, because on a windowed source they all report the rows it is holding and so agree with each other while the data that has been through is larger. covered is the rows a figure would be computed over; total is the rows the source knows about, or null when it cannot know (a stream still open that has evicted nothing has no idea how many rows are coming, and says so rather than repeating covered); windowed is true when a window bounded the computation. covered < total, or total === null, means the figure is approximate - that is the test to write. False windowed means the figure is over all of it: a memory source, or a stream that finished having dropped nothing. Read on demand and cheap (one matchCount() and one progress() on the source, nothing cached), so call it beside every figure you publish rather than once at setup - on a live stream the answer moves. |
| count() | number | Display rows: group rows included, collapsed children excluded. |
| totalCount() | number | Source rows before filtering. The denominator of "1,204 of 100,000". |
| value(key, colId) | unknown | The stored value. |
| text(key, colId) | string | The formatted display text. |
| values(key) | object | Every column's value for one row. |
| data() | unknown[] | The caller's original row objects, in source order. |
| forEach(fn) | void | Walks display rows without materialising them all. |
| forEachAll(fn) | void | Walks every row in the data, before any filter: leaf rows only, in the order they arrived. What you want for a total, an export or a reconciliation, where forEach would give you the view instead. A remote or paged source holds only what it has fetched and says so. |
| forEachExcept(colId, fn) | void | Walks the rows surviving every filter except that column's own, the faceting question, asked of the rows. It is what lets a header histogram keep every bar after one is clicked, and what lets a cross-filtering panel avoid narrowing itself out of existence. Needs a memory source; anything else falls back to the filtered rows and warns. |
| apply(change) | object | Transactional add / update / remove. Needs rowKey. |
| queue(change) | void | Batches a change into the next frame, the high-frequency path. |
| refresh(opts) | void | Re-run computed values and repaint, without re-running sort, filter or grouping. { rows, columns } narrows it to those cells; nothing named means every cell. Either way, the cached results for the named cells are discarded from every cache the grid keeps - the one behind rows.text() and the painted cell, and the one sort and filter read - so a non-stored computation is re-run on the next read, sort or filter. force: true also recomputes a pure (stored) computation for those cells and rewrites it, and repaints cells whose text did not change - the call to make when the answer changed for a reason the grid cannot see, such as an async lookup resolving. A column declared pure: false is never cached, so a plain refresh() is enough to show its new value. Without force, whether a stored (pure) computation is re-run for the named cells depends on the store the grid chose for the row count, and it flips at columnarBelow: below that many rows the named cell is re-run on its next read, at or above it the stored value stands until a force: true. Pass force: true when you want the same answer whatever the row count. |
| move(key, to) | { moved, from, to, reason? } | Move a row to another position in the data. Refuses, naming the reason, while a sort, filter or grouping is active. Emits row:moved; persisting the new order is yours. |
| groupHeadings(index) | Row[] | The group rows enclosing a display index, outermost first. Empty when the grid is not grouped. Useful for a breadcrumb of your own. |
| expand(key, deep?) | void | |
| collapse(key) | void | |
| expandAll() / collapseAll() | void |
grid.columns
| Method | Returns | Description |
|---|---|---|
| all() | ResolvedColumn[] | Every column, hidden included. |
| visible() | ResolvedColumn[] | In render order, including generated group and pivot columns. |
| get(id) | ResolvedColumn | |
| show(ids) / hide(ids) | void | |
| move(id, to) | void | Index into the full column order. |
| pin(id, side) | void | 'start', 'end' or null. |
| resize(id, px) | void | |
| autoSize(ids) | void | Fit each column to its rendered content. |
| fit() | void | Size the visible resizable columns so that every column the grid draws, together, exactly fills the body viewport's client width at the moment of the call: without the vertical scrollbar when there is one, the full inner width when there is not. Columns it does not size (resizable: false, and the grid's selection checkbox, detail expander, group and tree columns) keep their width and are taken out first; the rest share what is left in proportion to their widths, within each min/max. If that is less than their minimums, each goes to its minimum, never below, and the grid scrolls horizontally, with a warning. One-shot: it sets fixed widths (a flex column included) and does not follow later size changes; call it again after a resize or after late rows bring a scrollbar in. |
| group(ids) | void | Set the row-group columns, in order. |
| pivot(ids) | void | |
| totals(ids) | void | Which columns carry an aggregation. |
| setTotal(id, fn, opts?) | void | Change one column's aggregation. null stops totalling it. A named total the column's type says is meaningless is refused (), the same way it is at configuration. With no opts, fn becomes the shared total and clears any group/grand overrides; pass { scope: 'group' } or { scope: 'grand' } to set the group subtotals and the grand total independently (a scope with no override follows total). |
| aggregates(id) | TotalName[] | The aggregate names meaningful for a column, honouring its type's declaration - what the aggregate chooser offers. |
| distinct(id) | unknown[] | Distinct values, read from the dictionary rather than by scanning rows. |
| state() | ColumnState[] | Serialisable column state. |
| apply(state) | StateApplyReport | Restore it. Never throws and never refuses: a saved view written against an older column set applies as much of itself as still makes sense, and the returned { applied, skipped } names what it could not use and why. Columns added since the view was saved appear in their declared state, after the ones it names. See Saved views. |
grid.selection
| Method | Returns | Description |
|---|---|---|
| keys() | string[] | Selected row keys. |
| rows() | Row[] | |
| all() | Row[] | Including rows selected but currently filtered out. |
| set(keys) | void | Replace the selection. In mode: 'single', only the first key of the array is kept; the rest are dropped, they are not an error. |
| clear() | void | |
| ranges() | Range[] | Cell ranges, for spreadsheet-style selection. |
| setRange(range) | void | Replace every range with one. |
| addRange(range) | void | Add a range without discarding the others, the API form of ctrl-click. Becomes the anchor extendRange grows. |
| startRange(rowIndex, colId, opts) | void | Begin a range at a cell. opts.additive keeps the existing ranges. |
| extendRange(rowIndex, colId) | void | Extend the newest range, keeping its anchor. |
| corner() | { row, colId } | null | Bottom-right cell of the newest range, where the fill handle sits. |
| inRange(rowIndex, colId) | boolean | Is a cell inside any selected range? |
| cells() | { key, colId }[] | Every cell in the selected ranges. |
| statistics() | object | null | Everything summary() reports plus median, quartiles, deviation, distinct and outliers: over the selected cells, so a rectangle spanning three columns is one set of numbers. Null with nothing selected. |
| summary() | object | count, sum, min, max, avg over the range. |
grid.filters
| Method | Returns | Description |
|---|---|---|
| get() | FilterSet | The whole condition tree. |
| set(filters) | void | Replace it. null clears everything. |
| quick(text, opts?) | void | The quick filter, applied across every column. |
| clear() | void |
grid.sort
| Method | Returns | Description |
|---|---|---|
| get() | SortEntry[] | { col, dir, nullsFirst? }, in priority order. |
| set(entries) | void | Multi-sort by passing several entries. |
| clear() | void |
grid.edit
| Method | Returns | Description |
|---|---|---|
| start(key, colId) | void | Open an edit session. The row must be rendered. |
| stop(cancel?, opts?) | object | Commit or discard. Pass { value, key, colId } to write a value. |
| undo() / redo() | void | Depth from edit.undoDepth. |
| setCells(writes, type?) | number | Write many cells as one undoable step. Returns how many landed. |
| pasteInto(anchor, text, extent?) | number | Paste tab-separated text, using Excel's tiling rules. |
| previewPaste(anchor, text, extent?) | object | Compute what a paste would change without committing: { changes, rejected }. The engine behind edit.pastePreview. |
| pastePreview | boolean | Whether a bulk paste is previewed before it commits (edit.pastePreview). |
| settle(id, ok, reason?) | boolean | Report the outcome of an optimistic write. Only needed with edit.confirm: 'manual'; the id arrives on cell:pending. |
| pending() | OpenWrite[] | Writes still awaiting an outcome. Empty unless edit.commit is set. |
| status(key, colId) | 'pending' | null | Whether a cell has a write in flight. |
| addRow(row) | string | null | Append a row optimistically and persist it (over a source declaring mutate.append). Returns the client temp key; on the server key it fires row:confirmed after rekeying selection, expansion, focus and in-flight cell edits. null when append is unavailable. |
| deleteRow(key) | string | null | Delete a row optimistically and persist it (over a source declaring mutate.delete). Tombstones then confirms, or restores on refusal. null when delete is unavailable. |
| deleteRows(keys?, opts?) | string[] | Promise | The user-gesture delete (what the Delete key and the "Delete row" menu item call), through the cancellable beforeDelete event - on a memory grid as well as a remote one. Off until config.rowDelete. Keys default to the selection; returns the keys removed (empty on a veto or when disabled), or a Promise when a handler deferred. |
| settleRow(id, ok, reason?, reconcile?) | boolean | Report the outcome of a structural op. Only needed with edit.confirm: 'manual'; the id arrives on row:pending. |
| rowStatus(key) | 'pending' | null | Whether a row has an append/delete in flight. |
| pendingRows() | OpenRowOp[] | Structural ops still awaiting an outcome. Empty unless the source can append or delete. |
Appending and deleting rows. Over a remote source whose adapter declares
mutate.append/mutate.delete, grid.edit.addRow and
grid.edit.deleteRow are the structural counterparts of the cell edit path. An
appended row shows at once under a client temp key; when the server hands back the real key the
row is rekeyed everywhere the grid tracks it - the source row, selection, expansion,
focus and any in-flight cell edits all follow - and row:confirmed fires. A
delete tombstones the row immediately and either purges it on confirmation or restores it on
refusal. The example drives both against a mock adapter, and shows the rekey moving a selection:
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { createPushdownSource } = await import('../packages/core/src/source/pushdown.js');
// A mock adapter that persists append and delete. append returns the server key.
let nextId = 1;
const adapter = {
name: 'mock',
capabilities: { sort: true, mutate: { append: true, delete: true, returning: 'key' } },
async execute() { return { rows: [], total: 0 }; },
async mutate(op) {
if (op.kind === 'append') return { ok: true, keys: [`srv-${nextId++}`] };
return { ok: true }; // delete confirmed
},
};
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'name' }],
source: createPushdownSource({ adapter, edit: true }),
selection: 'multiple',
edit: true,
});
// The structural lifecycle events, bridged onto the grid's own bus.
const fired = [];
grid.on('row:pending', (e) => fired.push(`pending:${e.kind}`));
grid.on('row:confirmed', (e) => fired.push(`confirmed:${e.kind}`));
grid.on('row:reverted', (e) => fired.push(`reverted:${e.kind}`));
grid.on('row:conflict', () => fired.push('conflict'));
// Append: shows immediately under a temp key, then rekeys to the server key.
const temp = grid.edit.addRow({ name: 'Ada' });
grid.selection.set([temp]); // select the optimistic row
await new Promise((r) => setTimeout(r, 0)); // let mutate resolve; row:confirmed fires
const movedTo = grid.selection.keys()[0]; // selection followed the rekey
// Delete: tombstones then confirms.
grid.edit.deleteRow(movedTo);
await new Promise((r) => setTimeout(r, 0));
const gone = grid.rows.byKey(movedTo) === undefined;
grid.destroy();
// fired: pending:append, confirmed:append, pending:delete, confirmed:delete
return `${movedTo} selected; ${gone ? 'deleted' : 'still-there'}`;
The built-in delete gesture. deleteRow above only fires
beforeDelete on a remote source. For the common case - letting a user delete
rows of a memory grid with the Delete key or a "Delete row" menu item, confirmed
through the same beforeDelete hook - set config.rowDelete: true.
It is off by default because deleting data on a keystroke is destructive; every deletion still
flows through beforeDelete, so a handler can confirm or veto it with
preventDefault(reason). grid.edit.deleteRows is the programmatic entry.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'id' }],
rows: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
rowKey: 'id',
selection: 'multiple',
rowDelete: true, // opt in to the Delete-key / menu gesture and the API
});
// A confirm hook that vetoes deleting row 'b' but allows the rest.
grid.on('beforeDelete', (e) => { if (e.rows.includes('b')) e.preventDefault('kept'); });
grid.edit.deleteRows(['a']); // allowed - 'a' is removed
grid.edit.deleteRows(['b']); // vetoed - 'b' stays, delete:cancelled fires
const count = grid.rows.count();
grid.destroy();
return `${count} rows`; // 2 rows: 'b' and 'c'
Previewing a bulk paste. A paste can rewrite dozens of cells at once, and one
that lands somewhere unexpected looks exactly like one that worked. Set
edit.pastePreview: true and a paste into more than one cell opens a confirm/cancel
dialog first, listing every cell that changes (old → new) and every cell a commit
would reject. It is off by default, so existing paste behaviour is unchanged.
previewPaste computes that same diff without any UI:
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
// A per-row rule: the middle row is locked, so a paste over it is refused.
columns: [{ field: 'a', edit: { enabled: (p) => p.row.data.locked !== true } }],
rows: [
{ id: '0', a: 'A0', locked: false },
{ id: '1', a: 'A1', locked: true },
{ id: '2', a: 'A2', locked: false },
],
rowKey: 'id',
// Opt in: off by default, so a plain paste is unaffected.
edit: { enabled: true, pastePreview: true },
});
// What *would* happen - nothing is committed yet.
const preview = grid.edit.previewPaste({ key: '0', colId: 'a' }, 'X\nY\nZ');
// Confirming is setCells of the changes - the ordinary paste path.
grid.edit.setCells(preview.changes.map((c) => ({ key: c.key, colId: c.colId, value: c.newValue })), 'paste');
const changed = preview.changes.filter((c) => c.changed).length;
return `${changed} change, ${preview.rejected.length} rejected`; // rows 0 and 2 change; row 1 refused
grid.form
The row edit form, a drawer or dialog holding one control per field. Present whether or not
rowForm is configured; without it every method declines rather than throwing, so
a caller need not guard. See
Editing a row on a form.
| Method | Returns | Description |
|---|---|---|
| open(key) | boolean | Open a row by key. False if there is no such row, or no form is configured. |
| close() | void | Close without saving. Focus returns to where it was. |
| save() | boolean | Commit the fields and close. False if a validator refused, or there is nothing to save. |
| isOpen() | boolean | Whether the panel is showing. |
grid.pagination
A window over the rows the query already produced, not another query. A page change re-slices; it does not re-filter, re-sort or re-group, so paging a million rows costs nothing beyond the repaint.
| Method | Returns | Description |
|---|---|---|
| get() | { page, pageSize, total, pageCount } | total is the filtered row count, so it moves when a filter does. |
| set({ page?, pageSize? }) | void | Move, resize, or both. pageSize: 0 turns paging off and shows everything. Emits page:changed once the rows have moved. |
grid.scroll
| Method | Returns | Description |
|---|---|---|
| position() | { top, left } | |
| toRow(row, align?) | void | align: 'start', 'centre', 'end'. |
| toColumn(id) | void | |
| to(at) | void | Scroll to { top, left }. left is the logical offset: zero at the content's start whichever way the grid reads. |
| toCell(row, colId, align?) | void | Scroll a cell into view, both axes in one call. row is a row key or a display index. |
grid.export
| Method | Returns | Description |
|---|---|---|
| csv(opts) | string | Blob | Fields sanitised against formula injection. |
| excel(opts) | Promise<Blob> | Real .xlsx, written without a ZIP dependency. Large exports stream. |
| clipboard(opts) | Promise | TSV, with the grid's own paste parser as its counterpart. |
| print(opts) | void | Switches virtualisation and pinning off for the printed document. |
| rows(mode) | Row[] | 'all', 'visible' or 'selected'. |
Excel value conversion. A data type may declare toExcelValue and excelKind, because a spreadsheet's number formats are not free-form. A time of day is written as a fraction of a day under hh:mm:ss; a duration as days under [h]:mm:ss, where the brackets are what stop Excel wrapping at 24 hours. Both stay numeric, so they still sort and subtract in the sheet.
IP columns are stored as packed integers so they sort as addresses, and declare excelKind: 'string' so the dotted form is what reaches the file. Radix columns export decimal: OOXML cannot express base 16, and staying numeric was judged worth more than display fidelity.
grid.import
The mirror of grid.export: rows coming in from delimited text - a CSV or TSV file, or the tab-separated block a spreadsheet puts on the clipboard. The pipeline is parse, infer each column's type, map the columns onto the grid's own fields, then preview and confirm. The API is always present; set config.import to add the DOM affordances (a "Import rows from CSV…" cell-menu item, a file drop target, and paste).
| Method | Returns | Description |
|---|---|---|
| preview(text, opts?) | ImportPreview | Parse, infer and map without changing the grid - the columns, the coerced records, a sample and any warnings a confirm dialog needs. |
| csv(text, opts?) | object[] | Parse delimited text into coerced record objects, the inverse of export.csv. |
| apply(input, opts?) | ChangeResult | null | Add (mode: 'append', the default) or replace (mode: 'replace') the grid's rows from text, a preview or a record array. A client-side operation, so it applies to a memory-source grid; on any other source it declines rather than show rows that cannot persist. |
Excel. .xlsx is not read: it needs an inflate and XML reader the zero-dependency envelope does not carry. Save the sheet as CSV - every spreadsheet does - and import that.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'id' }, { field: 'age', type: 'integer' }],
rows: [{ id: 'seed', age: 1 }],
rowKey: 'id',
// Opt in to the DOM affordances; the grid.import API is present either way.
import: true,
});
// Preview changes nothing - it is what a confirm dialog shows.
const preview = grid.import.preview('id,age\nx,20\ny,30');
// Confirming appends the previewed rows; `age` arrives coerced to a number.
grid.import.apply(preview);
return `${preview.rowCount} previewed, ${grid.rows.count()} rows`; // 2 previewed, 3 rows
grid.state
| Method | Returns | Description |
|---|---|---|
| get() | GridState | Versioned and serialisable: columns, columnOrder, filters, quick, sort, group, pivot, expanded, selection, scroll, pagination. |
| apply(state, opts?) | object | Restore a view. Returns a report of anything it could not apply (a column that no longer exists, for instance) rather than failing silently. |
| baseline() | GridState | null | The state the grid started in, captured once after config.state and any default view, so the baseline is the grid you shipped, not the one before your own configuration ran. |
| reset() | object | null | Put the grid back to that baseline, as one undo entry. Clears anything the baseline does not mention, including the quick filter. |
| modified() | boolean | Whether anything has changed since construction. Lets a "restore" control disable itself rather than offering an action that would do nothing. |
grid.history
Undo across the whole grid, not only edits. Sorts, filters, column moves, grouping, an applied view and a restore all record an entry, and each carries a label written for a button: "sort by Region", not "sort".
| Method | Returns | Description |
|---|---|---|
| undo() | object | null | The entry that was undone. |
| redo() | object | null | |
| canUndo() / canRedo() | boolean | |
| peek(direction?) | object | null | What the next undo or redo would do, so a control can name it before it is pressed. |
| list() | object[] | The timeline, newest first. |
| transaction(label, fn) | object | null | Group several changes into one entry. Nested transactions join the outer one. |
| clear() | void |
A multi-cell paste is one entry, not one per cell. An AI plan is one entry however many actions it contains, labelled with what it did.
grid.diagnostics
What the grid is doing, as data: render counts and their causes, memory layout, operation and provider timing, event listener counts, effective configuration, and a list of things that look like mistakes.
This is the API; the devtools panel is a consumer of it. Built in that order deliberately: instrumentation shaped by a UI tends to report what is convenient to display rather than what is true, and an API that only exists behind a panel cannot be asserted against in a test.
// the assertion this exists to make possible
const before = grid.diagnostics.renders().dom.cellWrites;
grid.filters.set({ col: 'status', op: 'eq', value: 'active' });
await nextFrame();
const written = grid.diagnostics.renders().dom.cellWrites - before;
expect(written).toBeLessThan(200);
| Method | Returns | Description |
|---|---|---|
| snapshot() | object | Everything, in one structure. |
| renders() | object | Counts by cause, the last render's phase timings, DOM write counters and viewport state. |
| store() | object | Per-column backing kind and byte footprint, total bytes, rows against physical slots, tombstoned rows. |
| operations() | object | Count, mean and worst per operation kind, with a bounded sample of recent calls. |
| providers() | object | Calls, errors, in-flight count and latency per provider, with failures retained. |
| events() | object | Listener count per event type. |
| config() | object | { effective, supplied, defaulted }, which values you chose and which the grid chose. |
| warnings() | object[] | Everything flagged, newest first, each with a stable id. |
| dismiss(id) | void | Hide a warning for this session. Not permanently. |
| bundle() | object | A support bundle. Contains no row data. |
| checkOptions(options) | boolean | True when an options object changed identity without its contents changing. |
| record(kind, detail) | void | Record your own operation, so custom work appears alongside the grid's. |
| reset() | void | Zero the counters. Warnings and configuration are left alone. |
The support bundle
bundle() returns configuration, query state, timing history, warnings, provider statistics, version and environment. It never contains row data, cell values or column values, and says so in its own contains field. The guarantee is the point: a bundle that had to be read for confidential content before sending is a bundle nobody sends.
Warnings
Each carries a stable id a support conversation can name, a plain description, and the specific values involved. Two sources are merged: checks run against the grid, and everything the grid has reported through its own one-per-cause warnings.
| Id | What it means |
|---|---|
| options-identity-churn | An options object rebuilt on every parent render. A wrapper comparing by identity will tear the grid down each time. |
| duplicate-row-keys | Two rows share a key. Presents as "the wrong row updated", never as an error. |
| query-references-unknown-column | A filter or sort names a column that does not exist. Silently matches nothing. |
| listener-count-growing | Probable subscription leak in the host. Presents as gradual slowdown. |
| main-thread-eligible-for-worker | A large operation ran on the main thread despite the worker threshold. |
| slow-provider | A provider took more than a second to answer. |
The devtools panel
An optional module. It imports nothing (the grid is handed to it) so deployments that never load it pay nothing.
import { createGrid } from '@toclocoinc/lattice-grid';
import { createDevtools } from '@toclocoinc/lattice-grid/modules/devtools';
const grid = createGrid(el, config);
createDevtools({ grid }); // Ctrl+Shift+D collapses it
Nine tabs over the API above, a compact vitals strip to leave open while working, and a render heat overlay that tints cells as they are written: blue for a new value, red for a cell rewritten with the value it already held. The second colour is the one worth chasing: it is work the grid did not need to do, and no counter alone will tell you where it is.
The panel observes and never mutates. A configuration editor would create a second path into state that has to be kept correct forever, so there is not one. Nothing leaves the browser: there is no telemetry, and the bundle is produced only when you ask for it.
grid.presence
Who else is on this grid and what they are doing: cursor, selection, active edit, and an optional advisory lock. It prevents the two failure modes of multi-user data work: two people editing the same cell unaware of each other, and one person unable to tell whether anyone else is there at all.
The grid never opens a connection. You supply the transport and the identity; the grid renders what arrives and publishes what changes. A WebSocket, MQTT, a CRDT library or a polling endpoint all satisfy the interface. Without a provider the feature is inert.
presence: {
provider, // subscribe + publish
me: { id: 'u_17', name: 'Tony' },
throttleMs: 60,
lock: true // advisory; see below
}
| Method | Returns | Description |
|---|---|---|
| enabled | boolean | False without a provider. |
| peers() | Peer[] | Everyone else, most recently active first, each with idle, hidden and cursorFresh. |
| hiddenCount() | number | Peers none of whose positions are in this view. |
| editorOf(rowId, colId) | Peer | null | Who is editing a cell, if the claim is fresh. |
| lockedBy(rowId, colId) | Peer | null | Null unless lock is on. Advisory. |
| jumpTo(peerId) | boolean | Scroll to a peer's cursor. False when their row is not in this view. |
| publish() | void | Publish now. The grid already does this on cursor, selection and edit changes. |
| setPublishing(on) | void | Receive without appearing, for observer and supervisor roles. |
| setPaused(paused) | void | Suspend publishing. Done for you while the tab is hidden. |
| connect(provider) | void | Attach or detach after construction. |
| stats() | object | Published, received, throttle drops, provider errors, peer count. |
The provider
{
subscribe(onMessage) { /* call onMessage(peer) or onMessage(peer[]) */ return unsubscribe; },
publish(state) { /* send it however you like */ }
}
A message is one peer state or an array of them, so a transport that sends a full roster on connect and deltas afterwards needs no unwrapping. { id, left: true } removes a peer.
Presence carries intent, never values. A peer's committed edit must reach the grid as data, through whatever channel you already use, as a transaction, so it gets the flash-on-change treatment. Presence is throttled, lossy and ephemeral by design, so a value carried on it is a value that can be dropped. That is the kind of bug that surfaces once a month in production and cannot be reproduced.
Positions travel as row keys
Peers sort and filter independently, so a row index addresses a different record on every screen. Presence is positional in data terms: a peer's cursor renders wherever that row currently sits in your view, and is held but not drawn when the row is filtered out, on another page, or evicted from a bounded window. Those peers are counted by hiddenCount() and shown in the roster as “not in view”, so their absence does not read as a disconnection.
Idle and removal
Derived from local receipt time, never the timestamp in the payload. Clocks between clients disagree by seconds routinely, so a peer with a fast clock would look permanently fresh and one with a slow clock permanently idle. Silence past idleMs desaturates them; past removeMs they go. An explicit left signal is used when your transport provides one.
Locking is advisory
Locking reduces collisions. It does not eliminate them. Presence is throttled and can arrive out of order, so two clients can enter an edit at the same moment. The authoritative resolution is the conditional write in edit.commit, which returns a conflict and rolls the optimistic edit back. If you treat locking as a guarantee and skip that write, you will lose data.
With lock: true, starting an edit on a cell a peer holds returns false from edit.start, emits presence:lockRefused, and announces the holder through a live region, a cell that silently refuses to enter edit mode is indistinguishable from a broken grid.
What is drawn
A peer's cursor is a dashed border in their colour; your own focus ring is solid, and the difference is in the kind of line rather than only the hue so the two can never be confused. Their name shows for a moment after their cursor moves and on hover, then fades to the bare border. A selection is a low-opacity tint, with the most recent peer winning a contested cell outright rather than blending. An active edit is solid and tinted, the loudest treatment, because it is the state that matters most.
Nothing is inserted into the grid: every treatment is written onto cells that already exist, so presence cannot shift layout, cover an in-cell chart, or intercept a click. The roster is the exception, because it is a control.
Colours are assigned by hashing the peer id against --lattice-peer-1 … --lattice-peer-8, so one person is the same colour on every screen and across reloads.
grid.comments
Threaded comments attached to individual cells, for collaborative data review: flagging an anomaly, asking why a figure changed, recording the reason behind a manual correction. A commented cell carries a small triangle in its upper-right corner; clicking the corner opens the thread.
The grid owns presentation and interaction only. Storage, identity and permissions are yours. Comment data lives wherever you put it and is reached through a provider.
A stable rowKey is required. Comments are keyed on row identity plus field, never row index, and they outlive the values they annotate. Configure the grid without a rowKey and comments are disabled: named in the same console warning as the other identity-dependent features: rather than silently filing threads against positions that move on the next sort.
Identity must be stable across sessions and across data reloads, not merely within one session. A key derived from load order is not enough: reload the data in a different order and every comment reattaches to the wrong row.
comments: {
provider, // required; without it the feature is inert
mode: 'anchored', // or 'docked' for a side panel
markdown: false, // restricted: emphasis, code, links
rowLabel: (row) => row.data.name // so the panel says what is being discussed
}
| Method | Returns | Description |
|---|---|---|
| enabled | boolean | False without a provider or without stable row identity. |
| unavailable() | string | null | 'no-provider', 'no-row-identity', or null. |
| at(rowId, colId) | object | null | { count, unresolved, updated } for one cell. Counts only: this is read on every repaint. |
| open(rowId, colId) | Promise | Open a thread and load its bodies. |
| close(opts?) | void | Close and discard the bodies. |
| add(body, opts?) | Promise | Add to the open thread. opts.parentId replies within it. |
| edit(commentId, body) | Promise | |
| remove(commentId) | Promise | |
| resolve() / unresolve() | Promise | Mark the open thread. |
| request(rowIds, fields?) | void | Ask for index entries. Debounced; the viewport does this for you. |
| refresh() | void | Reload the index for known rows, after your application learns of a change elsewhere. |
| loadAll() | Promise<boolean> | Load the index for every row, which the comments-only filter needs first. |
| complete | boolean | Whether the index covers the whole row set. |
| hiddenUnresolved() | number | Unresolved threads on rows the current filter hides. Zero when the index is partial. |
| filterToCommented(opts?) | boolean | Restrict to rows carrying comments. unresolvedOnly narrows further. False when the index is incomplete. |
| thread / openKey / loading | , | The open thread, its cell key, and whether it is still loading. |
The provider
Every method returns a promise. A rejection surfaces in the panel without disturbing grid state, and an optimistic write is rolled back.
| Method | Description |
|---|---|
| loadIndex(rowIds, fields) | Counts and timestamps for the requested cells. Never bodies. Called for the viewport and on scroll, debounced. |
| loadThread(cellKey) | The ordered comments for one cell. |
| addComment(cellKey, body, parentId, ctx) | ctx.value is the cell's value at the time of writing. Store it. |
| editComment(id, body) | |
| deleteComment(id) | |
| resolveThread(cellKey) / unresolveThread(cellKey) |
The grid performs no authorisation. A comment may carry can: { edit, delete, resolve } and the grid draws affordances accordingly, but that is a convenience for the user and never a security control. Absent flags mean every affordance is shown. Your provider must reject what it must reject.
Author information is rendered exactly as the provider supplies it: author: { name, avatarUrl, initials }. The grid does not know who the user is and does not guess.
Bodies are text. The default path never produces markup. With markdown: true the panel handles emphasis, code and links only, builds elements rather than assigning HTML, and refuses any link scheme other than http, https and mailto.
Comments follow their row through sorting and grouping. When a commented row is filtered out its comments are not lost and not shown; hiddenUnresolved() reports what is outstanding on hidden rows so their absence does not mislead, and the status bar’s comments panel puts that count on screen whenever it is not zero. Comments remain available while streaming, and a thread whose row is evicted by a bounded window closes with an explanation. Comments do not appear in exports and do not serialise into saved views, a view captures display configuration, not data.
Keyboard: Alt+M opens the thread on the focused cell. The panel traps focus while open and returns it to the originating cell on close. Cells carrying comments announce the fact, and the unresolved count, through their accessible description.
grid.facets
A distribution chart in each column heading, which is also a filter control. Clicking a bar filters to that bucket; dragging across bars on an ordered column filters to the range. As filters are applied, the other columns' charts recount, so a dataset can be explored by clicking through headings rather than opening a dialog.
Off by default. The band roughly doubles the header's height.
facets: { enabled: true } // grid-wide
// per column, layered over the grid's settings
{ field: 'price', type: 'number', facet: { strategy: 'quantile', buckets: 16 } }
{ field: 'notes', facet: false } // opt one column out
| Method | Returns | Description |
|---|---|---|
| get(colId) | object | null | { bounds, counts, unfiltered, stale, suppressed }. Schedules the computation if it has not run; redraw on facet:computed rather than awaiting. |
| suppression(colId) | string | null | Why there is no chart: type, cardinality, rows, streaming, no-provider, disabled. Null when there is one. |
| config(colId?) | object | The resolved settings, column layered over grid. |
| select(colId, from, to?, opts?) | boolean | Filter to a bucket, or to the range from-to. opts.additive adds to a categorical set. Selecting what is already selected clears it. |
| clear(colId) | boolean | Remove only this column's filter, leaving every other filter in place. |
| selected(colId) | number[] | Which buckets the column's own filter currently covers. |
| toggle(colId, open?) | boolean | Expand or collapse the chart. Rides in a saved view. |
| isExpanded(colId) | boolean | |
| refresh(opts?) | void | Recount every chart. immediate skips the debounce. |
| expanded() | string[] | Every expanded column. |
A column is never counted against its own filter. Every other active filter applies; that column's own conditions are pruned out. Without this, clicking a bucket would collapse the chart to that single bar, leaving no way to see what was excluded or to widen the selection.
The filters are ordinary filters. They go through filters.set, so they undo, ride in saved views, and appear in whatever filter UI you already have. A drag emits a between range rather than a set of bucket indices, so it still means something after the data is replaced and the edges move.
| Option | Default | Description |
|---|---|---|
| enabled | false | Grid-wide, or per column. |
| collapsed | true | Start as a one-line density strip that opens on click. |
| height | 28 | Band height in pixels. |
| buckets | 20 | Numeric and date columns. |
| strategy | 'equal' | equal, quantile or log. Equal width looks wrong on skewed data. |
| granularity | auto | hour … year. Chosen from the span when omitted. |
| order | 'count' | count or alpha, for categorical columns. |
| cardinalityLimit | 50 | Distinct values above which a text column has no readable chart. |
| aboveLimit | 'suppress' | suppress, or topN for a top list with an aggregated remainder. |
| rowCeiling | 2000000 | Rows above which charts are suppressed. |
| debounce | 120 | Milliseconds a filter change waits before charts recount. |
| whilePaused | true | Whether a paused stream re-enables charts. |
| provider | , | Async bucket counts for a paged or remote source. Without one, charts are suppressed silently. |
| format | , | (bucket, count, unfiltered) => string for tooltips and accessible names. |
Live streams suppress charts. Buckets that move under the pointer are worse than no chart, the control lies about what clicking it will do. Filters already made stay applied, because they are ordinary filters. Pausing the stream brings the charts back; set whilePaused: false if you would rather it did not.
Server-side sources need a provider. It receives the column, the current filter state with that column's own conditions removed, and the bucketing settings, and returns counts. Results are cached against the filter state, but this is still one query per column per filter change, a grid with eight faceted columns will ask eight questions every time a filter moves, and the backend has to be able to absorb that.
Charts are keyboard operable: focus enters from the header, arrows move between buckets, Enter toggles, Shift with arrows extends a range on ordered columns, Escape clears. Each bucket carries its range and count as an accessible name, and the chart as a whole carries a one-sentence description of the distribution's shape, which is the part bar-by-bar labels cannot convey.
grid.updates
Control over an incoming feed: hold it, let it through, and see what the batching is actually saving you. Pausing does not drop anything: held changes keep merging, so a long pause costs one entry per changed row rather than one per update.
grid.updates.pause(); // hold the feed; it keeps arriving and merging
grid.updates.stats(); // { pending, queued, coalesced, flushes, ... }
grid.updates.flush(); // apply what is waiting, stay paused
grid.updates.resume(); // apply everything and go live again
| Method | Returns | Description |
|---|---|---|
| paused | boolean | True while updates are held. |
| pause() | boolean | Hold incoming updates. True when this call paused it. |
| resume() | object | Apply everything held and start applying again. Returns the rows added, updated and removed. |
| flush() | object | Apply what is waiting without leaving the paused state, a single step. |
| stats() | object | Counters for the feed and the buffer: what arrived, what will be applied, and the difference. |
| log(opts?) | object[] | The timestamped changes still held, oldest first. since narrows to a time window. |
coalesced is the number a batching strategy is actually bought with: rows that arrived more than once in a window and were written once. A feed where it stays at zero is not being coalesced, whatever the interval says.
The log is bounded two ways, because an entry is not a fixed size, one carrying a single changed cell and one carrying a fifty-thousand-row batch both count as one.
| Option | Default | Description |
|---|---|---|
| updates.logLimit | 2000 | How many changes are kept. |
| updates.logRows | 100000 | How many rows those changes account for between them. A feed delivering large batches reaches this one first. |
| updates.flush | 'frame' | frame lands on a paint boundary, which is what makes one repaint per batch reliable. microtask at the end of the current task, interval on the coalescing window, manual only when you call flush(). |
| updates.maxQueued | 20000 | Queued rows that force an early flush, whatever the strategy: including manual. |
| updates.budgetMs | 10 | Milliseconds one flush may spend applying before deferring the rest to the next frame. |
Applying changes
rows.apply(change) applies immediately and returns what happened; rows.queue(change) batches into the next flush and returns a promise. Both take the same shape: add, update, remove, and an optional at insert position.
An update is a patch, not a replacement. Fields absent from the update are untouched, so a delta from a websocket or a save response can be applied as it arrives without reading the row back first. Coalescing merges fields too: {price} and {volume} arriving as separate messages inside one window both survive.
grid.rows.apply({ update: [{ id: 'R1', price: 42 }] });
// every other field on R1 is left alone
Rows that cannot be applied are reported, not thrown. A batch of a thousand containing three bad rows applies the other 997 and lists the three.
| Reason | Meaning |
|---|---|
| unknown-id | An update or remove naming a row that is not in the grid. |
| duplicate-id | An add whose key already exists. Refused rather than admitted: selection, expansion, comments and the key index all resolve one key to one row. |
const result = grid.rows.apply({ update: [ ... ] });
result.rejected; // [{ operation, id, reason }]
These are batches, not database transactions. There is no isolation and no all-or-nothing guarantee: partial application with per-row rejection is the defined behaviour, which is why the API is not called a transaction.
stats() reports held against heldLimit: what the log is carrying now, against what it will carry. rows is a lifetime total of everything that ever arrived and says nothing about memory; these two do. Raise logRows for a deeper scrubber on a grid you have measured, and lower it on a feed of very wide rows.
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.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.redaction
Obscures a column's values on screen while leaving the shape of the data (row count, sort, filters, layout) perfectly readable. Built for presenting and screen sharing. Right-click a column heading for Redact column.
This is not a security control. The values stay in the model, the DOM, the clipboard and every export; anyone with the page can read them from devtools or by turning off one CSS rule. It defeats a camera, which is the whole claim. For a value that must not reach the browser at all, use permissions with writeOnly.
grid.redaction.toggle('salary'); // returns the state it is now in
grid.redaction.add('salary');
grid.redaction.set(['salary', 'bonus']);
grid.redaction.list(); // ['salary', 'bonus']
grid.redaction.clear(); // back to normal when the call ends
| Method | Returns | Description |
|---|---|---|
| has(colId) | boolean | Is this column redacted? |
| list() | string[] | Every redacted column id. |
| toggle(colId) | boolean | Redact, or stop. Returns the state it is now in. |
| add(colId) | void | |
| remove(colId) | void | |
| set(ids) | void | Replace the whole set. |
| clear() | void | Stop redacting everything. |
| active | boolean | True when at least one column is redacted. |
The treatment is a CSS token, so a host can swap it: --lattice-redaction-filter defaults to blur(5px) contrast(0.85) and accepts anything the filter property does, including url(#your-svg-filter) for a mosaic.
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.
Data bars and icon sets as rules
A rule can carry a data bar or an icon set instead of a style or a scale, so the same declarative, view-persisted, headless rule list that already paints colour scales also paints proportional bars and per-band glyphs. Both compile to a plain style object - a data bar is a CSS gradient on the background, an icon set a background-image - so they need no extra element, compose with the cell's text, and resolve the same way for a server-side export as for a browser paint. This is the rule-engine sibling of the cell decoration below; reach for a rule when you want the visual to travel in a saved view and to derive its bounds from the column's distribution.
// A data bar spanning the data, and a three-arrow icon set split at the tertiles.
grid.formatting.add('revenue', { dataBar: { from: 'minmax', colour: '#5b9bd5' } });
grid.formatting.add('score', { iconSet: { set: 'trafficLights' } });
// Bounds and bands can be pinned instead of derived; bars that straddle zero
// grow both ways from a shared axis, in their own colours.
grid.formatting.add('delta', { dataBar: { min: -100, max: 100, colour: '#2e7d32', negativeColour: '#c0392b' } });
grid.formatting.add('rank', { iconSet: { set: 'arrows', thresholds: [10, 20], reverse: true } });
A data bar takes min/max to pin its scale, or from: 'minmax' | 'quantile' | 'stddev' to derive it from the column; colour and negativeColour fill the two sides of a zero axis, and direction: 'rtl' reverses it. An icon set names a built-in - the keys of ICON_SETS (arrows, trafficLights, ratings) - or supplies its own icons; thresholds place the band edges, or, given none, the column is cut into equal-count bands; reverse flips the order so a high value can read as red.
const { createHeadlessGrid, ICON_SETS } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'id' }, { field: 'revenue', type: 'number' }, { field: 'score', type: 'number' }],
rowKey: 'id',
rows: [1, 2, 3, 4, 5].map((n) => ({ id: 'r' + n, revenue: n * 20, score: n })),
formatting: {
revenue: [{ dataBar: { from: 'minmax', colour: '#5b9bd5' } }],
score: [{ iconSet: { set: 'trafficLights' } }],
},
});
// A data bar compiles to a gradient the cell layer paints as a background.
const bar = grid.formatting.styleFor('revenue', 100).backgroundImage.includes('linear-gradient') ? 'bar' : 'none';
// An icon set resolves different bands to different glyphs.
const low = grid.formatting.styleFor('score', 1).backgroundImage;
const high = grid.formatting.styleFor('score', 5).backgroundImage;
const icon = low !== high ? 'icon' : 'flat';
const sets = Object.keys(ICON_SETS).length; // the three built-in sets
grid.destroy();
return [bar, icon, sets].join('|');
Runtime decorations: data bars and icon sets on demand
Where a colour rule paints the cell's background, a decoration changes the shape the cell renders as, a data bar sized by value, or a threshold icon set. grid.columns.decorate(id, spec) turns one on, changes it, or clears it with null, after the grid is built. It is presentation config rather than query state: unlike a formatting rule it is not on the undo timeline and does not travel in a saved view. Icon sets carry an aria-label per band and keep the value beside the glyph, so the meaning is announced, never only shown.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'score' }, { field: 'trend' }],
rows: [{ id: '1', score: 72, trend: 8 }],
rowKey: 'id',
});
// A data bar, sized 0..100, set at runtime.
grid.columns.decorate('score', { type: 'bar', min: 0, max: 100 });
const bar = grid.columns.get('score').cell.decoration.type;
// A built-in three-arrow icon set on the trend column, with its own bands.
grid.columns.decorate('trend', { type: 'icon', bands: [
{ min: 0, icon: 'chevronUp', label: 'increasing', variant: 'success' },
{ icon: 'chevronDown', label: 'decreasing', variant: 'danger' },
] });
const set = grid.columns.get('trend').cell.decoration.type === 'icon' ? 'arrows' : 'none';
const band = grid.columns.get('trend').cell.decoration.bands[0].label;
// Clearing a decoration returns the column to plain text.
grid.columns.decorate('score', null);
const cleared = grid.columns.get('score').cell.decoration === undefined ? 'cleared' : 'still-set';
// A runtime colour rule is the durable, view-persisted sibling.
grid.formatting.add('score', { when: { op: 'lt', value: 50 }, style: { background: '#fdecea' } });
const painted = grid.formatting.styleFor('score', 20) ? 'painted' : 'plain';
return [bar, set, band, cleared, painted].join('|');
grid.validation
Declarative column validation. Where edit.validate is an imperative function you write, this is the same job said as data: required, min/max, minLength/maxLength, pattern, oneOf, and a crossField predicate, declared per column in validation. Each rule is checked against a new value before it is written, riding the cancellable beforeEdit before-event: a failing value cancels the commit so no cell is written, marks the cell with the grid's ordinary invalid state (an accessible error, not only a red border), and fires validation:failed. A corrected value clears the mark and, where you are watching, fires validation:cleared. The cancellation carries reason: 'validation:<code>', so a host logging cancellations can tell a validation veto from any other.
Only a user-initiated edit is gated, the same contract beforeEdit itself keeps: a host API write (grid.edit.setCells) and a remote/router-applied delta are the authority and do not self-veto. A grid whose columns declare no rules wires no gate and keeps the byte-for-byte synchronous edit path.
// Declared per column, config-time.
createGrid(el, {
columns: [
{ field: 'name', edit: true, validation: { required: true, minLength: 2 } },
{ field: 'age', type: 'number', edit: true, validation: { min: 0, max: 120 } },
{ field: 'code', edit: true, validation: { pattern: '^[A-Z]{3}$', messages: { pattern: 'Three capitals.' } } },
],
});
grid.validation.check('age', 200); // { code: 'max', message: 'Must be at most 120.' } - records nothing
grid.validation.errorFor('r1', 'age'); // the recorded error for a cell, or null
grid.validation.errors(); // every cell that currently holds an error
grid.validation.define('age', { min: 18 }); // set or replace a column's rules at runtime
grid.validation.clear('r1', 'age'); // drop a mark by hand
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'id' }, { field: 'age', type: 'number', edit: true, validation: { min: 0, max: 120 } }],
rowKey: 'id',
rows: [{ id: 'r1', age: 30 }],
});
let failed = 0;
grid.on('validation:failed', () => { failed++; });
grid.on('validation:cleared', () => {});
// A user edit that breaks the rule is refused: the cell is not written.
grid.edit.start('r1', 'age'); grid.edit.stop(false, { value: 999 });
const blocked = grid.rows.byKey('r1').data.age; // still 30
const why = grid.validation.errorFor('r1', 'age').code; // 'max'
// A valid value writes through and clears the mark.
grid.edit.start('r1', 'age'); grid.edit.stop(false, { value: 40 });
const now = grid.rows.byKey('r1').data.age; // 40
grid.destroy();
return [blocked, why, now, failed].join('|');
grid.statistics
What the grid knows about its own numbers, and about how they have changed since the page loaded. Every figure is computed over the filtered rows, through the same column handles the totals row uses, so a median here and a median in the footer are the same number, by the same definition (R type 7).
grid.statistics.profile('margin');
// { column, rows, present, missing, distinct, min, max, mean, median,
// q1, q3, iqr, stddev, outliers, histogram: [{ from, to, count }, …] }
grid.statistics.profile('region'); // a categorical column
// { column, rows, present, missing, distinct, …numeric figures null…,
// histogram: [], topValues: [{ value, count, share }, …] }
grid.statistics.reduce('margin', 'p95'); // any registered kernel
grid.statistics.correlation('spend', 'revenue'); // Pearson's r, clamped to [-1, 1]
grid.statistics.weightedAverage('price', 'qty');
grid.statistics.covariance('spend', 'revenue');
grid.statistics.regression('spend', 'revenue'); // { slope, intercept, r2, stdError, n }
grid.statistics.spearman('spend', 'revenue'); // rank; one outlier cannot drag it
grid.statistics.kendall('spend', 'revenue'); // tau-b, null past 5,000 rows
grid.statistics.weightedQuantile('price', 'qty'); // the median by default
grid.statistics.series('price', { by: 'date', periodsPerYear: 252 });
// { volatility, annualisedVolatility, growth, maxDrawdown, maxDrawdownFrom,
// maxDrawdownTo, autocorrelation, upDays, downDays, … }
grid.statistics.capability('mm', { baseline: 20 });
// { cp, cpk, pp, ppk, sigmaWithin, sigmaOverall, outOfSpec, defectRate,
// limits: { centre, upper, lower, sigma }, violations: [{ index, rule }] }
grid.statistics.shadow('price', 'delta', 'R42'); // one row's shadow value
grid.statistics.rebase('price'); // "mark all": today's values become the baseline
grid.statistics.tracking(); // { columns, rows, forgotten }
The statistics tool panel is the end-user half of profile(): a column picker, the twelve figures and a histogram of the column's shape, all following the filters. Add it with toolPanel: { panels: ['columns', 'statistics'] }. It profiles categorical columns too: a text column shows its count, distinct count and Top values (each value with its count and share) instead of the numeric figures and the histogram it has none of.
The column header menu carries a Column statistics item that opens this panel seeded on the column it belongs to. It emits column:profile:open with { colId } rather than reaching into the dock, exactly as the header filter affordance emits column:filter:open; a mounted tool panel turns that into the open, seeded statistics panel.
The regression tool panel is its multi-column sibling: it fits the model you name and shows the coefficient table - each term's estimate ± standard error with its t and p - alongside R² and adjusted R², the variance-inflation factor per predictor, and the Breusch-Pagan heteroscedasticity flag, all over the filtered rows and computed by the one core engine (grid.statistics.regressionModel). Name the model on the panel: toolPanel: { panels: ['columns', { name: 'regression', props: { predictors: ['x1', 'x2'], response: 'y' } }] }. p-values are reported as numbers with a documented method, never a significance verdict.
The same fitted model can live in the data as shadow columns: shadow: { kind: 'fitPredicted', model: { predictors: ['x'], response: 'y' } }, and likewise fitResidual and fitInfluence - plus fitStdResidual, fitLeverage and fitCooksD, which surface the internally studentised residual, the hat-matrix leverage and Cook's distance the engine already computes. They are ordinary numeric/boolean cells - sortable, filterable, groupable, exportable - that read the fit by row key and follow the grid's filters (the model refits over the filtered rows); a row outside the fit reads null. fitInfluence flags Cook's D > 4/n by default (overridable with threshold), keeping "not influential" (false) and "cannot tell" (null) distinct.
It shows the twelve one-pass figures, then Shape (skewness, kurtosis, Jarque-Bera), Robust (trimmed and winsorized means, MAD, robust outliers), Concentration (Gini, HHI, entropy, evenness, top-3 share) and Capability where the column declares a spec. A section whose reductions all return null is left out rather than shown as a column of dashes.
Or put it in your own page. mountPanel takes no dock and does not create one: toolPanel may be off entirely, so a statistics readout can sit beside a chart, in your own sidebar, or in a settings dialog, at whatever size you give it. It repaints on the same events the rail does, so it stays in step with filters, edits and saved views without you subscribing to anything.
import { mountPanel } from '@toclocoinc/lattice-grid';
const stats = mountPanel({ grid, panel: 'statistics', container: sidebar });
stats.refresh(); // for a change the grid does not announce
stats.destroy(); // yours to call: the element belongs to your page
Any built-in panel works: columns, filters, views, quick, formatting, statistics, regression, compare, insights, as does a constructor of your own.
The insights panel
The insights tool panel is the on-screen half of the comparison analytics (): the API-only subsetVsPopulation(), datasetVsDataset(), capability() and compareGroups() rendered without you building any UI. It is opt-in - off unless you name it. Add it with toolPanel: { panels: ['columns', 'insights'] }.
It shows four things, all over the filtered rows: the columns of the current filtered subset ranked by effect size against the whole; the same ranking against a second grid when you pass one as toolPanel: { panels: ['insights'], /* config */ } with insights: { compareWith: otherGrid }; process capability for the chosen column where it declares a spec; and a two-group comparison - pick a column and a column to group by, and the panel shows the named test, its confidence interval and its effect size together.
The stance is enforced on screen. The effect size is never shown without its interval beside it; the test or method that produced a figure is always named, from the API's own method string; and nothing renders a significant flag, a verdict, a badge or a star - the p-value is shown as the plain datum it is, when it is shown at all. The panel adds no statistic of its own: every number it shows is the one grid.statistics returns.
createGrid(el, { toolPanel: { panels: ['columns', 'insights'] } });
// with a second dataset to rank this grid against:
createGrid(el, {
toolPanel: { panels: ['insights'] },
insights: { compareWith: otherGrid },
});
The reductions
Forty-one, all available to a totals row, to reduce() and to the profiling panel. Names are the same everywhere and the labels come from the message catalogue, so a grid in Polish reads in Polish.
| Group | Names |
|---|---|
| Basic | sum, avg, min, max, count, countValues, first, last, distinct, mode, range |
| Spread | variance, varianceP, stddev, stddevP, iqr, mad, sumSquares |
| Quantiles | median, p25, p75, p90, p95, p99 |
| Shape | skewness, kurtosis, jarqueBera: above 5.99 the column is not plausibly normal |
| Means | geomean, harmean, weightedAvg, trimmedMean, winsorizedMean |
| Outliers | robustOutliers: by the modified z-score, which an outlier cannot hide inside the way it inflates an ordinary one |
| Concentration | hhi, entropy, evenness, top3Share, top10Share, gini, the only group that reads a text column, because "how concentrated is this" is a question about categories |
| Positional | argmin, argmax |
Process capability
Declare the customer's tolerance on the column, and the capability figures, a control chart and any rule marking an out-of-tolerance cell all read the same limits.
columns: [{ field: 'mm', type: 'number', spec: { lower: 9.5, upper: 10.8, target: 10 } }]
Cp and Cpk use short-term variation, estimated from the moving range; Pp and Ppk use the overall standard deviation. The gap between them is the point: Cpk well above Ppk means the process drifted. Cp above Cpk means it is precise and aimed wrong, which needs a different fix from being too variable.
baseline fixes the control limits over the first N readings. Without it the limits are computed over everything (including whatever the process did wrong) so a step change pulls the centre line between the two levels and both halves land outside three sigma. Technically true, and useless for finding when it moved.
Seeing it
The statistics have chart types to match, in modules/charts. Each takes its numbers from this namespace rather than recomputing, so a coefficient in a matrix and the same one from the API cannot drift apart.
| Type | What it shows |
|---|---|
| correlogram | Every numeric column against every other, on a ramp centred at zero so the sign reads first. method: 'spearman' ranks instead; where the two disagree, the pair is related but not linearly. |
Sample quantiles against normal ones. Jarque-Bera says a column is not normal; this shows how, a heavy tail bends the ends, a skew bows the whole line. The reference runs through the quartiles, as R's qqline does, because a fitted line is dragged by the very tails you are inspecting. | |
| ecdf | The share at or below each value, as a step. No bins, so its shape is not partly a choice, and two overlay cleanly where two histograms fight. |
| lorenz | The curve a Gini is read off, against the diagonal a perfectly even column would trace. |
| control | An individuals chart: control limits from the moving range, the specification, and points breaking a Western Electric rule. The control limits are the process talking and the specification is the customer talking: conflating them is the classic error, so they are drawn differently. |
| histogram | curve: true overlays a kernel density estimate, which has no bin edges and so separates what is in the data from what is in the binning. |
| scatter | fit: true draws least squares per series with R² beside it. |
Values the grid maintains for you
Three of the ideas in this section are not standard grid vocabulary, so it is worth saying what they have in common before the detail. Each of them is a value the grid keeps up to date from data you already have, declared once rather than maintained by hand.
The alternative, in every application that needs one of these, is a parallel structure in the host: a copy of what each row looked like a moment ago, a rank recomputed on every tick, a cumulative total that has to be redone whenever the sort changes, and a dashboard panel running its own query beside the table. That code works for a while and then produces a number nobody can account for, usually because one part of it noticed a filter and another did not.
| Concept | What it is | Reach for it when |
|---|---|---|
| Shadow column | An extra column, declared against a real one, holding something the grid works out about it: how it has changed, or where it sits among the others. | You want “what was this an hour ago”, “how many places has it moved”, or “which decile is it in” as a column you can sort and filter on. |
| Running column | A cumulative value: the total, or the share of the total, by the time you reach this row. | You want a running balance, a cumulative percentage, or a Pareto curve down the page. |
| Derived grid | A whole second grid whose rows are built from the first: grouped, unnested, filtered, ranked or profiled. | You want a top-five panel, a breakdown by region, an exceptions list or a statistics summary beside the table, and it must never disagree with it. |
The line between a shadow and a running column is the sort order. A shadow is a function of the column: a row’s own history, or where its value sits among the others. Sort the grid differently and a rank is still the same rank. A running total is the opposite : it answers “how much by the time we reach this row”, and by the time is the order the rows are in, so re-sorting changes every value in the column. That is why they are declared separately rather than as two kinds of one thing.
A derived grid is a different scale of the same idea. A shadow adds a column to the rows you have; a derivation produces different rows altogether: one per sales person rather than one per sale. Because it is a source rather than a special kind of grid, the result sorts, filters, totals, themes and exports like any other, and can itself be the source of another.
All three read the rows the grid is currently showing, so a filter applied to the table moves the ranks, the running totals and every derived panel together. That is the property worth having: not that any one of them is clever, but that they cannot disagree.
Shadow columns
A shadow column is declared against another column and maintained by the grid. It has no field in the data and it is not a pure computed column either, because its value depends on what happened before. It is a real column throughout: sortable, filterable, totalled, grouped, exported, saved into a view, which is what makes "show me every circuit repriced more than twice this session, most-changed first" one gesture rather than a report.
columns: [
{ field: 'price', type: 'number' },
{ id: 'moved', title: 'Change', shadow: { of: 'price', kind: 'delta' } },
{ id: 'churn', title: 'Updates', shadow: { of: 'price', kind: 'updates' } },
{ id: 'run', title: 'Streak', shadow: 'streak' }, // shorthand: shadows the column beside it
]
| Kind | Value |
|---|---|
| updates | How many times the row's value has changed. Arrival is not a change, so a freshly loaded grid reads zero rather than one. |
| updatedAt | When it last changed, as a Date. |
| sinceUpdate | Milliseconds since it last changed. |
| delta | Current value minus the baseline. |
| deltaPercent | The same as a percentage. A change from nothing has no percentage and reads null rather than infinity. |
| rate | Change per second, from the last two readings. |
| history | The recent readings, oldest first. depth sets how many; the default is 20. Counts changes by default - see the time-windowed form below for a real time series. |
| firstValue | The baseline itself. |
| streak | Consecutive moves in one direction, signed. It resets on a turn, because "seven rises" means something and "seven changes" does not. |
A second family answers where the row sits among the others rather than what it did before. They share the same declaration and the same state, the tracker already holds every row's current value and its baseline, which is exactly what a rank and a rank change need.
| Kind | Value |
|---|---|
| rank | Competition rank, largest first: ties share the better rank and the next value skips, so two firsts are followed by a third. |
| rankAsc | The same ranking read from the other end. |
| rankChange | Places climbed since the baseline. Positive means climbed, even though the rank number itself falls: this is the "top movers" column. |
| percentile | The share of rows at or below this one, 0 to 100. |
| quartile | 1 to 4, agreeing with percentile: the 60th percentile is in the third quartile. |
| zScore | Deviations from the mean. A column with no spread reads null rather than zero. |
| shareOfTotal | The value over the column's total, as a percentage. A total of zero (a column of offsetting positions) reads null rather than a division by it. |
Running totals
A running column answers “how much by the time we reach this row”: a balance that accumulates down the page, or the share of the total accounted for so far. It is the column a Pareto chart is made of, and the one a finance report opens with.
It is declared separately from a shadow column, and the sort order is the reason. Every shadow is a function of the column (of a row's own history, or of where its value sits among the others) so it reads the same however the rows are arranged. A running total does not: sort the grid differently and every value changes, because the question is "how much by the time we reach this row", and by the time is the sort order.
columns: [
{ field: 'amount', type: 'number' },
{ id: 'cum', title: 'Running', running: { of: 'amount', kind: 'total' } },
{ id: 'share', title: 'Cumulative %', running: { of: 'amount', kind: 'percent' } },
]
Computed in one pass over the display rows and cached against that ordering, so a hundred thousand rows are walked once per sort rather than once per cell. A running column is not sortable. Sorting on one asks the sort to depend on its own output (the value is defined by the display order) so the column does not offer a sort unless its definition asks for one, and the query layer refuses such a sort with a warning rather than computing it. Sort by the column it runs over instead. Group headings and totals rows are skipped, a running total that counted a subtotal would double everything below it, and a row with no value carries the figure forward unchanged rather than resetting it.
History has two clocks: a count of changes, or a span of time
Plain history (above) is an event series: it records a reading only when the value changes, so a row that sits still never advances it. Bound to a cell.render sparkline ('line', 'area', 'column', 'winloss') that means a static row's sparkline freezes, then jumps when a change finally lands - while a title like "last 60s" keeps claiming a span the column never measured. Add a time window to make it a real time series instead:
{ id: 'spark', title: 'Last 60s', shadow: { of: 'price', kind: 'history', window: { kind: 'time', span: 60_000 }, depth: 20 } }
This is the same window: { kind, span } shape the rolling kinds below already accept - not a second spelling of it - and it changes what history means rather than adding a new kind: depth (20 here) is now how many buckets the span divides into, so this is sixty seconds as twenty three-second buckets. Each bucket is sampled once, at its close, as the row's last known value at that moment - carried forward from the previous bucket when nothing changed in between. A static row therefore draws a flat line that keeps advancing, one sample per bucket, and a change lands in the bucket it actually happened in rather than being appended at the end. The bucket clock runs on its own low-frequency timer (never a render loop), so the series keeps moving even while no data event ever reaches the column. window: { kind: 'count' } and { kind: 'session' } are refused for history - a plain count is already what depth means without a window, and a session has no fixed span to divide into buckets - and a caller who never sets window gets the original count-based behaviour, unchanged.
Positional kinds rank over every tracked row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. Pass scope: 'filtered' on the shadow spec to rank within what the filters left instead: both answers are legitimate, which is why it is a choice rather than a default.
Shadow state is keyed by row key, never by index: after any sort an index-keyed history would report one row's past against another row's present, and the wrong number would be sortable. Memory is capped at 200,000 tracked rows per column; past that the oldest are dropped and tracking().forgotten says how many, rather than a smaller number being reported as though it were the truth.
Rolling time-series columns
A total answers "how much"; a rolling total answers "how much lately, as the series ran" - the seven-day average that smooths a daily figure, the trailing sum, the change on the period before. These are rolling shadow kinds: real columns, sortable and filterable and exportable like any other, computed in one ordered pass and cached by row key. They compose the windowed-aggregate model the grid already uses for "the average lately" over a live stream, asked instead over a column arranged in a stated order.
The order is explicit and required - an orderBy column, never the screen sort, because a rolling figure defined by the current sort would change on every header click and a column sorted on its own rolling value would define itself. The window is the last span rows (count), the last span of the order axis (time), or the whole series so far (session). The first rows carry a partial window; that figure is still emitted, but a windowCoverage companion stamps how much of the window it actually covers, so a two-day average is never shown as a seven-day one. within chooses per-group (the default, partitioned by the grid's grouping) or across the whole dataset.
A rollingQuantile (a trailing median, a p95, set by q) is exact while the window is small and comes from a KLL sketch past an internal span cap and for a session window - where a windowApproximate companion reports which rows are approximate, so a sketched quantile is never presented as exact. At a million rows the single ordered pass stays well within the suite's budget (≈380ms for the window aggregates, ≈490ms for the exact rolling median, ≈420ms for the session sketch on the reference bench).
columns: [
{ field: 'day', type: 'date' },
{ field: 'sales', type: 'number' },
{ id: 'ma7', title: '7-day avg', shadow: { kind: 'rollingAvg', of: 'sales', orderBy: 'day', window: { kind: 'count', span: 7 } } },
{ id: 'cover', title: 'Coverage', shadow: { kind: 'windowCoverage', of: 'sales', orderBy: 'day', window: { kind: 'count', span: 7 } } },
{ id: 'p50', title: '30-day median', shadow: { kind: 'rollingQuantile', of: 'sales', orderBy: 'day', window: { kind: 'count', span: 30 }, q: 0.5 } },
{ id: 'ytd', title: 'Cumulative', shadow: { kind: 'cumulativeToDate', of: 'sales', orderBy: 'day' } },
{ id: 'delta', title: 'vs prev', shadow: { kind: 'periodOverPeriod', of: 'sales', orderBy: 'day' } },
]
A rolling window is a property of the series, so it is computed over every row before any filter: hiding rows with a filter narrows what you see, never what "the last seven" means. A missing reading is a gap, skipped rather than treated as a zero that would report a plunge and a rebound the series never made.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// A short series, ordered by t: values 2,4,5,4,5.
const base = { of: 'v', orderBy: 't', within: 'all' };
const win = { kind: 'count', span: 3 };
const grid = createHeadlessGrid({
columns: [
{ field: 't', type: 'number' },
{ field: 'v', type: 'number' },
{ id: 'sum', shadow: { kind: 'rollingSum', window: win, ...base } },
{ id: 'avg', shadow: { kind: 'rollingAvg', window: win, ...base } },
{ id: 'cov', shadow: { kind: 'windowCoverage', window: win, ...base } },
{ id: 'med', shadow: { kind: 'rollingQuantile', window: win, q: 0.5, ...base } },
{ id: 'cum', shadow: { kind: 'cumulativeToDate', ...base } },
{ id: 'pop', shadow: { kind: 'periodOverPeriod', ...base } },
],
rows: [
{ id: 'r1', t: 1, v: 2 }, { id: 'r2', t: 2, v: 4 }, { id: 'r3', t: 3, v: 5 },
{ id: 'r4', t: 4, v: 4 }, { id: 'r5', t: 5, v: 5 },
],
rowKey: 'id',
});
const round4 = (x) => Math.round(x * 10000) / 10000;
const round2 = (x) => Math.round(x * 100) / 100;
return [
grid.rows.value('r3', 'sum'), // 2+4+5 = 11
round4(grid.rows.value('r3', 'avg')), // 11/3
round4(grid.rows.value('r5', 'avg')), // (5+4+5)/3
round2(grid.rows.value('r1', 'cov')), // 1/3 of the window filled
grid.rows.value('r5', 'cum'), // running total to the end
grid.rows.value('r2', 'pop'), // 4 - 2
grid.rows.value('r3', 'med'), // median of 2,4,5 = 4
].join('|');
Seasonal decomposition
Splitting a series into trend + seasonal + residual answers "what's the underlying trend with the weekly pattern removed?". It is classical decomposition - the same algorithm statsmodels.seasonal_decompose uses, verified against it in the reference suite - delivered as four shadow columns over the same ordered pass: tsTrend (a centred moving average), tsSeasonal (the repeating index), tsResidual (what the two leave behind), and tsCoverage.
The period is caller-declared and required - 7 for a weekly cycle in daily data, 12 for a monthly cycle in monthly data; there is no auto-detection in v1. The model is additive by default; decomposition: 'multiplicative' is a declared option that is undefined on a non-positive series (those rows report null, with a warning). The centred window runs off the ends, so the leading and trailing rows have no trend - they are partial edges, reported as null and stamped tsCoverage: 0 rather than emitted as if full.
columns: [
{ field: 'day', type: 'date' },
{ field: 'sales', type: 'number' },
{ id: 'trend', title: 'Trend', shadow: { kind: 'tsTrend', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'season', title: 'Weekly', shadow: { kind: 'tsSeasonal', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'resid', title: 'Residual', shadow: { kind: 'tsResidual', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'cover', title: 'Coverage', shadow: { kind: 'tsCoverage', of: 'sales', orderBy: 'day', period: 7 } },
]
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// A period-4 series: trend 10+i plus a season [2,-1,0,-1], so value = trend + season.
const season = [2, -1, 0, -1];
const base = { of: 'v', orderBy: 't', within: 'all', period: 4 };
const grid = createHeadlessGrid({
columns: [
{ field: 't', type: 'number' },
{ field: 'v', type: 'number' },
{ id: 'trend', shadow: { kind: 'tsTrend', ...base } },
{ id: 'season', shadow: { kind: 'tsSeasonal', ...base } },
{ id: 'resid', shadow: { kind: 'tsResidual', ...base } },
{ id: 'cover', shadow: { kind: 'tsCoverage', ...base } },
],
rows: Array.from({ length: 8 }, (unused, i) => ({ id: String(i), t: i, v: (10 + i) + season[i % 4] })),
rowKey: 'id',
});
return [
grid.rows.value('4', 'trend'), // centred MA recovers the trend: 14
grid.rows.value('4', 'season'), // the phase-0 seasonal index: 2
grid.rows.value('4', 'resid'), // nothing left over: 0
grid.rows.value('4', 'cover'), // interior row: full, 1
grid.rows.value('0', 'trend') === null ? 'null' : 'x', // partial edge: null, not invented
grid.rows.value('0', 'cover'), // edge stamped partial: 0
].join('|');
Exponential smoothing
Smoothing pulls the signal out of a noisy series. tsSmoothed is the fitted level - not a forecast of the future - from single exponential smoothing (smoothing: 'ses', the default) or Holt's level+trend (smoothing: 'holt'). The recursion matches statsmodels and is verified against it in the reference suite. Holt-Winters (seasonal) smoothing is deferred; seasonality is covered by decomposition above.
The smoothing factor is either caller-set (alpha, and beta for Holt) or fit by minimising the in-sample SSE when omitted - and the chosen value is reported, not hidden, by the tsSmoothingAlpha / tsSmoothingBeta companion columns.
columns: [
{ field: 'day', type: 'date' },
{ field: 'sales', type: 'number' },
{ id: 'level', title: 'Smoothed', shadow: { kind: 'tsSmoothed', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
{ id: 'a', title: 'α', shadow: { kind: 'tsSmoothingAlpha', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
{ id: 'b', title: 'β', shadow: { kind: 'tsSmoothingBeta', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
]
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// SES at alpha 0.5 over 4,8,6,10: level runs 4, 6, 6, 8.
const base = { of: 'v', orderBy: 't', within: 'all', smoothing: 'ses', alpha: 0.5 };
const grid = createHeadlessGrid({
columns: [
{ field: 't', type: 'number' },
{ field: 'v', type: 'number' },
{ id: 'sm', shadow: { kind: 'tsSmoothed', ...base } },
{ id: 'a', shadow: { kind: 'tsSmoothingAlpha', ...base } },
],
rows: [4, 8, 6, 10].map((v, i) => ({ id: String(i), t: i, v })),
rowKey: 'id',
});
return [
grid.rows.value('1', 'sm'), // 0.5*8 + 0.5*4 = 6
grid.rows.value('3', 'sm'), // 0.5*10 + 0.5*6 = 8
grid.rows.value('0', 'a'), // the factor used, reported: 0.5
].join('|');
Stationarity (ADF)
Before you compare two series or detrend one, it helps to know whether it is stationary - reverting to a level or trend - or wandering with a unit root. grid.statistics.adf runs the Augmented Dickey-Fuller test and returns a scalar readout, not a per-row column: the statistic, the augmenting lag chosen by AIC, MacKinnon's critical values, an interpolated p-value (stamped approximate), and a plain-language verdict at the 5% level. The constant+trend regression and the AIC lag choice match statsmodels' adfuller, against which the statistic and lag are verified.
The lag search and what it costs. maxlag caps the number of augmenting lags the AIC search considers; left out, it is the Schwert rule ⌈12·(n/100)^0.25⌉ - 34 candidates on 7,000 rows - itself capped so the fixed sample keeps degrees of freedom. The candidates are nested, so the search builds one design matrix at the cap and reads every smaller candidate off it - one pass to accumulate the normal equations, then a small solve and a single residual pass per candidate, rather than a fresh fit each time. The default search over 7,000 rows is a matter of milliseconds. Setting maxlag narrows the search, never the arithmetic: the lag chosen, the statistic and the p-value are whatever the data says, and a cap that still contains the AIC-preferred lag returns exactly the same readout.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// A random walk (a unit root): it wanders rather than reverting.
const walk = [0.138, -0.725, -1.26, -0.536, -0.267, 0.167, -0.765, -0.146, -0.311, -0.586,
0.376, -0.41, 0.282, 0.865, -0.024, 0.149, -0.2, -0.654, -1.338, -1.917, -1.832, -1.333,
-0.492, -1.259, -1.89, -2.145, -2.146, -1.297, -1.26, -0.716, -0.846, -1.206, -2.126,
-1.724, -0.945, -1.599, -1.316, -0.413, 0.304, 0.732, -0.257, 0.086, -0.572, -0.501,
-1.153, -1.186, -1.455, -1.607];
const grid = createHeadlessGrid({
columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }],
rows: walk.map((v, i) => ({ id: String(i), t: i, v })),
rowKey: 'id',
});
const adf = grid.statistics.adf({ of: 'v', orderBy: 't' });
return [adf.verdict, adf.usedLag].join('|'); // non-stationary, 0 lags
Autocorrelation (ACF / PACF)
grid.statistics.acf shows how far back a series depends on itself: the autocorrelation (ACF) and partial autocorrelation (PACF) arrays out to a maximum lag, each with the approximate ±1.96/√n white-noise band (stamped approximate) - a lag whose bar clears the band is evidence of real dependence. The estimators are the biased ACF and the Yule-Walker (Levinson-Durbin) PACF, matching statsmodels, verified in the reference suite. Lag 1 is the single source of truth: acf[1] is the same number statistics.series(...).autocorrelation reports, and pacf[1] === acf[1].
The correlogram is the arrays fed to a bar chart over explicit points, with the band as reference lines - reusing the existing chart primitives:
const { acf, bounds } = grid.statistics.acf({ of: 'sales', orderBy: 'day', maxlag: 20 });
createChart({
grid, container: '#acf', type: 'bar',
points: acf.map((v, lag) => ({ x: lag, y: v })),
reference: [{ value: bounds.upper }, { value: bounds.lower }, { value: 0 }],
});
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// A deterministic AR(1): each reading leans 0.6 on the one before.
let s = 5; const rand = () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296 - 0.5; };
const y = []; let prev = 0;
for (let i = 0; i < 200; i++) { const v = 0.6 * prev + rand(); y.push(v); prev = v; }
const grid = createHeadlessGrid({
columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }],
rows: y.map((v, i) => ({ id: String(i), t: i, v })),
rowKey: 'id',
});
const res = grid.statistics.acf({ of: 'v', orderBy: 't', maxlag: 6 });
const series = grid.statistics.series('v', { by: 't' });
return [
res.acf[0], // lag 0 is always 1
res.pacf[1] === res.acf[1], // the first partial equals the first acf
Math.abs(res.acf[1] - series.autocorrelation) < 1e-9, // lag 1 is the single source of truth
].join('|');
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.
grid.views
Named states the user can return to. Views supplied in config.views.saved are defined views: listed apart in the picker, and neither renamable nor deletable, refused by the model as well as hidden in the interface. Views the user saves are their own and carry rename, share, default and delete.
createGrid(el, {
views: {
saved: [{ id: 'escalations', name: 'Escalations', description: 'Worst SLA first',
state: { filters: { col: 'statusId', op: 'eq', value: 4 },
sort: [{ col: 'utilisation', dir: 'desc' }] } }],
allowSave: true, // false removes the save form entirely
local: true, // saved views live in this browser's localStorage, no backend
// storage: { read, write }, // or bring your own backend; see the note below
},
});
| Method | Returns | Description |
|---|---|---|
| list() / get(id) | object[] / object | |
| save(name, opts?) | object | Captures the current state. opts: id, description, shared, isDefault. |
| apply(id) | object | null | One undo entry. Resets to the baseline first, so a view is a destination rather than a patch, the same view gives the same grid whatever was applied before it. |
| rename(id, name) | object | null | Null for a defined view. |
| remove(id) | boolean | False for a defined view. |
| setDefault(id) | object | null | null clears it. A default view is applied on load, without recording an undo entry. |
| export(id) / import(json) | object | A JSON payload. What "sharing" means is yours to decide. |
| diff(id) | object | null | What applying a view would change. |
| reload() | void | Re-read from storage, discarding what is in memory. |
| activeId | string | null |
The grid makes no network calls. storage.write is a synchronous mirror. To persist to a server, listen for view:saved, view:renamed, view:removed and view:default: each carries the one view that moved, so you can send a single record rather than diffing two lists. Because the grid does not track whether your write landed, a failed request leaves the view visible locally: catch it and call views.reload().
views.local is the no-backend option: true stores views under a default localStorage key, shared by every grid on the origin; { key: '…' } picks a key of your own, for more than one grid whose views should stay apart. Given alongside an explicit storage, storage wins and local is ignored, with a console warning, the two are never merged. Built on createLocalViewStorage, exported for direct use (a custom key, or a different Storage-shaped backing such as sessionStorage) without the local shorthand.
grid.diff
Audit mode. Give it a prior snapshot and every row reports whether it was added, removed or changed, and which cells moved.
| Method | Returns | Description |
|---|---|---|
| setSnapshot(rows) / clear() | void | Also settable as config.diff.snapshot. |
| summary() | object | { added, removed, changed, unchanged }. |
| statusOf(key) | string | 'added', 'removed', 'changed' or 'unchanged'. |
| changedColumns(key) | string[] | |
| before(key, colId) | unknown | The prior value. Also on the cell as data-before. |
| enabled | boolean | |
| swap() | boolean | Show the snapshot as the grid's data, and compare it against what was live until now. The snapshot is held as plain objects and never enters the columnar store, so a removed row cannot be sorted or filtered among live ones; swapping is the answer to that, the old rows become real rows with the whole pipeline behind them. Costs one ingest of each set, so it is a deliberate action rather than a toggle. The comparison reverses: what was an addition is now a removal. swapped reports which way round the grid is, and it is worth saying so in your interface. |
| swapped | boolean | True while the snapshot is the data. |
| removedRows | false | 'pinned' | 'data' | Whether a row in the snapshot but gone from the data is shown, and whether it counts as data. false (the default) leaves it out. 'pinned' shows it beneath the rows, struck through, outside the row set, not counted, not exported, not selectable. 'data' appends it to the set, so it is counted and exported. Neither is sorted or filtered among the live rows, because its values are the snapshot's; neither can be edited, because there is nothing left to write to. |
| strictNull | boolean | Off by default, so null, undefined and an absent field all count as the same absence. Set it to tell them apart, for an audit where a field being cleared and a field never being sent are different events. It compares the data as supplied, not as stored, so it is a statement about your snapshot rather than about the grid. |
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.permissions
Four levels per column, resolved from configuration or a callback. They are the four corners of read × write rather than a ladder:
| Level | Visible | Readable | Editable | For |
|---|---|---|---|---|
| hidden | , | , | , | Absent from the grid, the tool panel, exports, the clipboard, saved state, the filter model and formula references. |
| read | yes | yes | , | No editor opens; paste, fill and range-clear skip it. |
| writeOnly | yes | , | yes | A secret: an API key a user may rotate but never read. The cell shows a mask and the editor opens empty, and a formula in another cell cannot reference it. |
| write | yes | yes | yes | The default, so the feature is opt-in. |
permissions: 'read' // blanket
permissions: { salary: 'read', ssn: 'hidden' } // map; '*' sets the default
permissions: (column, ctx) => ctx.context.role === 'admin' ? 'write' : 'read'
permissions: { default: 'read', columns: { name: 'write' }, resolve }
grid.permissions.setContext({ role: 'clerk' }); // re-resolves everything
For three of the four this is a usability control, not a security boundary. Anything the grid can render it has already loaded, and devtools reaches it. writeOnly is the exception, and the reason it is worth having: nothing in the grid needs the value, so your server can send null for that field and the column still works: at which point the secret is genuinely not on the page. Enforce everything else on the server; permittedColumns and permittedExport are pure and dependency-free so the same policy can run there.
grid.ai
The grid composes a prompt describing its own columns and operators, you send it to whichever model you like, and it validates the reply before anything is applied. It makes no network call and has no default model.
| Method | Returns | Description |
|---|---|---|
| schema(opts?) | object | The schema: columns, types, permitted operators. |
| prompt(text, opts?) | string | The message to send, schema included. |
| plan(reply, opts?) | object | Parse and validate. Unknown columns and operators are rejected with a reason; valid actions in the same reply are kept. plan.describe() renders it in plain English for confirmation. |
| apply(plan) | object | Applies an approved plan as one undo entry, labelled with what it did. |
Actions: setFilters, setSort, groupBy, showColumns, hideColumns, setQuick, clear. Nothing else is executable, so a model cannot be talked into an operation the vocabulary does not contain. docs/AI-SKILL.md is the reference to hand your model.
Ask-your-data (modules/ai)
The opt-in AI module (createAI, UMD LatticeGridAI) turns a question into
a validated read-only query spec, runs it in the grid's own engine over the
grid.ai skill layer, and - on apply - fans the answer to any router-attached viewers
(a chart, a KPI tile) through the Data Router's load(). It uses the same BYO
ask() seam as the rest of the module: the grid makes no model call and holds no key.
Ask-your-data is read-only; a write/mutation the model asks for is refused and never
executed (writes are a separate, human-gated feature).
| Method | Returns | Description |
|---|---|---|
| query(question, opts?) | Promise<result> | Ask the model, validate the reply into a read-only spec. result.describe() shows the resolved query; nothing applies until result.apply() (or autoApply). |
| applyQuery(result, opts?) | object | Apply a reviewed result. Re-gated at the seam: an unsafe plan is refused. opts.router fans the answer to other viewers. |
| askBar(el, opts?) | controller | Mount the ask/review/apply bar with an "auto-apply safe reads" toggle (off by default). Mounts only when createAI's enable allows 'query'/'ask' (all allowed when omitted); query() itself is never gated. |
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { createAI } = await import('../packages/modules/ai/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'region' }, { field: 'amount', type: 'number' }],
rows: [{ id: 1, region: 'EMEA', amount: 100 }, { id: 2, region: 'AMER', amount: 300 }, { id: 3, region: 'EMEA', amount: 200 }],
});
// Your model returns a schema-constrained SPEC, never rows. WE run it, read-only.
const reads = createAI(grid, { ask: async () => ({ actions: [{ type: 'setFilters', filters: { col: 'region', op: 'eq', value: 'EMEA' } }] }) });
const result = await reads.query('EMEA only');
result.apply();
const rows = grid.rows.data().length;
// A write verb is refused by the read-only gate and never executed.
const writes = createAI(grid, { ask: async () => ({ actions: [{ type: 'setCells', edits: [] }] }) });
const write = await writes.query('change the data');
return `${rows} rows; write ${write.ok ? 'allowed' : 'refused'}`;
AI as a governed actor (modules/ai, writes)
The governed actor lets the model propose edits - a single NL-targeted
change (“set the Network Upgrade project to In Progress”) or a bulk cleanup - that a human previews as a before/after diff and approves. The model NEVER writes.
On approval the edit applies through the grid's own gate, exactly like a person's edit:
a grid cell edit via grid.edit.setCells(writes, 'cell', { origin: 'ai' }) (the
beforeEdit veto), a Kanban card move via board.move(…, { origin: 'ai' })
(the beforeMove veto). The AI can never bypass the gate: a host
beforeEdit/beforeMove handler that calls preventDefault()
(or vetoes async) stops the write, and nothing persists. Writes carry origin: 'ai'
on the before-event payload, so a host can allow a person's edit while vetoing the AI's - policing AI writes distinctly. Approved edits are optimistic and revert on a source reject
through the shipped write-back (there is no separate beforeCommit event; the edit gate
is beforeEdit, and the optimistic/revert half is grid.edit.settle).
NL targeting and any bulk edit bind to the current filtered view
(grid.rows.data()), never the whole table implicitly; a target not in the view is
surfaced for an explicit opt-in widen, not edited silently. A human label is resolved
to the column's stored option value; an unknown label is rejected, not coerced. An ambiguous match
(more than one row) surfaces its candidates for the user to pick.
| Method | Returns | Description |
|---|---|---|
| propose(instruction, opts?) | Promise<proposal> | Ask the model for structured edits, validate + resolve them against the current view, and return a reviewable proposal with a before/after diff. Nothing is written. opts.widen opts into the full dataset; opts.board routes a Kanban move. |
| applyProposal(proposal, opts?) | Promise<report> | Apply an approved proposal through the gate (origin: 'ai'). A vetoing before-handler stops it; the report gives applied/vetoed. |
| actorBar(el, opts?) | controller | Mount the propose → review-diff → approve bar, stating the scope. Mounts only when createAI's enable allows 'actor' (allowed when omitted); propose() itself is never gated. |
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { createAI } = await import('../packages/modules/ai/index.js');
const status = { options: [{ id: 'todo', label: 'To do' }, { id: 'doing', label: 'In Progress' }] };
const seed = () => createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'name' }, { field: 'status', lookup: status, edit: { enabled: true } }],
rows: [{ id: 'r1', name: 'Network Upgrade', status: 'todo' }, { id: 'r2', name: 'Payroll', status: 'todo' }],
});
// A MOCK ask() returns a structured PROPOSAL - never a write, never rows.
const ask = async () => ({ structured: { edits: [{ match: 'Network Upgrade', column: 'status', value: 'In Progress' }] } });
// Propose: a before/after diff, nothing written yet.
let grid = seed();
let ai = createAI(grid, { ask });
const proposal = await ai.propose('set the Network Upgrade project to In Progress');
const d = proposal.diff[0];
const before = `${d.oldDisplay} then ${d.newDisplay}`;
// A vetoing beforeEdit handler on the AI write => NOTHING persists.
grid.on('beforeEdit', (e) => { if (e.origin === 'ai') e.preventDefault('reviewed elsewhere'); });
await ai.applyProposal(proposal);
const vetoed = grid.rows.byKey('r1').data.status;
// Approve on a grid with no veto => the edit lands through the gate.
grid = seed();
ai = createAI(grid, { ask });
await ai.applyProposal(await ai.propose('set the Network Upgrade project to In Progress'));
const approved = grid.rows.byKey('r1').data.status;
return `diff ${before}; vetoed kept ${vetoed}; approved wrote ${approved}`;
grid.overlay
| Method | Returns | Description |
|---|---|---|
| show(kind, message?) | void | 'loading' or 'empty'. |
| hide() | void |
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.
grid.licence
There is one Lattice Grid and every copy is feature-identical. No community edition, no pro tier, no feature held back behind a key. A licence removes the trial watermark; that is the whole of what it does.
Free to develop against, licensed to deploy. A grid on localhost, or any loopback host: needs no key at all. On any other domain an unlicensed grid still renders everything and carries a small trial watermark linking to latticegrid.dev. Nothing ever refuses to render: the failure worth avoiding is a broken production screen, and no licensing state is worth causing one.
// Before creating a grid.
LatticeGrid.setLicence('LG1.…'); // your key; setLicense also works
const grid = LatticeGrid.createGrid(el, config);
grid.licence.state(); // 'licensed' | 'localhost' | 'trial'
| Method | Returns | Description |
|---|---|---|
| set(key) | object | Install a key for the process. Returns the provisional verdict; the check is asynchronous and licence:changed fires when it settles. |
| state() | string | 'licensed', 'localhost' or 'trial'. |
| info() | object | { valid, reason, issuedTo, expires, product }. expires is undefined for a perpetual key (the default) and an ISO date only for one deliberately issued with a term. |
| watermark() | boolean | Whether the trial mark is showing. |
| ready | Promise | Settles when the licence check finishes. |
Domains
A key names the hosts it covers. *.acme.com matches app.acme.com,
a.b.acme.com and acme.com itself, a wildcard that refused the
apex would be a puzzle rather than a licence. A bare acme.com matches only itself,
and a key naming no domains is valid anywhere. Matching ignores case and a trailing dot.
| Host | No key | Key for *.acme.com |
|---|---|---|
| localhost, 127.0.0.1, ::1, *.localhost | everything, no mark | everything, no mark |
| app.acme.com | everything, trial watermark | everything, no mark |
| acme.com | everything, trial watermark | everything, no mark |
| other.example.org | everything, trial watermark | everything, trial watermark |
.local, .internal and private IP ranges are not exempt. They are ordinary LAN names, and a corporate intranet is a deployment like any other.
Getting a key
Keys are issued from latticegrid.dev. A key is issued per domain rather than per developer or per seat: name the domains the grid will run on and one key covers every developer, every build and every user on them.
Keys are perpetual by default. A key carries no expiry unless one was deliberately issued (a trial, a time-boxed pilot) so the ordinary key is valid until the domains it names change, not one that quietly lapses on a date nobody is tracking.
Checking a key needs no network. There is no licence server, no call home, and nothing that can fail at three in the morning, a key carries its own answer and the grid reads it locally, fresh on every load. A key for the wrong domain, or a key that will not read at all, does the same thing a genuinely expired trial key does: log one console warning and show the watermark.
Install the key before creating a grid. Setting one later still works,
licence:changed fires and the watermark is removed, but the first frames of the
grid will carry it.
Sources
Where rows come from. memory is the default and needs no configuration.
A memory grid's rows may be declared either way. The top-level
rows option is the usual one and what every example here uses; the same array
inside the block works identically - createGrid(el, { columns, source: { mode:
'memory', rows } }) opens with exactly those rows, with the same
count(), value(), text(), type inference and bound KPI
and chart readings as createGrid(el, { columns, rows }), and
rows.load() and rows.apply() behave the same afterwards. Either
array is copied on ingest, so the one you passed is never written to (see
rows under Configuration). Declare them once:
a grid given both uses the top-level rows, ignores the block's, and warns
once naming both places. Only memory reads a rows key from the block - the fetching modes take theirs from the server.
| Mode | Needs | Description |
|---|---|---|
| memory | rows | Everything is present. The grid filters, sorts, groups and totals it. |
| paged | fetch | A page at a time from a server that paginates. |
| remote | fetch | Blocks fetched as the viewport reaches them, with sort, filter and grouping pushed to the server. |
| stream | connect | Rows arriving over time. Promotes to memory once complete. |
| derived | from | Rows built from another grid: grouped, unnested, filtered, ranked or profiled. Read-only, and follows the source. |
Load from a URL: a JSON or NDJSON file
createUrlSource(url, opts) points the grid straight at a file. A JSON
file (a top-level array, or a nested array picked out with rowsPath or map)
is read whole and handed over as rows. An NDJSON / JSONL file - one JSON value
per line - is streamed in incrementally: the first rows render while the rest is
still arriving, and a large file never sits in memory as one string. It is built on the
stream source, so it inherits the frame-coalesced render, the stream:chunk /
stream:end progress events and promotion to memory once a small file has fully landed.
Format is resolved in order: an explicit format: 'json' | 'ndjson' wins; else the URL
extension (.ndjson/.jsonl vs .json); else the
Content-Type (application/x-ndjson vs application/json); else a
sniff of the first bytes, or a clear error asking for an explicit format. Pass
fetch to supply auth or a proxy (default is the global fetch),
headers to merge request headers, batchSize to tune NDJSON chunking
(default 500), lenient: true to skip a malformed NDJSON line with a warning rather
than failing, and poll (ms) to re-fetch on an interval, replacing the rows each pass.
Every failure - a non-2xx status, a network error, a bad body, a malformed line - surfaces
as source:error and leaves the grid usable, never an uncaught throw. The fetch is aborted
when the grid is destroyed. Zero new dependencies: fetch,
response.body.getReader() and TextDecoder.
A JSON file loaded through a mock transport so the example runs headless with no
network; in an app, omit fetch and the global one is used. Run on every build.
const { createHeadlessGrid, createUrlSource } = await import('../packages/core/src/index.js');
// A mock transport, so this runs with no network. In your app, drop `fetch`.
const file = JSON.stringify([{ id: 1, city: 'Oslo' }, { id: 2, city: 'Lima' }, { id: 3, city: 'Cairo' }]);
const fetchImpl = async () => new Response(file, { headers: { 'content-type': 'application/json' } });
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'city' }],
source: createUrlSource('https://example.test/cities.json', { fetch: fetchImpl }),
});
await new Promise((r) => setTimeout(r, 50)); // let the file load
const count = grid.rows.count();
grid.destroy();
return count; // 3
For NDJSON, point it at a .ndjson or .jsonl URL - createUrlSource('/events.ndjson') - and the rows stream in as they parse.
Derived sources: a grid built from another grid
Most dashboards put a summary panel beside a table, the top five sales people, the breakdown by region, the exceptions list. Built by hand, that panel runs its own query, and sooner or later somebody filters the table and the panel does not follow. Everyone who has shipped a dashboard has been in the meeting where two numbers on one screen disagree.
A derived grid removes the possibility. It is a second grid whose rows are built from the first (grouped, unnested, filtered, ranked or profiled) so the panel is the table, one derivation later, and one filter moves both. It answers the questions a summary panel exists for: the top five sales people, the most-sold SKUs, a statistical profile of whatever the user has filtered to.
It is a source rather than a new kind of grid, so everything downstream: its own sorting and filters, totals, shadow columns, formatting, export, themes: works on the result and knows nothing about where the rows came from. Charts bind to one as readily as to any grid.
source: {
mode: 'derived',
from: salesGrid,
follow: 'filtered',
groupBy: 'rep',
select: { revenue: { of: 'amount', fn: 'sum' }, deals: { fn: 'count' } },
sort: [{ col: 'revenue', dir: 'desc' }],
limit: 5,
}
The pipeline runs in one order, and the order is the contract:
unnest → where → bucket → group → reduce → sort → limit.
where sits before grouping deliberately: filtering afterwards is a different
question, which groups, not which rows, and one key cannot mean both.
| Key | Type | Description |
|---|---|---|
| from | Grid | UnionSourceOptions[] | Required. The grid to read - or several to combine into one row set before the rest of the pipeline runs (a union; see below). A bare Grid in the array is shorthand for { grid }. |
| follow | 'filtered' | 'all' | 'selected' | 'grouped' | Which of its rows to read. filtered by default. grouped re-aggregates by whatever dimension the user has grouped the source by, so a panel tracks the reader rather than a dimension fixed when the page was built; with the source ungrouped it falls back to groupBy. Ignored (with a warning) when from is a union array - each entry has its own follow instead. |
| unnest | string | Expand an array property, one row per element, keeping the parent's fields. Address the element with a dotted path afterwards: lines.sku is the element, region is still the parent. A row whose property is absent or empty contributes nothing. |
| join | { with, on, type, select, prefix, follow } | Match each row against a second grid on a shared key and bring some of its fields across. Runs after unnest and before where, so a condition (and a grouping, and a total) can read a field the join produced. |
| where | (row) => boolean | A row predicate, applied before grouping. With no groupBy the rows pass through as themselves, which is how an exceptions list is built. |
| bucket | { of, by } | Round a date column down to the start of its period and group on that. by is day, week, month, quarter or year; weeks start on the ISO Monday. |
| groupBy | string | string[] | The dimension, or dimensions, to group by. Omit to pass rows through. |
| select | Record<string, {of, fn}> | The reduced columns, by output id. fn is any key of the totals-row kernels, so median, p95, stddev and gini are available as readily as sum. count needs no of. |
| sort | { col, dir }[] | Order the derived rows before limiting them. The grid's own user-facing sort is separate and unaffected. |
| limit | number | Keep at most this many rows. |
| limitPer | string | Apply limit within each distinct value of this column rather than overall, the best three SKUs in each region, which a global limit cannot express. |
| cumulative | { of, upTo } | Keep rows until their running share of the total reaches upTo, 0 to 1. The Pareto question. The row that crosses the cutoff is kept, because the set has to reach the share. |
| profile | string | string[] | Replaces the pipeline with a transpose: one row per column, with count, present, missing, distinct, min, max, mean, median, quartiles, deviation and outlier count as its columns. |
| orient | 'columns' | 'metrics' DerivedOrient | With profile, emit one row per statistic instead of one per column, the shape a dashboard tile wants. |
| crossFilter | boolean | string | Let this grid filter the grid it derives from. true cross-filters through whatever it groups by; a string names a different source column. |
| refresh | 'live' | 'idle' | 'manual' | number | When to re-derive. idle by default, coalescing to a frame, because a hundred cell updates in one frame are one derivation. A number debounces by that many milliseconds. live re-derives on every change. manual never re-derives on its own: the host triggers it by calling rows.load() on the derived grid, with no argument, which re-reads from there and then - a frozen panel refreshed on a button press, executed. |
// refresh: 'manual' - the summary re-derives only when the host asks.
refreshButton.addEventListener('click', () => summary.rows.load());
The relational statistics, as rows
A single-column statistic already has a route: select reduces a group with any
kernel the totals row uses, and that table is a superset of the statistics one, so
select: { p95: { of: 'amount', fn: 'p95' } } works, along with
median, stddev, gini and the rest.
statistics is for what select structurally cannot reach: the
figures needing two or more columns, or a second grid. Like profile it is a
terminal producer - it replaces the pipeline rather than joining it, and the two cannot
be used together. Every row carries n, the rows the figure covered. Full detail,
including the measured re-derive cost of each producer, is in
the detail reference.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const readings = Array.from({ length: 20 }, (_, i) => ({ id: i, t: i, v: 100 + i * 3, w: 50 - i }));
const plant = createHeadlessGrid({ rowKey: 'id',
columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }, { field: 'w', type: 'number' }],
rows: readings });
plant.rows.count();
// One row per column PAIR: { a, b, coefficient, n }. `w` runs exactly against `t`.
const pairs = createHeadlessGrid({ rowKey: '__key',
columns: [{ field: 'a' }, { field: 'b' }, { field: 'coefficient', type: 'number' }, { field: 'n', type: 'number' }],
source: { mode: 'derived', from: plant, refresh: 'live',
statistics: { fn: 'correlation', columns: ['t', 'v', 'w'] } } });
let tw = null;
pairs.rows.forEach((row) => {
if (pairs.rows.value(row.key, 'a') === 't' && pairs.rows.value(row.key, 'b') === 'w') {
tw = pairs.rows.value(row.key, 'coefficient');
}
});
// One row per METRIC of the series summary, not one per point.
const series = createHeadlessGrid({ rowKey: '__key',
columns: [{ field: 'metric' }, { field: 'value', type: 'number' }],
source: { mode: 'derived', from: plant, refresh: 'live',
statistics: { fn: 'series', of: 'v', by: 't' } } });
// One row per compared column, against a second grid. The peer is watched.
// `t` is on this grid only, so it is reported as unmatched rather than dropped.
const peer = createHeadlessGrid({ rowKey: 'id',
columns: [{ field: 'v', type: 'number' }, { field: 'w', type: 'number' }],
rows: readings.map((r) => ({ id: r.id, v: r.v * 2, w: r.w })) });
peer.rows.count();
const compared = createHeadlessGrid({ rowKey: '__key',
columns: [{ field: 'column' }, { field: 'magnitude', type: 'number' }],
source: { mode: 'derived', from: plant, refresh: 'live',
statistics: { fn: 'datasetVsDataset', with: peer, columns: ['v', 'w'] } } });
let shared = 0, unmatched = 0;
compared.rows.forEach((row) => {
if (compared.rows.value(row.key, 'magnitude') === null) unmatched += 1; else shared += 1;
});
return `t/w ${tw}; pairs ${pairs.rows.count()}; metrics ${series.rows.count()}; compared ${shared} + ${unmatched} unmatched`;
Union sources: combining several grids into one
"Worst performers across two datasets" is easy when the two datasets share a key: a
join brings the second grid's fields onto the first. It is not expressible at all
when they do not: incidents from two regions with no shared identifier, orders from two
systems, this quarter and last as one ranked list. from takes an array of sources
for exactly this: stack several row sets into one, then rank, group or filter the combined set
with the same pipeline a single from already runs.
source: {
mode: 'derived',
from: [
{ grid: eastIncidents, label: 'east' },
{ grid: westIncidents, label: 'west' },
],
sort: [{ col: 'severity', dir: 'desc' }],
limit: 10,
}
Every source is read (each narrowed by its own follow, filtered by
default) and concatenated in declaration order, deterministic rather than
interleaved, before unnest/join/where/bucket/
groupBy/select/sort/limit/limitPer/
cumulative run once over the result - so "the worst across both" is one
derivation, not a hand-merged array.
| Key | Type | Description |
|---|---|---|
| grid | Grid | Required. This source's grid. |
| label | string | Identifies this source. Carried onto every row as __source, and used to namespace that row's __key. Defaults to the source's position in the array ('0', '1', …). |
| follow | 'filtered' | 'all' | 'selected' | 'grouped' | Which of this source's rows to read, independent of every other source's. filtered by default. |
| map | (row) => unknown | Reshape this source's rows into a common shape before they join the rest - typically a rename or a projection for a field this source calls something else. |
__source is required, not optional. Every row carries it - the entry's label, or its declaration index when unlabelled - because without
it a combined list cannot be read, filtered or grouped by where it came from, which is most of
the point of stacking several sources. It is an ordinary field to where,
groupBy and select, exactly like a column the data itself carries.
The union of fields, not the intersection. A field present on only one source
is undefined on rows from the others - not fabricated, not coerced. Sources
are not type-reconciled: if two disagree on what a field means, map
is where you make them agree, before they combine, not something the union guesses at for you.
The key is namespaced. A derived grid's __key is the source row's
own key when nothing is grouped, and two sources sharing the same identifiers would otherwise
collide. So it is qualified by the source tag when there is no groupBy. Grouped, the
key is the group value exactly as it always has been - rows from different sources landing
in the same group when their group values agree is the point of grouping a union, not a
collision to guard against.
Not a join. There is no dedup and no merge-on-key: two sources reporting the
same fact both appear as separate rows, and there is no UNION-vs-UNION-ALL distinction to draw.
Reach for join when two sides share a key and you want them matched rather than
stacked; use groupBy on the combined set when you want them summed together.
Empty and failing sources. A source with no matching rows contributes nothing;
the rest of the union still derives. A source that throws while being read (or mapped) is named
in a warnOnce and skipped for that pass - reported, never silently dropped,
because a silently missing source would make "worst across both" quietly wrong.
A cycle is refused, not recursed. A source list that includes the grid being derived, directly or through a chain of other derived grids, is refused when the source is built, naming the offending source.
Not supported alongside a union. crossFilter has no single target
once there is more than one parent, and profile reduces one grid's own columns, so
both are refused with a warning rather than guessed at.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const east = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'title' }, { field: 'severity', type: 'number' }],
rows: [{ id: 'e1', title: 'disk-full', severity: 9 }, { id: 'e2', title: 'slow-query', severity: 3 }],
});
// A second, unrelated incident log - its own "rating" field, no shared id with `east` at all.
const west = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'name' }, { field: 'rating', type: 'number' }],
rows: [{ id: 'w1', name: 'oom-kill', rating: 10 }, { id: 'w2', name: 'stale-cache', rating: 2 }],
});
const worst = createHeadlessGrid({
columns: [{ field: 'title' }, { field: 'severity', type: 'number' }, { field: '__source' }],
source: {
mode: 'derived',
from: [
{ grid: east, label: 'east' },
// `map` brings west's differently-named fields into the common shape.
{ grid: west, label: 'west', map: (row) => ({ title: row.name, severity: row.rating }) },
],
sort: [{ col: 'severity', dir: 'desc' }],
limit: 2,
},
});
const titles = [];
worst.rows.forEach((r) => titles.push(worst.rows.value(r.key, 'title')));
// A per-source breakdown reads `__source` like any other field.
const bySource = createHeadlessGrid({
columns: [{ field: '__source' }],
source: { mode: 'derived', from: [{ grid: east, label: 'east' }, { grid: west, label: 'west' }] },
});
const sources = [];
bySource.rows.forEach((r) => sources.push(bySource.rows.value(r.key, '__source')));
worst.destroy(); bySource.destroy(); east.destroy(); west.destroy();
return `worst=${titles.join(',')}; sources=${sources.join(',')}`;
Read-only. A derived row is an answer, not a record: there is no write-back for the sum of four hundred rows, so writes are refused with a reason rather than accepted and discarded on the next refresh.
They chain. A derived grid can be the source of another to any depth: a profile of the top five, and a change at the root travels the whole chain. A cycle is refused rather than recursed.
The key. rowKey defaults to the derived key and need not be set.
It is the group value, which is what makes a live ranking readable: the row moves rather than
the values under it changing.
Cost. The first derivation is linear in the rows read and largely independent
of what is reduced: roughly 900 ms per 200,000 rows grouped into forty, whether the selection is one sum
or four statistics. After that, a change that names the rows it touched is patched
rather than re-derived: only the groups those rows entered or left are reduced again, so a live
feed costs time proportional to what changed rather than to the table. Five hundred updates
against that same source take under 300 ms in total, not 300 ms each. A joined
derivation is maintained the same way from both sides: the lookup is held between derivations
rather than rebuilt, a change to the fact table rejoins only the rows that moved, and a change
to the lookup rejoins only the rows behind the keys whose match actually changed, about
2 ms per fact update and 1.5 ms per lookup edit against a 200,000-row source joined to
2,000 customers.
A change that cannot be reasoned about that way, a new filter, a regrouping, a
derivation using unnest or where, or a lookup row arriving for
rows an inner join had dropped, falls back to a full derivation, which is correct but
costs the full linear pass. Narrow with
follow: 'filtered' so the derivation reads what the user is looking at rather than
the whole table.
What a change firing promises
Anything maintaining state from rows:changed (a derived grid, a chart, your
own cache) needs to know whether a firing names the rows that moved or merely says that
something did. One rows.apply announces itself more than once: the source reports
how many rows moved, the row model reports which ones, and the grid reports that a change
happened. Acting on all three does the work three times over.
| Field | Meaning |
|---|---|
| identified: true | added, updated and removed are arrays naming exactly the rows that moved. Safe to patch from. |
| companion: true | A second announcement of a change already reported with identity, or one made before the grid's own view caught up. Ignore it. |
| neither | A real change whose extent cannot be named: rows replaced wholesale, or a row moved, where what changed is the order. Re-read. |
The default is the safe one. A firing that carries neither flag is treated as a change of unknown extent, so a listener re-reads rather than assuming nothing moved. Read the flags rather than the shape of the payload: a firing that reports a row move carries counts, because no row changed value, and only the flags distinguish that case from a duplicate announcement.
Confidence intervals: how much to trust the figure
Every other statistic here describes the data you have. An interval describes how well that data pins down the figure you actually care about, and it is the one thing a descriptive tool can honestly say about the world beyond its rows.
The line this draws. Lattice quantifies uncertainty; it does not adjudicate hypotheses. There are no p-values, no significance tests and no verdicts, and reading two non-overlapping intervals as a significance test is a mistake often enough to be worth not encouraging. An interval says "the mean is 42, and the sample pins that down to between 39 and 45". It does not say 42 differs from 40.
| Call | Returns | Description |
|---|---|---|
| statistics.interval(colId) | ConfidenceInterval | The interval for a column's mean, using the t distribution rather than the normal: below about thirty readings the normal interval is noticeably too narrow, and at five it understates the width by roughly a sixth. |
| statistics.keyOf(data) | string | null | The key a row's data resolves to, without needing the row. What a caller holding raw data uses to reach the grid's view of it. |
| statistics.maintenance | Record<string, string> | Which reductions can be maintained against a change and which must rescan: sum and avg exactly, min and max only away from the extreme, median and the rest never. Ask before putting one in a footer over a million rows on a live feed: the difference is a totals row that costs nothing per tick and one that costs a full pass. |
| statistics.intervalOf(values) | ConfidenceInterval | The column-free form, for readings that are not a column, the rows behind one bar, a subgroup, a hand-assembled sample. One t-quantile serves the chart's whiskers and the panel's bounds alike. |
| statistics.interval(colId, { kind: 'proportion' }) | ProportionInterval | A Wilson score interval for a rate. where decides which rows count as successes; truthiness by default. |
| statistics.capability(colId).ruleSet | string | Which rule set produced violations. Named in the result because the two number their rules differently: “rule 3” means a trend under Nelson and four-of-five-past-one-sigma under Western Electric. |
| statistics.capability(colId).interval | CapabilityInterval | An interval for Cpk, by Bissell's approximation, and intervalPp for Ppk. |
| slopeInterval(fit) | object | An interval for a regression slope, from the standard error regression already reports. |
Every interval carries its level. The result's confidence field
says what it was computed at, so a figure copied out of one cannot lose the thing that makes it
readable. An interval without its level is not a smaller claim, it is an unreadable one.
A proportion is Wilson, not Wald. The textbook
p ± z√(p(1−p)/n) fails exactly where a rate is most
interesting: near zero it reaches below zero, and at no observed successes it
collapses to the single point zero: claiming perfect certainty from the least
informative sample there is. The Wilson interval stays inside 0 to 1 and stays sensible at the
extremes, so "none of forty failed" correctly reads as "the failure rate is under 9%" rather
than "the failure rate is zero".
Report the capability interval. Its absence is the commonest way a capability study overstates itself. A Cpk of 1.35 measured on thirty parts has a lower bound below 1.0, so a process that has "passed" a 1.33 requirement on thirty parts has demonstrated very little. The point estimate alone does not say that; the interval does.
It follows the filters. Like every statistic here, an interval reads the rows the filters left, so it narrows as the user narrows the grid. That is the correct behaviour and worth knowing: it describes the filtered population, not the whole table.
Reading an SPC chart
The figures are only half of it. A capability claim is made in a picture, and these are the two the discipline expects.
| Type | Shows |
|---|---|
| control | The readings in order, with the centre line and control limits the process itself sets, the tolerance the customer set, and every rule break marked and numbered. |
| capability | The readings as a histogram with the tolerance drawn across them, and a fitted normal curve for each of the two spreads: short-term and overall. |
| movingRange | The lower half of an I-MR pair: the gap between consecutive readings, against its own limits. Only the upper limit signals, because a range cannot be negative. |
The lines are named, on opposite edges. CL, UCL and
LCL at the right; LSL, USL and Target at the
left. Control limits are what the process does; specification limits are what the customer
asked for, and reading one as the other is the classic misreading of a control chart. A capable
process puts its control limits just inside its tolerance, so the two families sit close
together, which is exactly when they need telling apart, and why they are named at
opposite ends rather than left to collide.
Rule breaks carry their number. With Western Electric's four rules a marked
point was readable on its own; with Nelson's eight it is not. A spike (rule 1) is a bad part; a
six-point trend (rule 3) is tool wear or a drifting sensor. They call for different responses,
and a chart that marks both the same way has told you the less useful half of what it knows.
Set the rule set with rules: 'nelson' on the chart, as on
statistics.capability.
The capability report draws two curves, not one. Cp and Cpk are computed from short-term variation, Pp and Ppk from overall. When a process has drifted the two differ, and the four indices say so only as numbers a reader has to know how to compare. Drawn, the gap is the finding: a narrow solid curve inside a wide dashed one is a capable process that has been allowed to wander, a scheduling problem, not a machine problem.
What this covers, and what it does not. These are individuals charts: one reading per point, with short-term variation estimated from the moving range. That is the right instrument when readings arrive one at a time, a sensor, a test rig, a single-piece flow.
It is not the right instrument for subgrouped data, and the difference is not cosmetic. If you measure five parts an hour, the correct chart is X̄-R: its limits come from within-subgroup variation and are roughly √n tighter, which is what makes a shift in the process centre visible. Run an individuals chart over the same readings and the limits are computed from differences that mix within- and between-subgroup variation; they come out around twice as wide, and a two-sigma shift that X̄-R flags a dozen times over reads as scattered noise. Lattice does not ship X̄-R, X̄-S, or the attribute charts (p, np, c, u), and an individuals chart should not be substituted for them.
Pair the two control charts. An individuals chart asks whether the process has
moved; a movingRange chart asks whether it has become less
repeatable. A process can fail either without failing the other: it can drift
while its point-to-point variation holds steady, and it can hold its average while shaking
itself apart. The second is close to invisible on the individuals chart alone, because a wider
spread pulls that chart's own limits wider with it: it rescales to accommodate the very thing
that has gone wrong. Drawn one above the other, they are the standard I-MR pair.
Bind it to the readings, not to a summary of them. A whisker is computed from
the values the chart can see behind each mark. Bound to a grid whose rows are already one per
mark, a summary or a derived panel, the chart sees a single value per category and there is no
spread to draw: the readings that produced each average are upstream and no longer reachable.
Bind the chart to the rows the summary was computed from, or carry a margin yourself and use
error: { of: 'margin' }. A chart asked for whiskers it cannot compute says so once
rather than drawing nothing in silence.
Uncertainty on a chart. error: true draws a whisker on each mark,
computed from the readings behind it. Four bars side by side invite a comparison the numbers
alone cannot support, a five per cent gap between two categories of eight readings is
noise, and between two of eight hundred it is the finding. The whisker is what tells them
apart, and its absence is why bar charts are so often over-read. A mark with a single reading
gets none, because one reading has no spread and a zero-height whisker would claim certainty
rather than admit ignorance.
Fitted lines. fit: true draws a least-squares line through a
scatter with its R² beside it; fit: 'line' draws the line alone. A cloud of points
invites a reader to draw the line themselves, and people are consistently poor at it,
the eye is pulled by the extremes, which is exactly what least squares is not.
The interval travels with the index. The statistics panel shows the bounds
under Cpk and Ppk, and createStat takes an interval function that puts
them under the value. A tile is where a figure is read fastest and questioned least, which
makes it the place an interval earns its keep rather than the place it is least needed.
Pushdown adapters: one query, many engines
A remote source already receives a structured request: range, sort, filters, quick text, grouping, pivoting and totals. A pushdown adapter turns that request into whatever an engine speaks, so connecting a new back end is a translation layer rather than a new source.
import { createPushdownSource, odataAdapter } from '@toclocoinc/lattice-grid';
const source = createPushdownSource({
adapter: odataAdapter({ url: 'https://api.example.com/Orders' }),
compute,
});
createGrid(host, { source, columns: [...] });
An adapter never carries an engine. Each one takes what it needs as a
parameter: restAdapter takes a fetch and bundles no HTTP library,
dfqlAdapter takes a token, and duckdbAdapter takes a connection you
have already made. So a grid can drive a full analytical engine without this package carrying
one, and installing Lattice never installs anything else.
An adapter declares what it can answer. No engine speaks the whole query. OData takes a condition tree but only some operators; a single-term API takes one field and one value; a hand-written endpoint may take nothing but a page number. The adapter states its capabilities, the SDK divides the request accordingly, and the grid finishes whatever is left.
| Capability | Values | Meaning |
|---|---|---|
| filter | false | 'term' | 'flat' | 'tree' | Nothing, a single field and term, a flat conjunction, or a full condition tree. |
| operators | string[] | Which comparisons the engine understands. A condition using anything else stays with the grid. |
| sort | false | 'single' | 'multi' | How many columns it can order by. |
| quick | boolean | Whether free-text search across columns can be pushed. |
| range | boolean | Whether it can return a window rather than the whole result. |
| total | boolean | Whether it can report how many rows match. |
Anything left over means the whole result is fetched. Filtering a window
of rows in the browser is not a slower way to get the right answer, it is a fast way to get a
wrong one: the rows that belong on the first page may be on the ninth, and the count is whatever
the engine happened to return. So when the grid has work left to do it asks the engine for the
complete result, applies the remainder, and pages from what it holds. It says so once, naming
the part that could not be pushed, because the fix is usually a wider adapter rather than a
bigger machine. source.lastPlan() reports the division for any request.
A conjunction splits; a disjunction does not. An and group narrows
with each condition, so the engine can apply the conditions it understands and the grid narrows
what comes back. An or group widens with each branch, so pushing only the supported
branches returns fewer rows than the filter allows, and the grid cannot recover rows that were
never fetched. A disjunction the engine cannot fully answer therefore stays with the grid whole.
The same asymmetry governs faceting.
A sort is pushed whole or not at all. Ordering by the first column and fixing the rest in the browser needs every row anyway, so a partial sort buys nothing and returns rows in an order that is wrong until the grid corrects it.
| Adapter | For | Notes |
|---|---|---|
| odataAdapter | Any OData v4 endpoint | Writes $filter, $orderby, $top, $skip and $count. System options keep their $ unencoded, which several servers require. |
| restAdapter | The API you already have | Parameter names are yours to choose. Paging and sorting are assumed; filtering is assumed absent until you declare operators, because an adapter that claims to filter when the endpoint ignores it returns the wrong rows silently. |
| duckdbAdapter | A DuckDB connection | Writes SQL and takes the whole query: filter tree, multi-column sort, paging and grouping - a grouped grid is answered by GROUP BY, one level at a time, with the group counts, the subtotals, the matching count and the grand total all computed in the engine. from is any FROM expression, so read_parquet('s3://bucket/*.parquet') is as valid as a table name. The engine is yours to create and install; this imports nothing, so the bundle is unchanged whether you use it or not. |
| dfqlAdapter | DemandFlow entities | Speaks POST /v1/query. Sends the entity, the key attribute and the prefix to match, a field projection and one field-and-term filter, matched as a case-insensitive substring. It cannot sort or page, so the grid does both, and every request carries a countOnly line because limit caps rows scanned rather than matched: a filtered query returns an arbitrary subset, and the count is the only thing that reveals it. |
| graphqlAdapter | Any GraphQL endpoint | Configured, not zero-config: GraphQL has no fixed query semantics, so you pass buildQuery to turn the plan into a { query, variables } operation and parseResponse to read data back into rows and total. Defaults cover an offset/limit list with totalCount and a Relay cursor connection (first/after with pageInfo). The default pushes only the window and the total; declare operators or capabilities for filter/sort only alongside a buildQuery that emits them. A cursor connection is forward-only, so a deep window costs round trips proportional to its offset. |
What each adapter takes
Every adapter is a function of one options object. The tables below list what each accepts, the type, the default where it is not obvious, and what it means. The defaults are the load-bearing part: an adapter is designed to work when handed almost nothing, so most of what you can set is about telling it what your endpoint cannot do rather than switching features on.
odataAdapter
| Option | Type | Default | Meaning |
|---|---|---|---|
| url | string | - | The entity-set endpoint, e.g. https://api.example.com/Orders. Required. |
| headers | Record<string, string> | {} | Sent on every request, merged over Accept: application/json. This is where a fixed bearer token or an API key goes. See authenticating. |
| fetch | typeof fetch | the global fetch | Your own fetch, for a token that expires, a proxy, or a non-browser runtime. The adapter bundles no HTTP client. See authenticating. |
| count | boolean | true | Whether to ask for $count=true and read @odata.count. On by default because the grid sizes its scrollbar from the total; set false for a server that does not support it, or to spare a server the count for a grid that never shows one. With it off the adapter reports no total and the grid scrolls open-ended - it does not substitute the page length, which before 1.51 told the grid the entity set was exactly one page long. The count travels inline in the same request, so there is nothing to split out: suppression is the only lever OData offers. |
| search | boolean | false | Whether the server implements $search. Off by default, so quick-filter text stays with the grid until you confirm the endpoint honours it; true pushes it as $search. |
| edit | boolean | false | Opt into write-back. Off keeps the source read-only; true advertises mutate: { update: true, delete: true, append: true, returning: 'row' }, so a committed cell edit is persisted with PATCH, a row delete with DELETE /EntitySet(key), and an add-row with POST /EntitySet reading the created entity back for its server key. |
| key | string | the row key | The key property every write addresses a row by in its entity-key URL segment, e.g. /Orders(<key>), and that an add-row is rekeyed to from the created entity. Write-back only. |
restAdapter
The parameter names are yours, and the defaults are not zero. Paging and
sorting are assumed present; filtering is assumed absent until you declare
operators, because an adapter that claims to filter when the endpoint ignores it
returns the wrong rows silently. The query-string names default to
offset, limit, sort, order,
filter and q (search); params overrides any of them.
| Option | Type | Default | Meaning |
|---|---|---|---|
| url | string | - | The endpoint, e.g. /api/orders. Required. |
| headers | Record<string, string> | {} | Sent on every request, merged over Accept: application/json. Where a fixed token or key goes. See authenticating. |
| fetch | typeof fetch | the global fetch | Your own fetch, for an expiring token, a proxy or a non-browser runtime. See authenticating. |
| params | Partial<Record<'offset'|'limit'|'sort'|'order'|'filter'|'search', string>> | { offset:'offset', limit:'limit', sort:'sort', order:'order', filter:'filter', search:'q' } | Renames the query-string keys to whatever your endpoint already reads. Only the keys you name change; the rest keep the defaults above. |
| capabilities | PushdownCapabilities | { range:true, total:true, sort:'multi', filter:false, quick:false } | What the endpoint can answer, merged over the defaults. Declaring operators is the usual way to turn filtering on; reach for this to switch off paging or sorting an endpoint cannot do. |
| operators | string[] | - (filtering off) | The comparisons the endpoint genuinely applies, e.g. ['eq','gt','lt','contains']. Setting it turns filtering on as a tree; a condition using any other operator stays with the grid. |
| encodeFilter | (filters: object) => string | JSON.stringify | How the pushed condition tree becomes the filter parameter's value. Override it to emit whatever query language your service parses instead of JSON. |
| rows | (body: unknown) => unknown[] | body itself if an array, else body.rows then body.data | Pulls the row array out of the response body, for an envelope that nests it somewhere else. |
| total | (body: unknown, rows: unknown[]) => number | body.total then body.count, else the page length | Reads the count of all matching rows, not the page. The grid sizes its scrollbar from it, so a page-sized total makes a large result look like one page. |
| edit | boolean | false | Opt into write-back. Off keeps the source read-only; true advertises mutate: { update: true, delete: true, append: true, returning }, so a committed cell edit is persisted with PATCH, a row delete with DELETE, and an add-row with POST to the collection URL. |
| returning | 'row' | 'key' | 'none' Returning | none | The reconcile contract for a successful write. none is last-write-wins - the optimistic value stands; row reads the server's authoritative row (via writeRow) back before confirm; key reads only the server-assigned key. An add-row needs row or key so the temp row can be rekeyed. |
| keyField | string | id | The property an add-row response carries the server-assigned key in, read back (through writeRow) to rekey the optimistic row. Write-back only. |
| encodeMutation | (op: MutationOp) => { method: string, url: string, headers?: object, body?: unknown } | the default verb map | Full control of a mutation's HTTP shape, overriding the default method, URL and body. Supersedes writeUrlFor. |
| writeUrlFor | (op: MutationOp) => string | ${url}/${key} | The endpoint a single mutation targets, when the default per-row URL is not what the service uses. Addresses an existing row; an add-row POSTs to the collection url instead. Ignored when encodeMutation is supplied. |
| writeRow | (body: unknown) => unknown | the entity, or body.row/body.data | Pulls the authoritative row out of a write response when returning: 'row', and the created row an add-row reads its key from. |
Persisting a cell edit. With edit: true the adapter advertises
mutate, so a committed cell edit is sent as an HTTP request. REST has no universal
write convention, so the request is yours to shape: writeUrlFor names the per-row
endpoint, encodeMutation takes full control of method and body, and
writeRow reads the authoritative row back when returning: 'row'.
const { restAdapter } = await import('../packages/core/src/index.js');
// The per-row endpoint a mutation targets.
const writeUrlFor = (op) => `/api/orders/${op.key}`;
let sentMethod;
const adapter = restAdapter({
url: '/api/orders',
edit: true, // opt into write-back (advertises mutate.update / delete)
returning: 'row', // reconcile to the server's authoritative row
writeUrlFor,
// Full control of the request; supersedes the default envelope.
encodeMutation: (op) => ({
method: 'PATCH',
url: writeUrlFor(op),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(op.patch),
}),
// Pull the authoritative row out of this service's envelope.
writeRow: (body) => body.record,
fetch: async (url, init) => {
sentMethod = init.method;
return { ok: true, status: 200, json: async () => ({ record: { id: '42', status: 'shipped' } }) };
},
});
const result = await adapter.mutate({ kind: 'update', key: '42', patch: { status: 'shipped' } });
return result.rows[0].status; // 'shipped', read back from the server
duckdbAdapter
| Option | Type | Default | Meaning |
|---|---|---|---|
| connection | object | - | A live connection exposing query, and ideally prepare. Required. A connection without prepare is used only for unfiltered queries, because interpolating a user's filter into SQL is worse than not filtering. |
| from | string | - | A table, a view, or any FROM expression. Required. read_parquet('s3://bucket/*.parquet') is as valid as a table name. |
| fields | string[] | everything (SELECT *) | The columns to select. Name them to narrow the projection when the grid shows a subset of a wide table. |
| count | boolean | true | Whether to count the matching set at all. On by default: the total comes from a separate count(*) carrying the same WHERE, dispatched alongside the page query - see how the total is counted. Set false for a grid that never shows a count; then no count statement is issued, capabilities.total is false, and the result carries no total, so the grid scrolls open-ended rather than being told the page length is the whole set. That is a trade: with no total the scrollbar is open-ended and grid.scroll.toRow(n) cannot reach a row past the discovered end. See how the total is counted. |
| writable | boolean | false | Allow write-back against a plain writable table. Off keeps the source read-only, so a from that is a view or an expression can never be mutated by accident. Enables update, delete and append. |
| keyField | string | id | The key column an update and a delete target in their WHERE, and that an add-row is rekeyed by. Write-back is refused unless this names a real column, because an UPDATE/DELETE without a unique key could touch more than one row. |
| returning | 'row' | 'none' | row | The reconcile contract for a successful write. row appends RETURNING * and reconciles server truth (computed columns, triggers); none keeps the optimistic value. An add-row always RETURNINGs at least the key column regardless, since it needs that key to rekey the temp row. |
How the total is counted, and what it costs. The grid needs the size of the
matching set to size its scrollbar. Until 1.51 every page query carried
count(*) OVER () AS "__lattice_total", which looks free: the window is evaluated
before LIMIT, so one round trip returns both the window and the size of the set it
was cut from. Against a local table it is free. Against a remote Parquet it is the most
expensive thing the adapter does - a window function has to see every matching row of the
projected columns, so whatever row-group pruning or range reads DuckDB and the file
host might otherwise manage between them (see whether a Parquet
file streams or downloads whole) cannot help, and the whole file
crosses the wire to produce one page. Measured in Chrome on duckdb-eh.wasm against
a 10,000,000-row Parquet of 162,386,227 bytes - 162.4 MB decimal,
154.9 MiB binary, and every transfer figure on this page is decimal MB so that it can be
compared with it directly - on an origin counting bytes actually served, first paint pulled
162.5 MB. Slightly more than the file, because the reads overlap.
The total is now its own statement - SELECT count(*) FROM <from> WHERE …,
the same predicate through the same builder with the same typed casts and the same bound
values, and no ORDER BY or LIMIT. Read it with
adapter.countSqlFor(query), the counting counterpart of
adapter.sqlFor(query); it returns null when count is
false. The two statements are dispatched in the same tick - both started before either is awaited - so there is no browser round trip between them.
That does not make the count free on the clock, and on DuckDB-Wasm it often is not. One DuckDB-Wasm connection funnels its statements through a single worker, so the engine still runs the two in sequence; what dispatching together buys there is that the second is already queued the instant the first finishes. Measured on the file below: an unfiltered first paint takes 566 ms with the count and 558 ms without, so the count costs about 8 ms - genuinely hidden. A filtered query takes 2122 ms with the count and 573 ms without, so there the count costs about 1550 ms and is not hidden at all. (It is still faster than the 3064 ms the old window function took for the same query.) A server-side DuckDB with a thread pool runs the two at once and the distinction goes away.
Cheap, not free - and on a filtered query, not even cheap. An
unfiltered count(*) over Parquet is answered from the file's footer
metadata and reads no data at all: measured against the 162.4 MB file above, first paint
costs the same 5.71 MB with the count on as with count: false - the count's
own share is 0.00 MB. A filtered count still has to evaluate the
predicate. It reads the predicate columns rather than the whole projection, and row-group
statistics can prune entire groups (a predicate no row group can satisfy is answered from
metadata alone - country = 'ZZ' against this file costs 0.12 MB, count and
all). But a predicate whose columns are spread across every row group has to read them all:
country = 'GB' AND risk_score > 70 costs 45.2 MB with the count and
3.2 MB without, so 41.9 MB of it is the count. Against the
191.2 MB the old window function cost, that is still a 4× saving - and if your grid never
shows a count, count: false makes the same query a 59× one.
One case where 1.51 transfers more, not less: a session that eventually reads the whole
table anyway. Every individual query above is cheaper than or equal to its 1.50.0
counterpart, but a whole session need not be. 1.50.0 dragged the entire file down on
first paint in a handful of large sequential reads, after which everything was cache-warm and
every later query cost nothing. 1.51 reads lazily, and lazy range reads over a big Parquet
overlap where one eager read did not - so bytes already paid for can be paid for again.
Measured over one browser session doing first paint, a selective filter, a full-table
ORDER BY, a deep page and two more filters, with the counter reset between each:
1.50.0 transferred 162.5 MB in total and 1.51 transferred
209.8 MB. The user who never sorts the whole table pays 46.7 MB
instead of 162.5 MB and sees a first paint in 566 ms instead of 5808 ms; the user
who does sort the whole table has to read the whole file either way, and now pays some of it
twice. A full ORDER BY over an unindexed column with SELECT * is
unchanged at 162.5 MB before and after - this card neither helps nor hurts it.
Turning the count off changes what the grid knows, on purpose. With
count: false the adapter reports no total, and the grid does what it already does
for any source of unknown length: it scrolls open-ended and discovers the end when a short page
arrives. It does not substitute the page length for the total - a page presented as
the whole is a wrong number where a right one goes, and every “showing X of Y”,
scrollbar and row count would be wrong with nothing said.
So count: false is a trade, not a free win, and here is the part you will notice
first: the grid can only scroll as far as it has discovered. The scrollbar is
open-ended rather than proportional, a “showing X of Y” readout has no Y, and
grid.scroll.toRow(n) cannot jump to a row beyond the discovered end - asking for
row 900 of a not-yet-discovered million lands at the furthest row known so far, and reaching the
real row 900 means paging to it. Turn the count off for a grid whose users scroll; leave it on
for one whose users jump.
Whether a Parquet file streams or downloads whole is DuckDB's and the file host's
doing, not the grid's. duckdbAdapter only writes SQL; it never opens a
file, so it has no say in whether read_parquet(...) reads the whole thing or only
the row groups a query needs. Measured against a 1.5 MB Parquet on GitHub Pages: DuckDB-Wasm 1.32.0's default HTTP path issued 0 Range requests and
read 100% of the file, for every query including a single-value chip filter. Running
LOAD httpfs; on the connection before the first read_parquet(...)
call changed that to 25 Range requests and 30% of the file for the same query.
(DuckDB-Wasm 1.29.0 read 119% of the file on the same test - its ranges overlapped - so the
exact figures are a version's, not a promise.) A file registered with
db.registerFileBuffer(...) is always read whole, whatever version is loaded: a
buffer has already been downloaded in full before DuckDB ever sees it. And the file host has to
cooperate: it must answer HEAD, advertise Accept-Ranges: bytes and
answer a range request with 206 Partial Content and a Content-Range
header - GitHub Pages does all three - and, cross-origin, its CORS policy must expose
Content-Range and Content-Length or the browser cannot read them back.
Any of that missing and DuckDB falls back to reading the file whole, silently.
Typed binding for timestamp and date columns. A prepared statement binds a
filter value with the value's own type, not the column's: the grid sends an instant as an
ISO-8601 string, the client binds it as VARCHAR, and DuckDB refuses
"ts" >= ? against a TIMESTAMP column (Binder Error: Cannot
compare values of type TIMESTAMP and type VARCHAR). The adapter therefore types the
placeholder: a comparison or IN member against a TIMESTAMP,
TIMESTAMP WITH TIME ZONE, DATE, TIME or
TIMESTAMP_S/_MS/_NS column is written CAST(? AS <that type>),
and the value is still bound, never interpolated. The column's type comes from the engine - one
DESCRIBE SELECT * FROM <from> on the first query, cached for the adapter's
life and exposed as adapter.describe() - so an untyped grid column over a
timestamp is covered. When the schema does not name the column (a DESCRIBE that
failed, said once), the grid column's declared type on the condition is the fallback:
timestamp/datetime cast to TIMESTAMP,
date/dateString to DATE, time to
TIME. The engine's type wins when both are known. A Date or an
epoch-milliseconds number is bound as its ISO instant, because DuckDB has no cast from a number
to a timestamp. The same schema fixes blank: = '' is a conversion
error on any non-text column, so a typed column's blank test is IS NULL alone.
Text and numeric comparisons (VARCHAR, BIGINT, DOUBLE,
DECIMAL, HUGEINT) are written exactly as before, with no cast.
Time zones, honestly. The cast is the engine's, so its zone rules apply. Against
a naive TIMESTAMP column the wall-clock digits of the bound string are compared;
an instant ending in Z - which is what the grid's own date filter sends - therefore
matches a column that stores UTC wall time, the usual convention for log and event data. A
non-zero offset in the string is engine-version dependent (DuckDB 1.1 converts it to UTC, 1.5
keeps the digits as written), so send Z instants, not local offsets. Against a
TIMESTAMP WITH TIME ZONE column an offset or Z is honoured exactly,
and a string with no zone is interpreted in the engine's session
TimeZone (UTC in DuckDB-Wasm unless the ICU extension is loaded and the setting
changed). Against a DATE column an instant is truncated to its UTC day.
duckdbAdapter: grouping runs in the engine, one level at a time
Grouping a hundred million Parquet rows used to mean fetching a hundred million Parquet rows:
the adapter declared no group capability, so the push router never sent it a
grouped request and the grid grouped whatever it held. duckdbAdapter now declares
group: true and answers the grouped view with GROUP BY - one
statement per grid level, paged like any other window.
Nothing is configured. Group a grid over a DuckDB source and the grouping is pushed:
const grid = createGrid(el, {
columns: [
{ id: 'region' },
{ id: 'tier' },
{ id: 'amount', type: 'number', total: 'sum' },
],
groupBy: ['region', 'tier'],
source: createPushdownSource({
adapter: duckdbAdapter({ connection, from: "read_parquet('s3://bucket/sales/*.parquet')" }),
}),
});
The root level is one statement - SELECT "region", count(*), sum("amount") … GROUP BY
"region" ORDER BY "region" ASC NULLS LAST LIMIT ? OFFSET ? - so the group rows on screen
cost a grouped scan and no leaf crosses the wire. Expanding a group narrows the next level by
its parent's key; expanding the deepest one runs the ordinary row query with the same predicate
ANDed on, so the leaves arrive paged and sorted exactly as they would without grouping. Every
identifier goes through the same validation the row query uses and every value is bound, so the
grouped path is no more exposed than the read path.
Subtotals come from the engine, and a statistic it cannot express shows nothing rather
than something. Each totalled column contributes one aggregate expression, taken from
the same verified pushdown map the statistics panel uses (see
pushing statistics down) - sum,
avg, min, max, count, the quantiles, and the
rest. A column whose total is a host function, a two-column statistic such as
weightedAvg (a grouped request carries no weight column), and the one genuine
fallback weightedQuantile are not sent, are named in
source.lastPlan().aggregates.client with the reason, and warn once. The group row
then carries no value for that column. That is deliberate: the leaves of an unexpanded group
are not in the browser, so the only alternative to the engine's figure is a figure computed
over something that is not the group.
The counts a grouped grid shows are the engine's. Under grouping the display
count is group headers plus whatever is expanded, which is how a grid showing three rows under
one group once reported “4 of 3”. The root level's fetch therefore also asks for
count(*) over the matching set and the grand total over it, in one extra
statement, and an unfiltered count(*) once per adapter (on a Parquet file that is
a footer read, not a scan). rows.matchCount() and rows.totalCount()
read them, and grandTotalRow: 'bottom' draws its row from them.
Group order, and the collation decision. Group rows are ordered by their own
key, ascending unless the sort names that column, which is exactly what the grid does to
sibling group rows in memory - a sort naming some other column does not reorder groups in
either. Absent keys sort last ascending and first descending, written as explicit
NULLS LAST/NULLS FIRST rather than left to the connection's
default_null_order. No COLLATE is written and the ICU
extension is not loaded: the grid compares group keys with < on the
JavaScript string (UTF-16 code-unit order) and DuckDB's default VARCHAR ordering
is UTF-8 byte order, and the two agree for every character in the Basic Multilingual Plane.
They part only for supplementary-plane characters (emoji, CJK extension B and above) compared
against U+E000-U+FFFF. An ICU collation would disagree with the grid everywhere instead, so
binary ordering is the pin. Practically: 'North' sorts before 'north'
in both.
When grouping is not pushed, it says so. Grouping is all or nothing - group rows counted over the wrong set are wrong rows, not slow ones - so the whole level is
refused if anything else in the query stayed behind: a filter that did not fully push, a sort
the engine could not take, a quick search, a host where predicate, a grouping key
that is not a plain column, or fullDataset (which holds the whole set and groups it
client-side on purpose). source.lastPlan() then reports
grouped: false, 'group' in unpushed and a
groupReason sentence, and a one-time warning names it.
One known difference from a memory grid. A column holding both
NULL and the empty string produces two groups in the engine and one in a memory
grid, because the grid keys its group nodes on a display path where an absent value and an
empty string are both ''. The engine's answer is the right one; the two are
otherwise identical group for group, count for count and subtotal for subtotal, which the
parity suite asserts at every level.
dfqlAdapter
| Option | Type | Default | Meaning |
|---|---|---|---|
| entity | string | - | The DemandFlow entity to query. Required. |
| token | string | - | A personal access token, sent as the bearer credential. Required. Never commit one; read it from configuration at runtime. |
| url | string | https://rest.demandflow.com | The API base, for a non-default region or a self-hosted deployment. |
| comboKey | 'comboKey' | 'comboKey2' | 'comboKey3' | comboKey | The name of the key attribute to match on. comboKey is the standard hierarchy. |
| query | string | SUB | The prefix matched against the key attribute. SUB alone means every record of the entity in the tenant. |
| load | string[] | everything | Fields to project, which saves bandwidth but not query cost. |
| limit | number | server default | Caps rows scanned, not matched - which is why every request also sends countOnly to reveal the true match count. |
| headers | Record<string, string> | {} | Extra headers merged over the bearer token, for a gateway that needs its own. |
| fetch | typeof fetch | the global fetch | Your own fetch, for a proxy or a non-browser runtime. |
| writeUrl | string | the default write endpoint | Where record mutations are POSTed, when the deployment's write endpoint differs from the default. Write-back persists update, delete and add-row. |
| encodeCreate | (row: unknown) => Record<string, unknown> | the row's own fields | Maps a new grid row to the DemandFlow fields an append needs - its required entity/level/comboKey - since the structural append only knows the row's own fields. |
graphqlAdapter
GraphQL has no fixed query semantics, so this adapter is configured. A filter,
a sort and pagination are whatever the schema defines, so the two hooks are yours to write:
buildQuery turns the pushed plan into the { query, variables } body the
endpoint is POSTed, and parseResponse reads its data back into
{ rows, total }. The defaults cover an offset/limit list with a
totalCount and a Relay cursor connection; either is replaced whole by passing the
hook. The default query pushes only the window and asks for the total, which is why the default
capabilities are range and total and nothing more - declare
operators or capabilities for filter or sort only alongside a
buildQuery that genuinely emits them, or the grid returns the wrong rows silently.
If your schema does not expose totalCount. The adapter reports no
total, rather than the number of rows in the page, and the grid scrolls open-ended; the
endpoint is named once in a warning so the silence is not mistaken for a working count. Before
1.51 the page length was reported as the total, and that was not only a wrong number on screen - it truncated results. When the grid has residual work to finish it asks for the
whole result and the adapter walks offset/limit to get it,
stopping when it has as many rows as the total says exist. With the total invented from page
one, the walk stopped at page one: a 337-row connection came back as 100 rows, reported as
complete, and any client-side filter or sort then ran over that fraction. The walk now stops on
a short page or an exhausted cursor, so it returns everything and its count is exact. A schema
that does report totalCount was never affected.
| Option | Type | Default | Meaning |
|---|---|---|---|
| url | string | - | The GraphQL endpoint, POSTed a { query, variables } body. Required. |
| headers | Record<string, string> | {} | Sent on every request, merged over Accept and Content-Type: application/json. Where a fixed bearer token or API key goes. See authenticating. |
| fetch | typeof fetch | the global fetch | Your own fetch, for an expiring token, a proxy or a non-browser runtime. The adapter bundles no HTTP client. See authenticating. |
| field | string | items | The root query field the default query selects from, e.g. orders. Ignored when you pass buildQuery. |
| fields | string[] | ['id'] (with a warning) | The field names the default query's selection set requests. Name what your grid shows; a default query that selects nothing useful is surfaced rather than left an empty grid. |
| selection | string | - (uses fields) | A raw selection set for nested fields, e.g. 'id name address { city }', overriding fields. |
| pagination | 'offset' | 'cursor' GraphqlPagination | offset | The default convention: an offset/limit list, or a Relay cursor connection (first/after with pageInfo). A cursor connection is forward-only, so a deep window is paged forward to and costs round trips proportional to its offset. |
| pageSize | number | 1000 | The page size for the two forward walks: pulling the whole result (when residual work forces it) and walking a cursor connection to a window. |
| count | boolean | true | Whether the default query asks for totalCount. A totalCount on a connection is rarely free on the server - it is usually a second COUNT(*) over the same predicate - so set false for a grid that never shows a count: the field is dropped from the selection set, capabilities.total becomes false, and the grid scrolls open-ended. Unlike duckdbAdapter the count is not split into a second operation, because over HTTP that would cost an extra round trip rather than saving one. Ignored when you pass your own buildQuery. |
| vars | Partial<Record<'offset'|'limit'|'first'|'after', string>> | { offset:'offset', limit:'limit', first:'first', after:'after' } | Renames the pagination variables the adapter drives per page, to match the names your schema's arguments use. |
| capabilities | PushdownCapabilities | { range:true, total:true, filter:false, sort:false, quick:false } | What your buildQuery actually pushes, merged over the defaults. Declaring a capability the hook does not honour returns the wrong rows silently, so the default declares only the window and the total. |
| operators | string[] | - (filtering off) | The comparisons your buildQuery emits, e.g. ['eq','gt','contains']. Setting it turns filtering on as a tree; pair it with a buildQuery that translates the condition tree, or the filter is declared but not applied. |
| buildQuery | (request: RemoteRequest) => { query, variables } | the offset or cursor default | Turns the pushed plan - the window, and whatever filter/sort/quick you declared pushable - into the GraphQL operation to POST. This is where your schema's argument names live. |
| parseResponse | (data: object) => { rows, total, pageInfo? } | the offset or cursor default | Reads the operation's data into the row array and the count of all matching rows. For a cursor connection, return pageInfo (hasNextPage, endCursor) so the adapter can walk forward. |
| buildMutation | (op: object) => { query, variables } | - (write-back off) | Turns a mutation into a GraphQL operation. A declared follow-up wired by the write-back wave; capabilities.mutate stays false by declaration until then. |
Authenticating a remote adapter
Two shapes cover almost every endpoint. A fixed credential - an API key or a
long-lived token - goes in headers, which odataAdapter and
restAdapter send on every request. A credential that expires - a short-lived bearer token you refresh - goes in a custom fetch, which is the one
place that can mint a fresh value per request. dfqlAdapter takes its
token directly, and headers for anything a gateway adds on top.
A fixed token in headers. The map is sent on every request, so an
Authorization header authenticates the whole grid. Below, a custom
fetch stands in for the network only so the example can prove the header arrived:
const { odataAdapter } = await import('../packages/core/src/index.js');
let seen;
const adapter = odataAdapter({
url: 'https://api.example.com/Orders',
// A fixed credential authenticates every request.
headers: { Authorization: 'Bearer static-token-123' },
// Only here to capture what the adapter sent; in a browser, omit it.
fetch: async (url, init) => {
seen = init.headers.Authorization;
return { ok: true, json: async () => ({ value: [], '@odata.count': 0 }) };
},
});
await adapter.execute({ range: { start: 0, end: 20 } }, {});
return seen; // the header reached the request
An expiring token in a custom fetch. A token with a lifetime cannot
sit in a fixed map, because the map is read once and the token outlives no request that matters.
A custom fetch is called afresh for every request, so it is where you refresh the
credential and set the header on the outgoing call:
const { restAdapter } = await import('../packages/core/src/index.js');
// Stands in for a token service that hands out a new value each time.
let issued = 0;
const freshToken = async () => `token-${++issued}`;
let lastAuth;
const adapter = restAdapter({
url: '/api/orders',
fetch: async (url, init) => {
// Refreshed per request, then merged over whatever headers the adapter set.
const headers = { ...init.headers, Authorization: `Bearer ${await freshToken()}` };
lastAuth = headers.Authorization;
return { ok: true, json: async () => ({ rows: [], total: 0 }) };
},
});
await adapter.execute({ range: { start: 0, end: 20 } }, {}); // token-1
await adapter.execute({ range: { start: 20, end: 40 } }, {}); // token-2
return lastAuth; // a fresh token on the second request
Wiring it to the API you already have
Most data sits behind a service someone on your team wrote. The adapter below sends four
parameters and expects { rows, total } back. Start by declaring only what the
endpoint genuinely does, and widen it as you teach the endpoint more.
const source = createPushdownSource({
compute,
adapter: restAdapter({
url: '/api/orders',
// Only the comparisons the endpoint really applies. Claiming more here
// returns the wrong rows rather than merely running slowly.
operators: ['eq', 'gt', 'lt', 'contains'],
params: { offset: 'skip', limit: 'take' },
}),
});
The request that reaches your service, and the answer it owes:
| Parameter | Example | Meaning |
|---|---|---|
| skip / take | 40, 20 | The window. Return exactly that slice. |
| sort / order | amount,name / desc,asc | Columns in priority order, and a direction for each. |
| filter | JSON condition tree | Only the conditions your declared operators cover. Everything else the grid keeps. |
| q | free text | Present only when you declare quick: true. |
// Express. FastAPI and ASP.NET differ only in how the query string is read.
app.get('/api/orders', async (req, res) => {
const { skip = 0, take = 100, sort, order, filter } = req.query;
let q = db('orders');
if (filter) q = applyConditions(q, JSON.parse(filter)); // your translation
if (sort) {
sort.split(',').forEach((col, i) => {
q = q.orderBy(col, (order || '').split(',')[i] === 'desc' ? 'desc' : 'asc');
});
}
// The count is of everything matching, not of the page. A grid scrollbar is
// sized from it, so a page-sized total makes the grid look empty below.
const [{ count }] = await q.clone().clearOrder().count({ count: '*' });
const rows = await q.offset(Number(skip)).limit(Number(take));
res.json({ rows, total: Number(count) });
});
The total is the commonest mistake. It is the number of rows matching the filter, not the number returned in this page. The grid sizes its scrollbar from it and requests windows against it, so returning the page length makes a large result look like one page.
A worked example, executed on every build so it cannot go stale
(data-run, PRD C3):
const { capabilitiesOf, splitFilters, resolveMutate } = await import('../packages/core/src/source/pushdown.js');
// An adapter that understands three comparisons and nothing else.
const caps = capabilitiesOf({ filter: 'tree', operators: ['eq', 'gt', 'lt'] });
// Read-only by declaration: an adapter that says nothing about writing
// cannot mutate, and one that opts in resolves to a complete capability.
if (resolveMutate() !== false) throw new Error('a silent adapter must stay read-only');
if (!resolveMutate({ update: true }).update) throw new Error('an opt-in must resolve');
// A conjunction splits: what the engine knows goes to it, the rest stays here.
const { pushed } = splitFilters({
op: 'and',
conditions: [
{ col: 'a', op: 'eq', value: 1 },
{ col: 'b', op: 'gt', value: 2 },
{ col: 'c', op: 'lt', value: 3 },
],
}, caps);
return pushed.conditions.length; // all three are supported
Building an adapter from the parts
createPushdownSource is the whole story for most callers. When an engine needs a
source of its own, the four pieces it is assembled from are exported separately, so a custom
source can plan and finish work the same way rather than reimplementing the split.
| Export | Signature | Description |
|---|---|---|
| capabilitiesOf | (declared?) => Required<PushdownCapabilities> | Resolves what an adapter declared against the defaults, giving a complete set with no absent keys to test for. |
| resolveMutate | (declared?) => false | MutateCapability | Resolves an adapter's mutate declaration against the defaults. Returns false when the adapter cannot mutate, so a source over it stays read-only by declaration and refuses a write loudly rather than dropping it. |
| splitFilters | (filters, caps) => { pushed, residual } | Divides a condition tree into the half the engine takes and the half left over. A conjunction splits; a disjunction that is not fully supported stays whole on the client, because pushing part of an or returns fewer rows than the filter allows and the grid cannot recover what was never fetched. |
| planQuery | (request, caps) => PushdownPlan | Plans one request: the query to send, the work to finish afterwards, whether the whole result is needed, and which parts stayed behind. |
| applyResidual | (rows, residual, compute) => unknown[] | Applies whatever the engine could not, through the grid's own filter and sort kernels rather than a second implementation, so a residual predicate means exactly what the same predicate means anywhere else. |
| NO_CAPABILITIES | Readonly<Required<PushdownCapabilities>> | The set an adapter that declares nothing is treated as having: everything off. Such an adapter still works; the grid simply does all the work. |
Residual work needs the complete result. applyResidual expects
every matching row, not a window. Filtering a window is not a slower route to the right answer,
it is a fast route to a wrong one: the rows that belong on page one may sit on page nine.
planQuery sets needsAll whenever that applies, and
createPushdownSource switches to fetching everything and paging from what it holds.
Whole-dataset statistics: fullDataset
A windowed source computes a total, statistic or group over the loaded window - the
rows on screen - not the whole matching set, unless residual work already forced a whole-result
fetch. “Median revenue” in the footer becomes the median of ~200 rows, wrong and
looking right. fullDataset.enabled makes the whole-result fetch sticky and
explicit: the entire matching set is held client-side once per query and every window,
total and statistic is served from it, so the figures are computed over everything. It reuses
the same whole-result path residual work already takes - needsAll - rather than a
parallel mechanism. It is off by default and strictly opt-in. For a restAdapter,
which cannot compute, it is the only way to get a correct whole-dataset statistic at all.
Memory-guarded, refused loudly. A matching set past maxRows or
maxBytesEstimate is refused - thrown, surfaced as a
source:error with no rows shown - never silently truncated. Presenting a fraction
as the whole is the exact failure whole-dataset pull exists to prevent, so it is never the
failure mode of the fix itself. Narrow the filter or raise the limit.
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | false | Hold the whole matching set client-side and serve every window, total and statistic from it. |
| maxRows | number | 1_000_000 | Refuse (visible source:error) when the matching set is larger. |
| maxBytesEstimate | number | 512 MB | Refuse past this estimated heap cost, sampled from a representative row. |
// An adapter that can page but reports the whole matching count. Without
// fullDataset a stat would see only the window; with it, the whole set.
const { createPushdownSource } = await import('../packages/core/src/source/pushdown.js');
const all = Array.from({ length: 100 }, (_, i) => ({ id: i, amount: i }));
const adapter = {
name: 'demo',
capabilities: { range: true, total: true },
execute: async (query) => {
const start = query.range ? query.range.start : 0;
const end = query.range ? query.range.end : all.length;
return { rows: all.slice(start, end), total: all.length };
},
};
const source = createPushdownSource({ adapter, fullDataset: { enabled: true, maxRows: 1000, maxBytesEstimate: 5_000_000 } });
// Ask for a 10-row window; fullDataset holds all 100, so the mean is the true one.
const block = await source.fetch({ range: { start: 0, end: 10 }, filters: null, sort: [], quick: '' });
const held = block.total; // 100: the whole set is held, not the 10-row window
return all.reduce((s, r) => s + r.amount, 0) / held; // 49.5, the true whole-dataset mean
Refusing a partial result: allowPartialResults
When a query has residual work - a filter, sort or quick search the engine could not push - the source asks the adapter for the whole matching set, applies the residual here, and pages from what it holds. If the adapter instead returns a page of that result (it paged when told not to), the client-side filter or sort runs over the wrong rows: the rows that belong on page one may be in the fraction that was never fetched, so a page is presented as the full filtered set. That is a wrong answer, not a slow one.
By default the source refuses such a shortfall - it throws, and the remote
source surfaces a source:error with no rows shown, rather than filter a fraction
and lie. The fix is to make the adapter follow the engine's own paging before returning, or hold
the data in memory. allowPartialResults: true is the knowing escape hatch: a caller
who accepts the permissive behaviour - an adapter that genuinely cannot page and a result small
enough not to matter, or a diagnostic run - keeps the old warn-once-and-proceed path. It is
off by default, because a silent wrong answer is the one thing the design refuses. It does not
affect the fullDataset memory guard, nor the no-residual short-return warning.
| Key | Type | Default | Description |
|---|---|---|---|
| allowPartialResults | boolean | false | Accept a partial/paged result to a whole-set request that residual work will filter over, keeping the warn-once-and-proceed behaviour instead of refusing. Off by default: the shortfall is thrown. |
// An eq-only engine: the `gt` filter is residual and runs in the browser. The
// adapter reports 40 matching but returns only 1 row - a page shown as the whole.
const { createPushdownSource } = await import('../packages/core/src/source/pushdown.js');
const adapter = {
name: 'shortfall',
capabilities: { filter: 'tree', operators: ['eq'] },
execute: async () => ({ rows: [{ id: 1, a: 5 }], total: 40 }),
};
const req = { range: { start: 0, end: 10 }, filters: { col: 'a', op: 'gt', value: 1 }, sort: [], quick: '' };
// Default: refused. Filtering 1 of 40 rows would return the wrong rows.
const strict = createPushdownSource({ adapter });
let refused = false;
try { await strict.fetch(req); } catch (e) { refused = /returned 1 of 40|refused/.test(e.message); }
// Knowing opt-in: warns once and proceeds with the fraction.
const lax = createPushdownSource({ adapter, allowPartialResults: true });
const block = await lax.fetch(req);
return refused ? `refused; opted in: ${block.rows.length} rows` : 'not refused';
Running a host predicate: whereRowLimit
A where predicate is a host function - whether
this user may see the row, whether you hold a rate for its currency. No engine can evaluate one,
so the only way a pushdown source can honour it is to fetch every matching row and filter
here. That is a real answer, and it is also a windowed grid quietly turning into a whole-dataset
download - the one thing a pushdown source exists to avoid.
So it is gated rather than done on your behalf. Under whereRowLimit (default
50_000, the same anchor as the grid's workerThreshold) the predicate
runs and the counts are whole-dataset counts. At or past it - or when the adapter reports
no row total, since the only way to learn the size from such an adapter is to
fetch the set - the source refuses: the predicate is not applied, the rows
it would exclude stay on screen, and one warning names the adapter, the size, the limit and the
way out. Raise the limit when you want the download.
The { condition } twin is the route that works at any size. It is
pushed to the engine, which narrows the fetch itself, so no limit applies and nothing is held here.
Reach for the limit only when the predicate genuinely cannot be expressed as a condition.
| Key | Type | Default | Description |
|---|---|---|---|
| whereRowLimit | number | 50_000 | The most rows the source will fetch and hold in order to run a twinless where predicate. At or past this many matching rows the predicate is refused and warned about rather than the whole set downloaded. An adapter reporting no row total counts as over the limit. 0 refuses every predicate. |
// Three rows, two of them ana's. The predicate is a host function with no twin,
// so the engine cannot narrow the fetch and the source must hold the set to run it.
const { createPushdownSource } = await import('../packages/core/src/source/pushdown.js');
const all = [{ id: 1, owner: 'ana' }, { id: 2, owner: 'bo' }, { id: 3, owner: 'ana' }];
const adapter = {
name: 'demo',
capabilities: { range: true, total: true },
execute: async (q) => ({ rows: q.range ? all.slice(q.range.start, q.range.end) : all, total: all.length }),
};
const where = { active: true, names: ['mine'], version: 1, passes: (row) => row.owner === 'ana' };
const req = { range: { start: 0, end: 10 }, filters: null, sort: [], quick: '', where };
// Under the limit: the whole matching set is fetched and the predicate runs.
const under = createPushdownSource({ adapter, whereRowLimit: 1000 });
const applied = await under.fetch(req);
// At or past it: refused and warned about, and every row stays on screen.
const over = createPushdownSource({ adapter, whereRowLimit: 2 });
const refused = await over.fetch(req);
return `applied: ${applied.rows.length}, refused: ${refused.rows.length}`;
Pushing statistics down: aggregates
A DuckDB-class engine can compute a median or a standard deviation over the whole matching set
far faster than pulling every row to do it here. The aggregates config decides, at
grid setup, which statistics are computed by the engine and which by the grid. It is a
design-time developer choice - fixed for the life of the grid, never a runtime
toggle, never shown to an end user. Absent, every aggregate is computed client-side, so no
existing grid changes behaviour.
Each statistic is classified IDENTICAL (the engine's result equals the grid's
own kernel, verified against it) or MAY-DIFFER (the engine computes it by a
method that can differ from the grid's definition). The classification drives this
documentation and build-time provenance, not whether a stat is pushed - that is your
choice. Only weightedQuantile is a genuine fallback: the engine
cannot express the grid's midpoint convention, so it is always computed client-side.
| Key | Type | Default | Description |
|---|---|---|---|
| default | 'engine' | 'client' | 'engine-if-identical' AggregateMode | 'client' | engine pushes everything the engine can express (using its method for MAY-DIFFER stats); engine-if-identical pushes only the verified-identical ones and keeps MAY-DIFFER client-side - the recommended setting for a windowed DuckDB source; client computes everything here. |
| overrides | Record<stat, 'engine' | 'client'> | - | Per-stat overrides that win over default. A stat the engine cannot express is always client-side regardless. |
No mixed provenance. An engine number and a client number never appear in one
result set. Aggregates are pushed only when the filter is fully pushed; a residual
filter the engine could not apply forces every aggregate client-side, because an
engine figure computed over a superset beside a client figure over the real set would be
wrong-but-plausible. lastPlan().aggregates reports, per statistic, whether the
engine or the client computed it and the class it was assigned - build-time inspection, not a
per-figure runtime marker.
The classification table below is generated from the single pushdown map
(STAT_PUSHDOWN), so it cannot drift from what the adapter actually emits:
| Statistic | Class | DuckDB expression | Note |
|---|---|---|---|
| sum | IDENTICAL | sum(col) | Pushes to DuckDB with the same result. |
| avg | IDENTICAL | avg(col) | Pushes to DuckDB with the same result. |
| min | IDENTICAL | min(col) | Pushes to DuckDB with the same result. |
| max | IDENTICAL | max(col) | Pushes to DuckDB with the same result. |
| count | IDENTICAL | count(*) | Pushes to DuckDB with the same result. |
| countValues | IDENTICAL | count(col) | Pushes to DuckDB with the same result. |
| range | IDENTICAL | (max(col) - min(col)) | Pushes to DuckDB with the same result. |
| variance | IDENTICAL | var_samp(col) | Pushes to DuckDB with the same result. |
| varianceP | IDENTICAL | var_pop(col) | Pushes to DuckDB with the same result. |
| stddev | IDENTICAL | stddev_samp(col) | Pushes to DuckDB with the same result. |
| stddevP | IDENTICAL | stddev_pop(col) | Pushes to DuckDB with the same result. |
| sumSquares | IDENTICAL | sum(col * col) | Pushes to DuckDB with the same result. |
| median | IDENTICAL | median(col) | Pushes to DuckDB with the same result. |
| p25 | IDENTICAL | quantile_cont(col, 0.25) | Pushes to DuckDB with the same result. |
| p75 | IDENTICAL | quantile_cont(col, 0.75) | Pushes to DuckDB with the same result. |
| p90 | IDENTICAL | quantile_cont(col, 0.9) | Pushes to DuckDB with the same result. |
| p95 | IDENTICAL | quantile_cont(col, 0.95) | Pushes to DuckDB with the same result. |
| p99 | IDENTICAL | quantile_cont(col, 0.99) | Pushes to DuckDB with the same result. |
| iqr | IDENTICAL | (quantile_cont(col, 0.75) - quantile_cont(col, 0.25)) | Pushes to DuckDB with the same result. |
| mad | IDENTICAL | mad(col) | Pushes to DuckDB with the same result. |
| distinct | IDENTICAL | count(DISTINCT col) | Pushes to DuckDB with the same result. |
| skewness | IDENTICAL | skewness(col) | Pushes to DuckDB with the same result. |
| kurtosis | IDENTICAL | kurtosis(col) | Pushes to DuckDB with the same result. |
| geomean | IDENTICAL | exp(avg(ln(col))) | Pushes to DuckDB with the same result. |
| harmean | IDENTICAL | (count(col) / sum(1.0 / col)) | Pushes to DuckDB with the same result. |
| entropy | IDENTICAL | entropy(col) | Pushes to DuckDB with the same result. |
| correlation | IDENTICAL | corr(weight, col) | Pushes to DuckDB with the same result. |
| hhi | IDENTICAL | CASE WHEN count(col)=0 THEN NULL ELSE list_sum(list_transform(map_values(histogram(col)), lambda v: (v::DOUBLE/count(col))*(v::DOUBLE/count(col)))) END | Pushes to DuckDB with the same result. |
| evenness | IDENTICAL | CASE WHEN count(col)=0 THEN NULL WHEN count(DISTINCT col)<2 THEN 1.0 ELSE entropy(col)/log2(count(DISTINCT col)) END | Pushes to DuckDB with the same result. |
| top3Share | IDENTICAL | CASE WHEN count(col)=0 THEN NULL ELSE list_sum(list_slice(list_sort(map_values(histogram(col)),'DESC'),1,3))::DOUBLE/count(col) END | Pushes to DuckDB with the same result. |
| top10Share | IDENTICAL | CASE WHEN count(col)=0 THEN NULL ELSE list_sum(list_slice(list_sort(map_values(histogram(col)),'DESC'),1,10))::DOUBLE/count(col) END | Pushes to DuckDB with the same result. |
| gini | IDENTICAL | CASE WHEN list_min(list(col) FILTER (WHERE isfinite(col)))<0 THEN NULL WHEN len(list(col) FILTER (WHERE isfinite(col)))=0 THEN NULL WHEN list_sum(list(col) FILTER (WHERE isfinite(col)))=0 THEN 0 ELSE 2.0*list_sum(list_transform(list_sort(list(col) FILTER (WHERE isfinite(col))), lambda v, i: i*v))/(len(list(col) FILTER (WHERE isfinite(col)))*list_sum(list(col) FILTER (WHERE isfinite(col))))-(len(list(col) FILTER (WHERE isfinite(col)))+1.0)/len(list(col) FILTER (WHERE isfinite(col))) END | Pushes to DuckDB with the same result. |
| trimmedMean | IDENTICAL | CASE WHEN len(list(col) FILTER (WHERE isfinite(col)))=0 THEN NULL ELSE list_avg(list_slice(list_sort(list(col) FILTER (WHERE isfinite(col))), floor(len(list(col) FILTER (WHERE isfinite(col)))*0.1)::BIGINT+1, len(list(col) FILTER (WHERE isfinite(col)))-floor(len(list(col) FILTER (WHERE isfinite(col)))*0.1)::BIGINT)) END | Pushes to DuckDB with the same result. |
| winsorizedMean | IDENTICAL | CASE WHEN len(list(col) FILTER (WHERE isfinite(col)))=0 THEN NULL ELSE list_avg(list_transform(list_sort(list(col) FILTER (WHERE isfinite(col))), lambda v: least(list_sort(list(col) FILTER (WHERE isfinite(col)))[len(list(col) FILTER (WHERE isfinite(col)))-floor(len(list(col) FILTER (WHERE isfinite(col)))*0.1)::BIGINT], greatest(list_sort(list(col) FILTER (WHERE isfinite(col)))[floor(len(list(col) FILTER (WHERE isfinite(col)))*0.1)::BIGINT+1], v)))) END | Pushes to DuckDB with the same result. |
| robustOutliers | IDENTICAL | (SELECT CASE WHEN len(d.a)=0 THEN NULL WHEN d.mad=0 THEN NULL ELSE len(list_filter(d.a, lambda v: abs(0.6745*(v-d.med)/d.mad)>3.5)) END FROM (SELECT xs AS a, list_median(xs) AS med, list_median(list_transform(xs, lambda w: abs(w-list_median(xs)))) AS mad FROM (SELECT list(col) FILTER (WHERE isfinite(col)) AS xs)) d) | Pushes to DuckDB with the same result. |
| jarqueBera | IDENTICAL | CASE WHEN len(list(col) FILTER (WHERE isfinite(col)))<8 THEN NULL WHEN list_avg(list_transform(list(col) FILTER (WHERE isfinite(col)), lambda v: power(v-list_avg(list(col) FILTER (WHERE isfinite(col))),2)))=0 THEN NULL ELSE (len(list(col) FILTER (WHERE isfinite(col)))/6.0)*(power(list_avg(list_transform(list(col) FILTER (WHERE isfinite(col)), lambda v: power(v-list_avg(list(col) FILTER (WHERE isfinite(col))),3)))/power(list_avg(list_transform(list(col) FILTER (WHERE isfinite(col)), lambda v: power(v-list_avg(list(col) FILTER (WHERE isfinite(col))),2))),1.5),2)+power(list_avg(list_transform(list(col) FILTER (WHERE isfinite(col)), lambda v: power(v-list_avg(list(col) FILTER (WHERE isfinite(col))),4)))/power(list_avg(list_transform(list(col) FILTER (WHERE isfinite(col)), lambda v: power(v-list_avg(list(col) FILTER (WHERE isfinite(col))),2))),2)-3,2)/4.0) END | Pushes to DuckDB with the same result. |
| weightedAvg | IDENTICAL | sum(col*weight) FILTER (WHERE isfinite(col) AND isfinite(weight))/nullif(sum(weight) FILTER (WHERE isfinite(col) AND isfinite(weight)),0) | Pushes to DuckDB with the same result. |
| mode | MAY-DIFFER | mode(col) | DuckDB returns a modal value even for an all-distinct column; the grid returns null. Tie-breaking can also differ, and on a text column an empty string counts as a value. Under pushdown you will see a value. |
| weightedQuantile | FALLBACK | - | No SQL equivalent for the grid's weighted-quantile midpoint convention; always computed client-side (needs a full-dataset pull for a correct figure over a remote source). |
// Push the verified-identical stats to the engine; keep the fallback here.
// STAT_PUSHDOWN is the published map every stat's class and SQL comes from.
const { createPushdownSource, STAT_PUSHDOWN } = await import('../packages/core/src/source/index.js');
void STAT_PUSHDOWN; // the single source of truth for the classification table above
const adapter = {
name: 'demo',
capabilities: { filter: 'tree', operators: ['eq'] },
execute: async () => ({ rows: [], total: 0 }),
// A real duckdbAdapter runs SQL; here we just echo which stats arrived.
executeAggregates: async (query, aggs) => Object.fromEntries(aggs.map((a) => [a.id, 1])),
};
const source = createPushdownSource({ adapter, aggregates: {
default: 'engine-if-identical', // push only the verified-identical stats
overrides: { mode: 'client' }, // but always keep mode's exact definition
} });
const split = await source.aggregate(
{ filters: null, sort: [], range: null },
[{ id: 'a', col: 'revenue', fn: 'median' }, { id: 'b', col: 'size', fn: 'weightedQuantile' }],
);
// median is IDENTICAL so it pushes; weightedQuantile is a fallback so it stays here.
return `engine=${split.engine[0].class} client=${split.client[0].class}`;
Joining two grids
Two grids each holding their own data, and a third showing where they meet. Orders against
customers; shipments against carriers; enrolments against students. The third grid derives from
one side and names the other as its join partner.
const joined = createGrid(host, {
source: {
mode: 'derived',
from: orders,
join: {
with: customers,
on: { left: 'customerId', right: 'id' },
select: ['name', 'tier'],
},
},
columns: [{ field: 'ref' }, { field: 'name' }, { field: 'tier' }, { field: 'amount' }],
});
| Key | Type | Description |
|---|---|---|
| with | Grid | Required. The grid holding the other side. |
| on | string | { left, right } | Required. The shared key: one field name when both sides use it, or one each. |
| type | 'inner' | 'left' JoinType | inner by default, keeping only rows that matched, which is usually what “common data” means. left keeps every row and leaves the brought-across fields undefined, the shape you want when the unmatched rows are the finding. |
| select | string[] | Which of the partner's fields to bring across. All of them by default. |
| prefix | string | Rename the brought-across fields, for when both sides have a name worth keeping. |
| follow | 'all' | 'filtered' RowScope | Which of the partner's rows to read. all by default: a lookup table is normally the whole table, and a customer list filtered to Europe would otherwise silently drop every other order from a grid the reader takes to be all orders. |
The row count does not change. A key appearing twice on the right keeps the first match rather than emitting a row per pair. SQL would multiply them out; here that would change the row count of a grid the reader thinks of as “the orders” and quietly double every total taken from it.
Both sides are live. The partner is read at derivation time, not captured when the grid was built, and editing it re-derives, a corrected tier in the customer grid moves the order into a different band in the joined one.
Cross-filtering: the path back up
Derivation runs one way. A derived grid reads its source and never writes to it, which is what makes a chain of them safe to reason about. Cross-filtering is the single deliberate path back up: clicking a row in a summary panel filters the grid it summarises.
const byRep = createGrid(panel, {
source: {
mode: 'derived', from: main, groupBy: 'rep', refresh: 'live',
crossFilter: true,
select: { total: { of: 'amount', fn: 'sum' } },
},
columns: [{ field: 'rep' }, { field: 'total' }],
});
byRep.on('row:clicked', (e) => byRep.crossFilter.toggle(e.key));
The event is row:clicked, not row:click. A handler bound to the wrong
name subscribes without error and never fires, so this example is executed on every build to
keep the name honest: it wires the same handler to a stand-in grid, emits the event, and checks
the click reached the cross-filter.
// A stand-in for the grid's event bus and cross-filter, so the wiring above can
// be executed here without a DOM. The names are the product's own.
const listeners = {};
const filtered = [];
const grid = {
on: (name, fn) => { (listeners[name] = listeners[name] || []).push(fn); },
emit: (name, e) => { for (const fn of listeners[name] || []) fn(e); },
crossFilter: { toggle: (key) => { filtered.push(key); } },
};
// The line from the example, verbatim in its event name.
grid.on('row:clicked', (e) => { if (e.key) grid.crossFilter.toggle(e.key); });
// A click on a rep row. The wrong name - 'row:click' - would reach no handler,
// and this block would produce '' instead of the key.
grid.emit('row:clicked', { key: 'EMEA' });
grid.emit('row:click', { key: 'US' });
return filtered.join(',');
| Member | Returns | Description |
|---|---|---|
| enabled() | boolean | Whether this grid can cross-filter a source. False on a grid that is not derived, or whose source has no crossFilter. |
| column() | string | null | The source column the filter is pushed onto. |
| get() | string[] | The keys currently filtering the source. |
| set(keys) | void | Filter the source to these derived rows. null clears. |
| toggle(key) | void | Add or remove one key: what a click handler wants. |
| clear() | void | Take this grid's filter off its source. |
A panel does not filter itself. The grid pushing the filter leaves its own condition out when it reads the source back. Without that, clicking one rep would collapse the panel to that single row and strand the reader with nothing else to click. It is the same rule that keeps a header histogram showing every bar after you click one (facets), applied between grids instead of within one.
Several panels compose. Each leaves out only its own condition, so two panels over different columns narrow each other while both stay whole: pick a rep and the region panel shows that rep's regions, pick a region and the rep panel shows that region's reps.
It needs a memory source. Leaving a panel's own condition out means asking
the source for every row that survives the other filters, which a source holding one
page cannot answer. Over a remote, paged or stream
source the read falls back to the ordinary filtered rows (narrowed by the very column
the panel asked to be excluded from) and the panel collapses to the row that was
clicked. It warns when it does. Exclude the originating panel on the server instead.
It is an ordinary filter. The condition goes through the source's
filters.set, so it undoes, rides in a saved view, and appears in whatever filter UI
the grid already has. There is no second filter model beside the real one.
The remote request
Your fetch receives one object and returns { rows, total }.
| Field | Type | Description |
|---|---|---|
| range | { start, end } | The block wanted, end exclusive. Not from/to. |
| sort | { col, dir }[] | In priority order. |
| filters | FilterSet | The condition tree, in the wire form described under operators. |
| quick | string | Present only when the quick filter is set. |
| groupBy / groupPath | string[] / unknown[] | Which columns group, and which node this block belongs to. |
| pivotBy / pivotMode | string[] / boolean | |
| totals | string[] | Columns wanting an aggregate, so the server can compute them. |
| context | unknown | Your own config.context, passed through untouched. |
| signal | AbortSignal | Aborted when the request is superseded: pass it to fetch. |
| protocol | number | Wire version, so a server can tell what it is talking to. |
Blocks are requested as the viewport reaches them and cached. Changing the sort, the filter or the grouping invalidates the cache and re-queries.
Events
One bus. There are no onX configuration properties, and every event is
described once, in the generated
event table at the end of this page: all 162 of them, with
when each fires, the payload interface a handler receives and whether it can be cancelled.
That table is generated from the declarations, so it is the release’s own answer rather
than a second list to keep in step.
const off = grid.on('cell:changed', e => save(e.row.data));
grid.once('ready', init);
grid.on('*', e => console.log(e.type, e)); // wildcard, for debugging
off(); // on() returns its own unsubscribe
What a handler receives. Every payload is a GridEvent: the event’s own fields, plus type, grid and origin. origin is 'api', 'user', 'init' or 'ai' - a host persisting state reads it to ignore its own writes and avoid a feedback loop, and it is how a genuine user gesture is told from a module-driven re-entry.
Cancelling an action. Every user-initiated mutation is gated by a before… event carrying a BeforeEvent: the action’s own context plus preventDefault(reason?), defaultPrevented and reason. Calling preventDefault() (or returning false) cancels the action, and a handler may be async - the mutation is held until every before-handler settles, which is what makes a confirm dialog or a server check a genuine gate. On a veto the paired <action>:cancelled fires with the reason. Host/API writes and remote or router-applied deltas (origin !== 'user') do not fire these.
The declared list is complete, and stays complete: tools/check.js compares every emit() in the grid against the declared event names and fails the build on a mismatch. Each module - charts, the tabbed grid, the kanban board, the KPI panel, the dashboard layout, the Gantt, the AI controller and the Data Router - raises its own events, which are listed on that module’s own surface rather than on this bus.
A declared event is reachable directly, executed
Every name in the event table is a first-class event: grid.on(name, ...)
binds it without the unknown-name warning, and each maps to a framework handler prop. Shown for
annotation:changed, which promoted from a wildcard-only emission to a
declared event. Run headless on every build.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { isKnownEvent } = await import('../packages/core/src/events/index.js');
const { handlerName } = await import('../packages/modules/shared/adapter.js');
const grid = createHeadlessGrid({ columns: [{ field: 'a' }], rows: [] });
// Binding a declared event does not trip the unknown-name warning that an
// undeclared one would - that warning is exactly what removed.
const warnings = [];
const original = console.warn;
console.warn = (...a) => warnings.push(a.join(' '));
const off = grid.on('annotation:changed', () => {});
console.warn = original;
off();
grid.destroy();
return [
isKnownEvent('annotation:changed'), // declared at on() time
warnings.length, // 0: no unknown-event warning
handlerName('annotation:changed'), // the adapter prop the frameworks expose
].join(' | ');
Cancellable before-events: guarded editing and confirm-before-delete, executed
Every user-initiated mutation has a cancellable before event. A handler cancels the pending action with
preventDefault(reason?) and may be async - the mutation is held until it
settles, which is what makes a confirm dialog or a server check a genuine gate. On a veto the paired
<action>:cancelled fires with the reason. Host/API writes and remote deltas do not
fire them. Run headless on every build.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'name', edit: { enabled: true } }, { field: 'v', type: 'number', edit: { enabled: true } }],
rows: [{ id: 'a', name: 'Ann', v: 1 }, { id: 'b', name: 'Bo', v: 2 }],
selection: 'multiple',
});
const log = [];
// Guarded editing: veto a commit on a locked row, and hear the cancellation.
grid.on('beforeEdit', (e) => { if (e.key === 'a') e.preventDefault('locked'); });
grid.on('edit:cancelled', (e) => log.push('edit ' + e.reason));
// Guard the query and layout surfaces.
grid.on('beforeSort', (e) => e.preventDefault('view-locked'));
grid.on('sort:cancelled', () => log.push('sort'));
grid.on('beforeFilter', (e) => e.preventDefault('no-filter'));
grid.on('filter:cancelled', () => log.push('filter'));
grid.on('beforeColumnMove', (e) => e.preventDefault('fixed'));
grid.on('columnMove:cancelled', () => log.push('colmove'));
grid.on('beforeColumnResize', (e) => e.preventDefault('fixed'));
grid.on('columnResize:cancelled', () => log.push('colresize'));
grid.on('beforeColumnHide', (e) => e.preventDefault('mandatory'));
grid.on('columnHide:cancelled', () => log.push('colhide'));
grid.on('beforeGroup', (e) => e.preventDefault('frozen'));
grid.on('group:cancelled', () => log.push('group'));
grid.on('beforeRowMove', (e) => e.preventDefault('ordered'));
grid.on('rowMove:cancelled', () => log.push('rowmove'));
grid.on('beforeRowAdd', (e) => e.preventDefault('quota'));
grid.on('rowAdd:cancelled', () => log.push('rowadd'));
// A row dropped in from another grid: fires on the receiving grid, naming the
// row under the pointer, so "assign this to that" can veto the insert.
grid.on('beforeRowReceive', (e) => { if (e.overKey !== null) e.preventDefault('assigned'); });
grid.on('rowReceive:cancelled', () => log.push('receive'));
grid.on('beforeSelect', () => {});
grid.on('selection:cancelled', () => log.push('sel'));
// Confirm before delete: an async handler holds the delete until it settles.
grid.on('beforeDelete', async (e) => { await Promise.resolve(); e.preventDefault('user cancelled'); });
grid.on('delete:cancelled', (e) => log.push('delete ' + e.reason));
// Past-tense notifications a host can also observe (not gates): print, remote
// export, and the keyboard-shortcuts overlay.
grid.on('print:before', () => log.push('print-before'));
grid.on('print:after', () => log.push('print-after'));
grid.on('export:request', () => {});
grid.on('export:done', () => {});
grid.on('shortcuts:opened', () => {});
grid.on('shortcuts:closed', () => {});
// A vetoed sort (sync) and an edit blocked on the locked row.
grid.sort.set([{ col: 'v', dir: 'desc' }]);
grid.edit.start('a', 'name');
grid.edit.stop(false, { value: 'Nope' });
return grid.sort.get().length + '|' + grid.rows.byKey('a').data.name + '|' + log.join(',');
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.
Quick filter modes
One box, four ways to match. The mode persists until changed, so a host sets it once and goes on passing text alone.
grid.filters.quick('acme london', { mode: 'words' });
grid.filters.quickState(); // { text: 'acme london', mode: 'words' }
| Mode | Matches | Example |
|---|---|---|
| contains | The text appears somewhere in the row. The default. | cir finds CIR-100 |
| words | Every term appears, in any order and any column. | acme london finds a row with one in each |
| fuzzy | The characters appear in order, not necessarily together. | crc finds CIR-200 Manchester |
| regex | A regular expression, case-insensitive. | ^CIR-[12] |
Matching is against one cached text blob per row, built from the columns the viewer is permitted to see. Hidden and unreadable columns are excluded, so the row count cannot become an oracle for a value behind them.
An unfinished regular expression (foo( on the
way to foo(bar)) falls back to a literal search rather than matching nothing, so the
grid does not blank on every open bracket. fuzzy does not reorder rows: ranking
results would fight the sort the user chose.
Units of your own
Twenty-six unit systems ship: length, mass, pressure, data, bitrate, angle, temperature, flow and the rest. A family of your own takes three things, and all three are yours to set: the symbols, where the symbol sits relative to the number, and how the rungs relate to each other.
import { registerUnitSystem, defineUnit, createUnitType } from '@toclocoinc/lattice-grid';
// `factor` is how many base units one of these is. Exactly one must be 1.
registerUnitSystem('distance', [
defineUnit('mm', 0.001, ['millimetre', 'millimetres']),
defineUnit('cm', 0.01, ['centimetre', 'centimetres']),
defineUnit('m', 1, ['metre', 'metres']),
defineUnit('km', 1000, ['kilometre', 'kilometres']),
]);
createGrid(el, {
dataTypes: {
distance: createUnitType({ system: 'distance', unit: 'm', display: 'auto' }),
},
columns: [{ field: 'span', type: 'distance' }],
});
The factor values are the relationship between the rungs: there is no separate ladder to declare, and no ordering to get right, because the ladder is sorted by factor at load. A hand-ordered list of thirty units is one transposition away from an auto display that walks backwards, and that mistake is invisible in review.
| Option | Description |
|---|---|
| system | A built-in system, or one registered with registerUnitSystem. |
| unit | What the column stores. It need not be the system's base: the same ladder with unit: 'km' stores kilometres, and 50mm typed in becomes 0.00005. |
| display | 'auto' walks the ladder for the most readable rung, or name a symbol to fix it. |
| placement | 'prefix' puts the symbol in front: $1,200, and applies to input as well as display. Defaults to a suffix. |
| decimals | Fixed fraction digits; or minDecimals / maxDecimals, or significantFigures. |
| locale | Separators and grouping. Follows the grid's locale when unset. |
A unit given { auto: false } stays off the display: 'auto' ladder while remaining accepted on input and available as an explicit display. That is how imperial units sit beside metric ones without an auto readout jumping between the two.
The stored value is always a plain number in the column's own unit. Sorting, filtering, grouping, totals and the pivot all read that number and never the text, which is why 250mm sorts below 1.5 cm correctly rather than 1 sorting before 9. registerUnitSystem is global and throws on a duplicate name, so register each system once at startup rather than inside a component that may mount twice.
Compound display: 5 ft 11 in, 1 h 23 m
compound renders one stored number across an ordered subset of the system's units. It is display and parse only: the value stays a single base-unit number, so sort, filter, group and total are the same arithmetic they always were. The order is free - the units are sorted largest to smallest - and the smallest one carries any remainder. Parsing sums the parts, so a paste of 5 ft 11 in round-trips, and a single 71 in or a bare 6 still work.
const { formatUnit, parseUnit } = await import('../packages/core/src/columns/types/unit.js');
// A height column stored in metres, displayed as feet and inches.
const cfg = { system: 'length', unit: 'm', compound: ['ft', 'in'], locale: 'en-GB' };
const shown = formatUnit(1.8034, cfg); // across the two units
const stored = parseUnit('5 ft 11 in', cfg); // summed back to metres
const stable = formatUnit(parseUnit(shown, cfg), cfg) === shown; // round-trips
return `${shown} | ${stored} | ${stable}`;
The compound units need not include the stored unit, and any unit of the system is accepted on input: 71 in pasted into a feet-and-inches column is still 71 inches. Excel export and display: 'auto' share a rule here - a column of mixed-scale text is not summable in a spreadsheet, so the export uses the raw base number on the configured unit. The compound cell editor (mid-value keystrokes, roll-over between feet and inches, caret behaviour at a boundary) is a separate, later piece; this is the read-and-paste half.
Currency: an amount and a code
Currency is a real type, not a display format. Every other unit multiplies by a factor
fixed at load; a currency's “factor” is an exchange rate that moves, so it never
joins the unit factory. A value is an amount and a code - { amount: 10, code: 'USD' } is a different value from
{ amount: 10, code: 'EUR' }, and the code rides on every cell. The grid ships
and fetches no rates: the caller supplies a rate source, and a rate that is needed but absent
is surfaced loudly, never as zero. A footer refuses to add unlike currencies
unless a display currency and rates reconcile every value, the same stance temperature takes
for refusing a meaningless sum.
const { createCurrencyType, parseMoney, formatMoney, convertMoney, rateFunction, MISSING_RATE } =
await import('../packages/core/src/columns/types/currency.js');
// The caller owns the rates; the grid ships none. A missing one is loud, never zero.
const rates = { USD: 1, EUR: 0.92 };
const money = createCurrencyType({
code: 'USD', display: 'EUR', rates, rateBase: 'USD', decimals: 2,
nullDisplay: ' - ', missingRate: 'no rate', excel: '€#,##0.00', codes: ['USD', 'EUR'],
});
const rate = rateFunction(rates, 'USD');
const tenInEur = convertMoney(parseMoney('$10', { code: 'USD' }), 'EUR', rate);
const loud = formatMoney({ amount: 5, code: 'XYZ' }, money.currencyConfig).startsWith('no rate');
const marker = MISSING_RATE.length > 0;
return `${tenInEur.toFixed(2)}|${loud}|${marker}`;
| Option | Description |
|---|---|
| code | The default currency code for a bare numeric input. A number with its own symbol or code keeps that code. |
| display | The currency to render and total in. Omit to keep each cell in its own currency. |
| rates | The caller's rate source: a (from, to) => rate | null function, or a table of rates per unit of a common base. |
| rateBase | The code a rate table is denominated in. The cross rate is base-independent, so this documents the table's denomination for the reader. |
| missingRate | The loud marker rendered when a needed rate is absent. Defaults to MISSING_RATE. |
| decimals | Fixed fraction digits; omit for the currency's own convention. |
| nullDisplay | Text shown for an empty cell. |
| excel | An Excel number-format override for export. |
| codes | The code list the currency editor's picker offers. |
The stored value is always the amount and its own code. Sort, filter, group, copy and Excel export all read the underlying amount - converted to the display currency when rates allow, so £5 and $6 order by real value. Five ready-made types ship (currency, usd, eur, gbp, jpy); a mixed-currency column adds display and rates through createCurrencyType.
The statistic block
createStat draws the tile a dashboard opens with: a label, a value, its change
against a baseline, and a line saying what the comparison was. Two things make it worth using
rather than writing. It reads the grid, so it cannot disagree with the table
beneath it, a tile saying £4.2M above a table filtered to £1.8M is worse
than no tile, and that is what a hand-built tile does the first time somebody adds a filter.
And it formats through the column's own type: a stat over a
seconds column reads 42.1 ms, over a money column with an auto ladder
£1.2M, with nothing declared.
import { createStat } from '@toclocoinc/lattice-grid';
createStat({
grid, container: '#mrr',
title: 'Monthly recurring revenue',
of: 'mrr', fn: 'sum',
baseline: lastMonth,
footer: 'vs. last month',
});
| Key | Type | Description |
|---|---|---|
| grid | Grid | The grid to read. |
| container | Element | string | Required. An element, or a selector resolved against the grid's document. |
| title | string | The label above the value. Hidden when absent rather than left blank. |
| of | string | The column to reduce. Omit for count. |
| fn | TotalName | Any of the totals-row kernels: sum, avg, median, p95, distinct, gini and the rest. sum by default. |
| show | string | Report this column from the row holding the extreme, rather than the extreme itself: { of: 'sales', fn: 'max', show: 'rep' } is the name of the best rep. Needs min or max; no single row holds an average, so any other reduction is refused with a warning. |
| value | unknown | fn | A literal value (numeric or otherwise) or a function of the grid, instead of a reduction. |
| footer | string | fn | Text under the value, or a function of it. |
| baseline | number | fn | What the value is compared against. A zero baseline reports the absolute change and no percentage, because “up infinity per cent” is not a reading anyone can act on. |
| goodWhen | 'up' | 'down' | 'neither' StatGoodDirection | Whether a rise is good news, which decides the colour. up by default. Revenue up is green and error rate up is red; a tile that paints every rise green is misleading on half a dashboard. |
| scope | 'filtered' | 'all' | 'selected' StatFollowScope | Which rows feed the value. filtered by default; all for a tile that is deliberately a constant, such as the denominator a filtered number is a share of. |
| live | boolean | false stops the tile following the grid. refresh() still works, so a caller can drive it. |
| format | (value, grid) => string | Override the formatting the column's type would apply. |
| empty | string | Shown when there is no value. An em dash by default. |
| decimals | number | Fraction digits for a value whose reduction changed the unit. 2 by default. |
The column's formatter is borrowed only where the reduction leaves the unit alone. A Gini
coefficient over a money column is a ratio between 0 and 1, and rendering it as
$0.34 says it is thirty-four cents; counts, ratios and variances, which are
in units squared: fall back to a plain number.
Returns { element, value, refresh, destroy }. A misconfigured tile returns an inert
handle rather than throwing, so a dashboard with one bad tile still renders the other eleven.
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.
Formulas
A leading = in a numeric cell is a formula. The grid stores what it comes to.
=5 + 5
=quantity * unitPrice
=[Unit Price] * 1.2
=ROUND(quantity * unitPrice, 2)
=IF(quantity > 10, "bulk", "single")
=SUM(readings) // an array property on the row
References name columns of the same row, not cells, a grid sorts, filters,
groups and pages, so A1 would mean a different row from one moment to the next.
Matching is on field or title, ignoring case and spacing; bracket a name that contains spaces.
A property with no column of its own is reachable too.
| Group | Functions |
|---|---|
| Maths | SUM, AVERAGE/AVG, MIN, MAX, COUNT, PRODUCT, ABS, SQRT, POWER, MOD |
| Rounding | ROUND, ROUNDUP, ROUNDDOWN, FLOOR, CEILING |
| Logic | IF, AND, OR, NOT, COALESCE |
| Text | CONCAT, LEN, UPPER, LOWER, TRIM, LEFT, RIGHT |
| Statistics | MEDIAN, PERCENTILE, QUARTILE1, QUARTILE3, IQR, STDEV, STDEVP, VAR, VARP, COUNTDISTINCT |
The statistical functions use R type 7 quantiles, the same definition as the totals row, grid.statistics and the distribution formatting rules, so the four never disagree about what a median is. PERCENTILE reads 90 and 0.9 as the same request. Over an empty set they return a number rather than null, because a formula is arithmetic and has to keep composing.
Operators + - * / ^ with parentheses, comparison for IF, and postfix
%. ^ is right-associative and unary minus binds tighter than it, so
-2^2 is 4: Excel's answer rather than mathematics'.
// Your own functions, on top of the built-in library.
createGrid(el, {
formulaFunctions: {
MARGIN: ([revenue, cost]) => (revenue - cost) / revenue,
},
});
The result is stored, not the expression. A formula is a way
of entering a value: like 1,200, (50) or 12%, and it
commits as one undo step with the column's own validation. Persisting a formula and recalculating
it when a dependency changes is a separate feature; referencesOf() is exported for
anyone building it.
No eval, no new Function. A formula
is text a user typed, so evaluating it with the JavaScript engine would let anyone who can edit a
cell run code in your page. It is a hand-written parser and the only callable things are the
functions above.
Bare arithmetic is not a formula. 2-1 is a
plausible product code and 1/2 a plausible date, so both are refused rather than
guessed at. Declare a formula with =.
Your own menu items and buttons
The cell menu's function form is handed the cell that was clicked and the built-in items, so adding one entry does not mean reproducing the other thirteen.
createGrid(el, {
contextMenu: (params, defaults) => [
...defaults,
{ separator: true },
{
name: `Open ${params.value} in CRM`,
action: (ctx) => open(`/crm/${ctx.data.accountId}`),
},
],
});
params and the action's argument carry the same cell:
{ key, colId, value, row, data, column, index, grid }, where data is
your original row object. Return the array you want shown: add, remove, reorder or replace.
Returning an empty array suppresses the menu; returning nothing at all leaves the defaults
alone, so a missing return cannot silently delete the menu.
The same option is accepted on a column definition, so a column's menu is
declared where the column is rather than as one more branch inside a single grid-level callback.
It takes the same shapes plus a bare array for the common “these items here too”
case: boolean | MenuItem[] | (params, defaults) => items.
createGrid(el, {
columns: [
{ field: 'owner', contextMenu: [{ name: 'Reassign', action: reassign }] }, // appended after the grid's items
{ field: 'amount', contextMenu: (p, defaults) => [...defaults, { name: 'Reprice', action: reprice }] },
{ field: 'ref', contextMenu: () => [{ name: 'Copy reference', action: copyRef }] }, // only this: ignore defaults
{ field: 'nationalId', contextMenu: false }, // no menu on this column, others unaffected
],
});
The three levels compose as a chain: built-in defaults, then the grid-level
contextMenu, then the column's - each handed the previous result as its
defaults, so a column adding one item never restates the built-ins. The
array form chains too: contextMenu: [items] on a column is exactly
contextMenu: (p, defaults) => [...defaults, ...items], so the built-ins and every
grid-level item stay and the column's items follow them, in the order written. To
replace a column's menu instead, use the function form and ignore
defaults: contextMenu: () => items. (Earlier releases let an array replace the grid-level
items; the two forms now agree.) Suppression
follows the same order and the more specific level wins: false on a column is a
statement about that column alone. The reverse holds too, and is worth knowing before you
rely on grid-level contextMenu: false as a safety property: a column that declares
its own contextMenu opens one anyway. Grid-level false is a
default, not a lock - it is what makes “no menu anywhere except here”
expressible. On a right-click inside a multi-column selection
the clicked column's menu is the one that opens - not the intersection,
which loses items, and not the union, which offers actions wrong for most of the selection. A
group row, pivot group row or full-width row belongs to no column, so the chain has one link
fewer and the grid-level menu stands. The keyboard routes (Shift+F10 and
the Context Menu key) honour the column exactly as the pointer does. See
the guide for the full table of combinations.
The empty tail of a row is the row's. When the columns do not fill the
grid's width, each row has an empty area to the right of the last column. A right-click there
opens the grid's menu for that row, never the browser's, exactly as a right-click on a group row
does: there is no column under the pointer, so the column link is missing from the chain and the
grid-level menu stands, and cell:contextmenu (and so a builder's params)
carries colId: null, column: undefined and value: undefined
with the row, key and index filled in. The built-in items that act on
a cell - Paste, Clear, Fill down, Edit cell - are not offered there (there is no cell
for them to act on); the row and grid items are. A builder that reads
params.column should expect it to be absent there. The area below the last row
belongs to no row and keeps the browser's menu. On 1.54 and earlier a right-click in the
tail fell through to the browser's menu, which looked as though the grid had none; on those
versions give one column layout: { flex: 1 } so the cells reach the edge and there
is no tail to click.
Expand all and Collapse all are built-in row/grid items: present, in this order, on the row context menu and on every column's
header menu (the 3-dot button and a right-click on the heading) whenever the grid is grouped,
hidden rather than disabled otherwise. Each drives the public
grid.rows.expandAll() / collapseAll(), is matched by its translated
name like any other built-in item (catalogue keys menu.expandAll /
menu.collapseAll), and passes through the same contextMenu /
columnMenu chain above.
columnMenu takes the same form for the header's menu: both the 3-dot button
and a right-click on a heading. Its params is
{ colId, column, grid }. Anything of your own that you put on a column definition
is on column.def, so an item can appear on some columns and not others.
createGrid(el, {
columns: [{ field: 'jan', title: 'Jan', context: { month: 1 } }],
columnMenu: (params, defaults) => {
// Your own keys live on the definition you wrote.
const month = params.column.def.context?.month;
if (!month) return defaults;
return [...defaults, { separator: true },
{ name: 'Select quarter', action: () => selectQuarter(month) }];
},
});
The rail takes host buttons the same way. A string names a built-in and an object is yours, placed where it appears in the list rather than appended after the built-ins.
createGrid(el, {
toolPanel: {
side: 'left',
actions: ['undo', 'redo', {
name: 'sync',
title: 'Sync to the server', // or a function, re-read on every repaint
icon: 'restore',
run: ({ grid, keys, cells }) => api.sync(keys),
enabled: () => grid.history.canUndo(),
}],
},
});
An icon naming a sprite the registry does not have draws the blank glyph
and logs a [lattice] warning once, naming the icon and how to register it - it does not fail silently as an empty, still-clickable button.
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.
Accessibility
Built to WCAG 2.2 level AA. Every operation is reachable without a pointer, including resizing
and reordering a column, which have key bindings and column-menu items rather than depending on a
drag. The grid reports itself as grid or treegrid following its
configuration; rows and cells carry their position in the dataset rather than in the rendered
window, so a reader on row 500,000 is told so; and rows in a hierarchy carry their position among
their siblings, which a reader cannot count for itself when most of a branch was never rendered.
Focus is real focus rather than aria-activedescendant, and survives row recycling.
Tabbing into a grid shows a focus ring around the grid at once; the first arrow key moves focus,
and the ring, to a cell, and from then on Tab returns to that cell.
Sorting, filtering, selection, grouping, expanding, paging, undo, paste and a refused edit are all
announced. In Windows High Contrast Mode state is translated into borders and system colours
instead of tints. No information is carried by hue alone.
The full keyboard map, the screen reader support statement and the known limits, including the drag-only pivot zones, are in the guide.
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).
Filter grammar
The condition tree is a published wire protocol, not an internal shape. It serialises into saved state and travels to a remote source unchanged.
grid.filters.set({
op: 'and',
conditions: [
{ col: 'region', op: 'eq', value: 'EMEA' },
{ col: 'capacity', op: 'between', value: [100, 500], bounds: '[)' },
{ op: 'not', conditions: [
{ col: 'status', op: 'in', value: ['closed'] },
] },
],
});
| Group | Operators |
|---|---|
| Equality | eq, ne |
| Ordering | lt, lte, gt, gte |
| Ranges | between, notBetween, with bounds of '[]', '[)', '(]' or '()' |
| Sets | in, notIn |
| Text | contains, notContains, startsWith, endsWith, matches |
| Blankness | blank, notBlank |
| Multi-value | containsAny, containsAll, containsNone, for cells holding an array of ids |
| Grouping | and, or, not |
An operator outside this list is refused, not applied. filters.set() and state.apply() drop a condition whose op is not one of the operators above rather than installing it - it never reaches filters.get() and the grid is left exactly as filtered as it was before. A [lattice] warning names the operator received and the operators valid for that column's type. In a compound filter only the offending leaf is dropped; every other condition still applies.
One filter, not two. A condition set from a header popup, from the tool panel, or through grid.filters.set() all merge into the same tree. Reading grid.filters.get() always gives the whole truth.
Host predicates: where
Some filters cannot be written as a condition, because what they test is not in any column: whether this user may see the row, whether you hold a rate for its currency, whether it is in the set your last API call returned. Register those as named predicates.
grid.filters.where('visibleToMe', row => row.owner === me);
grid.filters.where('rateKnown', row => rates.has(row.ccy), { deps: ['ccy'], pinned: true });
grid.filters.where('visibleToMe', null); // remove
grid.filters.where(); // the registered names
grid.filters.reapply('rateKnown'); // re-run one
grid.filters.reapply(); // re-run all
Registering is activating. There is no "a filter is present" flag to keep in step, because that flag is the thing that goes wrong: it is a second piece of state describing the first, and when the two disagree the grid either filters while reporting that it is not, or reports a filter while every row passes. A predicate is in force from the moment it is registered until it is removed.
Several are in force at once under their own names, ANDed with each other and with the
condition tree; removing one leaves the rest alone. The predicate is handed the data
row, the same shape DerivedSourceConfig.where receives.
| Option | Type | What it does |
|---|---|---|
| deps | string[] | The columns the predicate reads, in the same spirit as value.deps on a computed column. Declared, the verdict is cached per row and re-run only when one of these columns changes on that row. Omitted, the predicate is treated as reading the whole row and runs on every pass - never stale, and never skipped either. |
| pinned | boolean | Survive filters.clear(). For row-level permissions and tenant scoping, where a "clear filters" button must never widen what the user can see. |
| condition | FilterSet | A declarative twin, pushed to the source while the function stays as the residual. It must be implied by the predicate: the grid ANDs both, so a twin wider than the function costs only time, while one narrower than it hides rows the function would have kept. |
Coming from AG Grid's external filter? The three pieces map onto two. isExternalFilterPresent() disappears - registration is presence. doesExternalFilterPass(node) becomes the named predicate you pass to where. onFilterChanged() becomes either deps, when what changed is a column the grid can watch, or reapply(name?), when it is something the grid cannot see at all - a rate table arriving late, a permission refresh.
A predicate runs where the whole dataset is. Memory, stream and derived sources hold every row, so the function runs across all of them and the counts it produces are whole-dataset counts. The paged and remote sources hold only what they fetched, and what reaches them is the condition tree rather than the function: a predicate on either of those narrows nothing by itself, and the grid warns once when you register it, naming the predicate and the source kind.
A pushdown source is the exception, up to a point. It can fetch the whole matching set and run the function over it, so it does - while that set is under whereRowLimit (default 50,000 rows). At or past the limit, or when the adapter reports no row total, it refuses: the predicate is not applied, the rows it would exclude stay on screen, and a warning names the adapter and the way out. Honouring it past that point would silently turn a windowed grid into a whole-dataset download, which is the thing a pushdown source exists to avoid.
The twin is the route that always works. Give the predicate a condition twin - it is ANDed into the tree the source is sent, so the engine narrows the fetch itself, at any size, and the grid stays silent because that case genuinely works. That is the supported route on a server-delegated or pushdown source.
Only names are state. filters.get() still returns exactly what the user set. state.get() carries where: string[] - the names in force - because a predicate is your code and cannot be serialised into a saved view or restored from one. state.apply() naming a predicate you have not registered reports the skip rather than installing anything, and never removes a predicate a saved view did not name.
A pinned permission filter and a "my items" toggle on one grid, executed on every build:
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const me = 'ana';
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'team' }, { field: 'owner' }, { field: 'ccy' }],
rows: [
{ id: '1', team: 'eu', owner: 'ana', ccy: 'USD' },
{ id: '2', team: 'us', owner: 'ana', ccy: 'USD' },
{ id: '3', team: 'eu', owner: 'bo', ccy: 'ZWL' },
],
});
// Row-level permission. Pinned, so "clear filters" cannot widen it, and it
// carries a declarative twin a server can push.
grid.filters.where('teamVisible', (row) => row.team === 'eu', {
pinned: true,
condition: { col: 'team', op: 'eq', value: 'eu' },
});
// An ordinary "my items" toggle, re-run only when `owner` changes on a row.
grid.filters.where('myItems', (row) => row.owner === me, { deps: ['owner'] });
const both = grid.rows.count(); // permission AND my items
grid.filters.where('myItems', null); // toggle off
const afterToggleOff = grid.rows.count();
grid.filters.clear(); // the pinned one survives
const afterClear = grid.rows.count();
const stillOn = grid.filters.where().join(',');
const inState = grid.state.get().where.join(',');
grid.destroy();
return `${both}|${afterToggleOff}|${afterClear}|${stillOn}|${inState}`;
A configuration, executed
This block runs on every build. If a key here stopped being honoured, or was renamed, the build would fail rather than the documentation quietly going stale.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// Every one of these is a documented configuration key, set together so the
// example proves they are accepted and honoured rather than merely spelled.
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'name', field: 'name' }, { id: 'size', field: 'size', type: 'number' }],
rows: [{ id: '1', name: 'a', size: 3 }, { id: '2', name: 'b', size: 1 }],
rowHeight: 32, headerHeight: 40, overscan: 8, autoHeight: false,
showHeader: true, density: 'compact', theme: 'light',
locale: 'en-GB', timeZone: 'UTC', title: 'Readings',
gridLines: 'both', cornerRadius: 4, stripedRows: false, targetSize: 'default',
sampleSize: 100, quickFilterText: '', maximise: false,
shortcuts: true, rowReorder: false,
stickyGroupHeaders: true, groupFooter: false,
totalFilteredOnly: false, showTotalInHeader: false,
aggregateChooser: false,
allowUnsafeTemplates: false, useWorker: false,
sharedMemory: false, workerThreshold: 100000,
columnVirtualisationAbove: 40, showColumnFunctions: false,
});
const n = grid.rows.count();
grid.destroy();
return n;
Writing direction and the two alignment vocabularies, executed
direction is a recognised configuration key, and a column's resolved
align keeps the spelling it was given: left/right are physical edges,
start/end are logical and mirror in a right-to-left grid. A number column with no
align of its own still defaults to the logical end.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
direction: 'rtl',
rowKey: 'id',
columns: [
{ id: 'l', field: 'l', align: 'left' }, // physical: the left edge in either direction
{ id: 'r', field: 'r', align: 'right' }, // physical: the right edge in either direction
{ id: 's', field: 's', align: 'start' }, // logical: the right edge in this RTL grid
{ id: 'e', field: 'e', align: 'end' }, // logical: the left edge in this RTL grid
{ id: 'n', field: 'n', type: 'number' }, // a number column defaults to the logical end
],
rows: [{ id: '1', l: 'a', r: 'b', s: 'c', e: 'd', n: 1 }],
});
const resolved = ['l', 'r', 's', 'e', 'n'].map((id) => grid.columns.get(id).align);
const out = [grid.config().direction, ...resolved].join('|');
grid.destroy();
return out;
Non-blocking stream ingest, executed
A stream source loaded with ingest.useWorker on. In a browser a
chunk that clears ingest.workerThreshold is columnized on a Worker so the main thread
is not blocked; here in Node there is no Worker, so it columnizes in-process - the same code,
the same result, which is exactly what this asserts. With retainSource:false the
grid keeps only the packed columns, so rows.data() returns reconstructed objects
rather than the caller's own. This makes stream (and remote) ingest
non-blocking; memory and paged sources still read the caller's objects on the main thread.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// A stream source: the producer pushes chunks of rows as it finds them.
async function* open() {
yield { rows: [{ id: '1', city: 'Oslo', pop: 700000 }, { id: '2', city: 'Bergen', pop: 280000 }] };
yield { rows: [{ id: '3', city: 'Tromsø', pop: 77000 }] };
}
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'city', field: 'city' }, { id: 'pop', field: 'pop', type: 'number' }],
source: { mode: 'stream', open },
// Columnize stream chunks off the main thread when a chunk is large enough,
// and keep only the packed columns rather than the caller's row objects.
ingest: { useWorker: true, workerThreshold: 1, retainSource: false },
});
// The stream loads over async frames; wait for it to finish before counting.
await new Promise((resolve) => grid.on('stream:end', resolve));
const n = grid.rows.count();
grid.destroy();
return n;
Source-layer memory reduction, executed
A memory grid loaded with ingest.dropSourceRows on. Once the column store
is built, the caller's row objects are released from the source layer and the grid config, so the
packed columns are the only resident copy - an order-of-magnitude drop at scale. Reads are served
by reconstructing a row from the columns, so the values are unchanged; what is gone is
object identity, which is why rows.data() returns a fresh object each call rather than
the one you supplied.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const supplied = [{ id: '1', city: 'Oslo', pop: 700000 }, { id: '2', city: 'Bergen', pop: 280000 }];
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'city', field: 'city' }, { id: 'pop', field: 'pop', type: 'number' }],
source: { mode: 'memory' },
rows: supplied,
// Release the caller's objects; keep only the packed columns.
ingest: { dropSourceRows: true },
});
const back = grid.rows.data();
// Same values, reconstructed from the columns - but not the caller's own object.
const valuesMatch = back[0].city === 'Oslo' && back[0].pop === 700000;
const identityDropped = back[0] !== supplied[0];
grid.destroy();
return valuesMatch && identityDropped;
Events, executed
Fourteen events raised by ordinary calls, asserted on every build. An event that stopped firing, or changed name, fails here rather than in a consumer.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'n', field: 'n' }, { id: 's', field: 's', type: 'number' }],
rows: [{ id: '1', n: 'a', s: 3 }, { id: '2', n: 'b', s: 1 }],
});
// A wildcard handler receives one event object; `type` says which arrived.
const seen = new Set();
grid.on('*', (event) => seen.add(event.type));
grid.sort.set([{ col: 's', dir: 'asc' }]); // sort:changed
grid.filters.set({ col: 's', op: 'gt', value: 0 }); // filter:changed
grid.columns.hide('n'); // column:visible
grid.columns.move('n', 1); // column:moved
grid.columns.pin('n', 'left'); // column:pinned
grid.columns.groupColumns(['n', 's'], { title: 'Both' }); // columngroup:changed
grid.set('rowHeight', 30); // config:changed
grid.rows.apply({ update: [{ id: '1', s: 9 }] }); // rows:changed, model:changed
grid.state.reset(); // state:reset, state:changed
const raised = seen.size;
grid.destroy();
return raised;
View persistence, executed
One event carries every state change and says what caused it, so a save layer subscribes once and skips the restore-to-default - which would otherwise write the default straight back over the view the user had just abandoned.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'n', field: 'n' }, { id: 's', field: 's', type: 'number' }],
rows: [{ id: '1', n: 'a', s: 3 }, { id: '2', n: 'b', s: 1 }],
});
const causes = [];
let writes = 0;
grid.on('state:changed', (event) => {
causes.push(event.cause);
// The one cause a save must ignore: persisting a reset writes the default
// back over the view the user has just abandoned.
if (event.cause === 'reset') return;
// A real host debounces, then writes grid.state.get() - which is
// permission-sanitised, unlike the raw capture.
writes++;
});
grid.sort.set([{ col: 's', dir: 'asc' }]); // cause: 'user', sections: ['sort']
grid.state.apply({ version: 2, sort: [] }); // cause: 'apply', with a report
grid.state.reset(); // cause: 'reset' - deliberately not saved
const result = `${causes.join(',')}:${writes}`;
grid.destroy();
return result;
Every grid namespace and method, executed
Reading a namespace builds it, so this proves each is reachable rather than declared and absent. The plain methods are called, not merely named.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'n', field: 'n' }],
rows: [{ id: '1', n: 'a' }],
});
// Every namespace the grid exposes. Reading one builds it, so this proves
// each is reachable rather than declared and absent.
const namespaces = [
'ai', 'columns', 'comments', 'config', 'crossFilter', 'detail',
'diagnostics', 'diff', 'edit', 'element', 'export', 'facets',
'filters', 'form', 'formatting', 'highlight', 'history', 'licence',
'messages', 'overlay', 'pagination', 'permissions', 'presence', 'presentation',
'ready', 'redaction', 'rows', 'scroll', 'selection', 'sort',
'state', 'statistics', 'timeline', 'updates', 'views', 'destroyed',
];
const present = namespaces.filter((name) => name in grid).length;
// And the plain methods, each called rather than merely typed.
grid.set('rowHeight', 30);
grid.setAll({ overscan: 6 });
grid.get('rowHeight');
grid.getVersion();
grid.getPinnedRows();
grid.setPinnedRows({ top: [], bottom: [] });
grid.rendererHost();
const off = grid.on('config:changed', () => {});
grid.once('config:changed', () => {});
grid.emit('config:changed', {}, 'api');
grid.off('config:changed', off);
typeof grid.attachRenderer;
grid.destroy();
return present;
Every core export, executed
Named and resolved against the barrel on every build. A rename or a removal fails here rather than in a consumer's build.
const core = await import('../packages/core/src/index.js');
// Every declared export of the core package, named and checked. A symbol
// that was renamed or dropped fails here, not in a consumer's build.
const declared = [
'AR', 'AR_SA', 'CS_CZ', 'DA_DK',
'DEFAULT_LOCALE', 'DE_DE', 'EL_GR', 'EN_GB',
'EN_US', 'ES_ES', 'FI_FI', 'FR_CA',
'FR_FR', 'HU_HU', 'IT_IT', 'JA_JP',
'LOCALES', 'MESSAGE_KEYS', 'NB_NO', 'NL_NL',
'NO_CAPABILITIES', 'PL_PL', 'PT_BR', 'RO_RO',
'SV_SE', 'UK_UA', 'UNIT_SYSTEMS', 'applyResidual',
'auditCatalogue', 'compileRules', 'createPushdownSource', 'createRadixType',
'createUnitType', 'defineUnit', 'dfqlAdapter', 'duckdbAdapter',
'evaluateFormula', 'formatList', 'formatUnit', 'getVersion',
'graphqlAdapter', 'ingest', 'ingestSync', 'licenceInfo',
'licenceState', 'licenseInfo', 'licenseState', 'looksLikeFormula', 'odataAdapter',
'parseUnit', 'planQuery', 'referencesOf', 'registerModules',
'registerUnitSystem', 'resolveCatalogue', 'resolveLocale', 'restAdapter',
'restoreState', 'serialiseState', 'setLicence', 'setLicense',
'version',
];
return declared.filter((name) => core[name] !== undefined).length;
The dhtmlx translation, executed
Every translated key in one definition, with the result asserted. A key the wrapper stopped honouring drops out of the output and fails the build.
const { translateColumn } = await import('../packages/modules/dhtmlx-compat/columns.js');
// Every dhtmlx column key the wrapper translates, in one definition. A key it
// stopped honouring would drop out of the result and fail this example.
const translated = translateColumn({
id: 'size',
header: 'Size',
type: 'number',
width: 120, minWidth: 80, maxWidth: 200,
resizable: true, hidden: false, draggable: true,
align: 'right',
tooltip: 'How big', tooltipTemplate: null,
template: (v) => String(v), htmlEnable: false,
sortable: true,
editable: true, editorType: 'datePicker',
editorConfig: { min: 0 }, options: [],
summary: 'sum',
});
// And the constructor keys, which translate at the grid rather than the column:
// rowKey, columns, data, autoHeight, rowHeight, headerRowHeight,
// multiselection, dragItem and rowTransfer.
return [
translated.id,
translated.layout.width,
translated.cell.align,
translated.sort.enabled,
translated.edit.editor,
translated.total,
].join('|');
Cell spans, executed
dhtmlx's imperative addSpan maintains a span table, and that table drives
Lattice's own per-cell span functions - installed on each column's cell definition. A span
declared through the shim is read back through the very functions the renderer calls.
// Aliased on import: the shim's span factory, bound to a local name.
const { createSpans: installSpans } = await import('../packages/modules/dhtmlx-compat/spans.js');
// A minimal stand-in for a grid: it just holds its columns, which is all the
// span shim touches. `createSpans` installs `cell.spanRows`/`cell.spanColumns`
// on each column by re-setting them through its own wrapper.
let columns = [{ id: 'name', cell: {} }];
const grid = {
get: () => columns,
set: (_, next) => { columns = next; },
};
const spans = installSpans(grid);
spans.addSpan('R0', 'name', 3, 2); // rowspan 3, colspan 2
// The column's own cell functions now read the maintained table.
const { cell } = columns[0];
const spanned = { row: { key: 'R0' }, colId: 'name' };
const plain = { row: { key: 'R1' }, colId: 'name' };
return [
cell.spanRows(spanned),
cell.spanColumns(spanned),
cell.spanRows(plain),
cell.spanColumns(plain),
].join('|');
Every module export, executed
Every shipped module’s exports, resolved against its own barrel on every build.
// Every declared export of every shipped module, resolved against its own
// barrel. A module that stopped exporting something fails here.
const modules = [
[await import('../packages/dom/src/index.js'), [
'ContextMenu', 'Messages', 'Registry', 'autoInit',
'createGrid', 'createLocalViewStorage', 'createMessages', 'createStat',
'deltaOf', 'gridElementsWithin', 'hydrateTable', 'mountPanel',
'readTable', 'toneOf', 'LatticeGrid',
]],
[await import('../packages/modules/charts/index.js'), [
'Chart', 'PALETTE', 'SCHEMES', 'TYPES',
'createChart', 'chartRange', 'canChartRange', 'deriveRangeSpec', 'regressionPlots',
'registerScheme', 'resolveScheme', 'schemeNames',
'setDefaultScheme',
]],
[await import('../packages/modules/htmx/index.js'), [
'HTML_ROW_WARNING_THRESHOLD', 'QUERY_CHANGED_EVENT', 'SCROLL_NEAR_END_EVENT', 'attach',
'destroyWithin', 'driveInfiniteScroll', 'driveOobUpdates', 'driveServerMode',
'ingestResponse', 'initWithin', 'queryParams', 'restoreStateWithin',
'rowsFromFragment', 'rowsFromJson', 'saveStateWithin', 'warnIfLargeHtmlPayload',
]],
[await import('../packages/modules/webcomponent/index.js'), [
'ATTRIBUTE_CONFIG', 'EVENT_PREFIX', 'GridElementController',
'TAG_NAME', 'createLatticeGridElement', 'defineLatticeGrid', 'domEventName',
'observedAttributeNames',
'createLatticeElements', 'defineLatticeElements', 'ELEMENT_TAGS',
'createLatticeKPIElement', 'createLatticeChartElement', 'createLatticeKanbanElement',
'createLatticeGanttElement', 'createLatticeLayoutElement', 'createLatticeTabsElement',
'createLatticeRouterElement', 'adoptLatticeStyles', 'latticeStyleSheets',
]],
[await import('../packages/modules/devtools/index.js'), [
'CONSOLE_ACTIVATION', 'createDevtools', 'expose',
]],
[await import('../packages/modules/react/index.js'), [
'EVENT_NAMES', 'handlerName',
]],
[await import('../packages/modules/vue/index.js'), [
'createLatticeGrid', 'dashedName',
]],
[await import('../packages/modules/svelte/index.js'), [
'createLatticeAction',
]],
[await import('../packages/modules/dhtmlx-compat/index.js'), [
'Grid',
]],
[await import('../packages/modules/data-router/index.js'), [
'createDataRouter',
]],
[await import('../packages/modules/mock-socket/index.js'), [
'MockWebSocket', 'rng', 'opsFeed', 'priceFeed',
]],
[await import('../packages/modules/kanban/index.js'), [
'createKanban',
]],
[await import('../packages/modules/geo-world-110m/index.js'), [
'pack',
]],
];
let present = 0;
for (const [mod, names] of modules) {
present += names.filter((name) => mod[name] !== undefined).length;
}
return present;
Nested configuration, executed
Thirteen option blocks, each key written where it belongs. Parsed and evaluated on every build, so a key that was renamed or moved shows up here.
// Placeholders for the things a real page supplies. The point of this block is
// the option names: each one below is a documented key, written where it
// belongs, so a key that was renamed or moved stops matching its interface.
const source = {}, other = {}, provider = {}, compute = {}, adapter = {};
const fetch = async () => ({ rows: [], total: 0 });
const open = () => ({ close() {} });
// A derived grid - DerivedSourceConfig
const derivedSourceConfig = { mode: 'derived', from: source, follow: 'filtered', unnest: 'tags',
join: { with: other, on: 'id' }, where: (r) => r.size > 0,
bucket: { of: 'taken', by: 'day' }, groupBy: 'team',
select: { total: { of: 'size', fn: 'sum' } },
limit: 10, limitPer: 'team', cumulative: { of: 'size', upTo: 0.8 },
profile: 'size', orient: 'metrics', refresh: 'idle', crossFilter: true };
// Editing - EditConfig
const editConfig = { enabled: true, commit: 'blur', confirm: false, start: 'dblclick',
enterMovesDown: true, undoDepth: 50, pendingTimeout: 2000 };
// Selection - SelectionConfig
const selectionConfig = { checkbox: true, headerCheckbox: true, checkboxOnly: true, ranges: true, fill: true, fillHandle: true };
// Tree data - TreeConfig
const treeConfig = { parentKey: 'parentId', path: 'path', orphans: 'root',
loadChildren: async () => [], hasChildren: (r) => !!r.kids };
// Master detail - DetailConfig
const detailConfig = { isMaster: (r) => true, target: '#detail', cacheLimit: 20,
onCreate: () => {}, placement: 'below' };
// Presence - PresenceConfig
const presenceConfig = { provider, me: { id: 'u1' }, roster: [], palette: ['#0d6b68'],
idleMs: 30000, removeMs: 60000, throttleMs: 100, lock: true, lockMs: 5000 };
// Comments - CommentConfig
const commentConfig = { provider, debounce: 300, indexLimit: 5000, markdown: false };
// A paged source - PagedSourceConfig
const pagedSourceConfig = { mode: 'paged', pageSize: 100, maxCachedPages: 5, fetch };
// A streaming source - StreamSourceConfig
const streamSourceConfig = { mode: 'stream', open, coalesceMs: 16, maxRows: 1e6, promoteToMemoryBelow: 5e5,
// A rolling *time* window beside the count one: keep five minutes, aged by the
// row's own clock. Omit ageBy and rows age from when they arrived instead.
maxAge: 5 * 60 * 1000, ageBy: 'ts' };
// A pushdown source - PushdownSourceConfig
const pushdownSourceConfig = { adapter, compute, pageSize: 200 };
// CSV export - CsvExportOptions
const csvExportOptions = { fileName: 'rows.csv', delimiter: ',', lineEnding: '\\n',
headers: true, download: false, quote: 'minimal', processCell: (v) => v };
// Facets - ColumnFacetConfig
const columnFacetConfig = { cardinalityLimit: 200, aboveLimit: 'search', bucketFn: (v) => v,
buckets: 20, granularity: 'day', strategy: 'even' };
// Numbers and units - NumberFormat
const numberFormat = { decimals: 2, minDecimals: 0, maxDecimals: 4, scale: 1,
unit: 'metre', system: 'si', space: true, binary: false, format: 'auto',
display: 'yesNo', label: 'Size', hint: 'in metres' };
// How rows enter the store - IngestConfig
const ingestConfig = { retainSource: false };
return [derivedSourceConfig, editConfig, selectionConfig, treeConfig, detailConfig,
presenceConfig, commentConfig, pagedSourceConfig, streamSourceConfig,
pushdownSourceConfig, csvExportOptions, columnFacetConfig, numberFormat, ingestConfig].length;
The remaining option names, executed
Set on a real grid and checked against its own diagnostics: an unrecognised key
raises config.unknown, so a renamed or dropped option fails here.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const { reportedWarnings } = await import('../packages/core/src/internal/util.js');
// Top-level configuration keys. Each is set on a real grid, and the grid is
// asked whether it recognised them: an unrecognised key raises
// `config.unknown:<key>`, so a renamed or dropped option fails right here.
const documented = [
'ai', 'alignedGrids', 'anomalySummary', 'columnDefaults', 'columnGroups', 'columnMenu',
'columnPresets', 'columnTagFilter', 'comments', 'components', 'context',
'contextMenu', 'dataTypes', 'detail', 'diff', 'edit',
'environment', 'facets', 'formatting', 'formulaFunctions', 'fullWidth',
'grandTotalRow', 'groupPanel', 'highlightOnChange', 'historyBar', 'hostFilter', 'ingest',
'licence',
'pagination', 'permissions', 'pinnedBottomRows', 'pinnedTopRows', 'pipes',
'pivot', 'presence', 'responsive', 'rowClass', 'rowForm',
'rowStyle', 'rowTemplate', 'rowTransfer', 'selection', 'source',
'state', 'statusBar', 'toolPanel', 'totalFns', 'totalOnlyChangedColumns',
'tree', 'typeOptions', 'updates', 'variants', 'views',
'workerUrl',
];
const before = reportedWarnings().length;
const config = { rowKey: 'id', columns: [{ id: 'n', field: 'n' }], rows: [] };
for (const name of documented) config[name] = undefined;
const grid = createHeadlessGrid(config);
const unknown = reportedWarnings().slice(before)
.filter((w) => w.key.startsWith('config.unknown:'));
grid.destroy();
// These belong to nested option blocks and are exercised in the block above:
// announce, apply, at, background, byKey, compute
// config, count, crossFilter, destroy, group, groupSelectsChildren
// groupSelectsFiltered, height, loaded, order, pageSizes, refresh
// reload, render, rowLabel, sort
if (unknown.length) throw new Error(`unrecognised: ${unknown.map((w) => w.key).join(', ')}`);
return documented.length;
Every event name, executed
Each documented event is subscribed to and unsubscribed on every build. A consumer wiring a handler to a renamed event gets silence, which is indistinguishable from an event that has not fired yet - so the name is checked rather than left to be discovered.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// Every documented event name, checked against the bus that would carry it.
// Subscribing to a name the grid does not know is the failure this catches:
// a consumer wiring a handler to a renamed event gets silence, and silence
// is indistinguishable from an event that simply has not fired yet.
const documented = [
'cell:changed', 'cell:clicked', 'cell:confirmed', 'cell:conflict',
'cell:contextmenu', 'cell:dblclicked', 'cell:edit:end', 'cell:edit:start',
'cell:mouseover', 'cell:mouseout', 'cell:mousedown', 'cell:mouseup',
'cell:pending', 'cell:reverted', 'clipboard:copy', 'column:filter:open', 'column:profile:open', 'column:grouped',
'column:menu:open', 'column:pivoted', 'column:resized', 'columns:changed',
'columns:tagged', 'comment:added', 'comment:deleted', 'comment:edited',
'comment:failed', 'comment:indexLoaded', 'comment:resolved', 'comment:threadClosed',
'comment:threadOpened', 'comment:unresolved', 'destroy', 'detail:toggled',
'diff:changed', 'diff:swapped', 'export:progress', 'facet:computed',
'facet:expanded', 'facet:failed', 'facet:filtered', 'form:closed',
'form:error', 'form:opened', 'form:saved', 'formatting:changed',
'group:toggled', 'header:contextmenu', 'highlight:changed', 'find:changed', 'history:applied',
'history:changed', 'licence:changed', 'page:changed', 'permissions:changed',
'presence:failed', 'presence:joined', 'presence:left', 'presence:lockRefused',
'presence:published', 'presence:updated', 'presentation:captured', 'presentation:changed',
'presentation:ended', 'presentation:scale', 'presentation:spotlight', 'presentation:started',
'presentation:view', 'range:changed', 'ready', 'redaction:changed',
'render:done', 'render:first', 'row:clicked', 'row:copied',
'row:dblclicked', 'row:edit:end', 'row:edit:start', 'row:moved',
'row:pending', 'row:confirmed', 'row:reverted', 'row:conflict',
'row:received', 'row:sent', 'rows:deferred', 'rows:paused',
'rows:queued', 'rows:resumed', 'scroll', 'scroll:end',
'selection:changed', 'size:changed', 'source:error', 'stream:chunk',
'stream:end', 'stream:evicted', 'timeline:attached', 'timeline:detached',
'timeline:seek', 'timeline:seeking', 'toolpanel:focus', 'tree:loadAborted',
'tree:loadFailed', 'tree:loaded', 'tree:loading', 'view:applied',
'view:default', 'view:removed', 'view:renamed', 'view:saved',
'views:changed',
'rowDrag:started', 'rowDrag:moved', 'rowDrag:left', 'rowDrag:ended',
];
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'n', field: 'n' }],
rows: [{ id: '1', n: 'a' }],
});
// `on` returns its own unsubscribe, so a name it accepts round-trips.
let wired = 0;
for (const name of documented) {
const off = grid.on(name, () => {});
if (typeof off === 'function') { off(); wired += 1; }
}
grid.destroy();
return wired;
Module and utility APIs
Everything else the package declares. Each entry is public: it is declared in the type definitions, which is what makes it a promise.
Locale and messages
| Name | Signature | Description |
|---|---|---|
| DEFAULT_LOCALE | string | The locale used when none is configured and none can be read from the page. |
| LOCALES | Record<string, object> | Every built-in catalogue, by locale name. |
| resolveLocale | (configured?, declared?, fallback?) => string | Settle which locale applies: what you configured, then what the page declares, then the fallback. |
| createMessages | (opts?) => Messages | Build a message catalogue. A partial set lays over the built-in British English one. |
| Messages | class | The catalogue itself. t(key, params) resolves one message; configure() replaces the set at runtime. |
| formatList | (items, locale?, type?) => string | Join a list the way the locale does, as a conjunction or a disjunction. |
Formulas and units
| Name | Signature | Description |
|---|---|---|
| evaluateFormula | (text, params?) => FormulaResult | Evaluate one expression. The same closed language the grid uses: no eval, no host access. |
| looksLikeFormula | (text) => boolean | Whether a pasted or typed value should be treated as a formula. |
| parseUnit | (text, opts?) => number | null | Read a value with a unit on it back to a number in the base unit. null when it will not parse. |
| formatUnit | (value, opts?) => string | The inverse: render a base-unit number on the ladder the column asked for. |
| UNIT_SYSTEMS | Record<string, UnitDescriptor[]> | Every registered unit system, by name. |
Statistic tiles
| Name | Signature | Description |
|---|---|---|
| deltaOf | (value, baseline) => object | The change between a value and its baseline, as a tile shows it. |
| toneOf | (direction, goodWhen) => string | Which way to colour a change, given whether a rise is good news. |
Licensing and modules
| Name | Signature | Description |
|---|---|---|
| licenceInfo | () => LicenceInfo | What the current key says: product, holder, expiry. licenseInfo is the same function under the American spelling. |
| licenceState | () => LicenceInfo | Whether the current host is licensed, and why not if it is not. licenseState is its alias. |
| registerModules | (modules, opts?) => void | Install optional modules once, for every grid on the page. |
| CONSOLE_ACTIVATION | string | The console incantation that activates a trial key. |
Ingesting rows, and menus
| Name | Signature | Description |
|---|---|---|
| ingestSync | (rows, plan?, opts?) => object | Build a column store and an inferred schema from raw rows, synchronously. The result records why each column got the type it did. |
| ContextMenu | class | The menu the grid opens on right-click, reusable for a menu of your own. open(p) places it. |
The charts module
| Name | Signature | Description |
|---|---|---|
| TYPES | readonly ChartType[] | Every chart type name createChart accepts. |
| SCHEMES | Record<string, readonly string[]> | The built-in colour schemes, by name. |
| PALETTE | readonly string[] | The default series colours. |
| registerScheme | (name, colours) => void | Add a colour scheme, or replace one of ours under the same name. |
| resolveScheme | (spec?) => object | Settle which scheme a chart will draw with. |
| setDefaultScheme | (name) => void | Change the scheme every chart uses unless it asks for another. |
| schemeNames | () => string[] | Every scheme name available, built-in and registered. |
The framework adapters
React, Vue and Svelte build their public surface from the same list, so an event becomes a prop or an emit without either side keeping a second copy.
| Name | Signature | Description |
|---|---|---|
| EVENT_NAMES | readonly string[] | Every event the grid declares. Mirrors the EventName union, and the build fails if the two diverge. |
| handlerName | (event) => string | The React prop for an event: cell:changed becomes onCellChanged. |
| dashedName | (event) => string | The Vue and Svelte listener name: cell:edit:start becomes cell-edit-start. |
The web component
| Name | Signature | Description |
|---|---|---|
| defineLatticeGrid | (tag?) => void | Register <lattice-grid>, or your own tag name. |
| createLatticeGridElement | (deps?) => class | Build the element class without registering it, for a custom registry. |
| GridElementController | class | The controller behind the element, if you are wrapping it yourself. |
| TAG_NAME | string | The default tag, lattice-grid. |
| EVENT_PREFIX | string | What DOM events are prefixed with. |
| ATTRIBUTE_CONFIG | Readonly<Record<string, unknown>> | Which attributes map to which configuration keys. |
| observedAttributeNames | () => string[] | The attributes the element reacts to. |
| domEventName | (event) => string | The DOM event name a grid event is dispatched under. |
| defineLatticeElements | (opts?) => Record<string, class> | Register one element per viewer from the factories you pass, under prefix (lattice- by default). Safe to call twice. See Web Components. |
| createLatticeElements | (deps?) => Record<string, class> | The classes, without registering them. |
| ELEMENT_TAGS | Readonly<Record<string, string>> | The tag each viewer takes, before the prefix. |
| createLatticeKPIElement | ({ createKPI }) => class | The <lattice-kpi> class on its own. |
| createLatticeChartElement | ({ createChart }) => class | The <lattice-chart> class on its own. |
| createLatticeKanbanElement | ({ createKanban }) => class | The <lattice-kanban> class on its own. |
| createLatticeGanttElement | ({ createGantt }) => class | The <lattice-gantt> class on its own. |
| createLatticeLayoutElement | ({ createLayout }) => class | The <lattice-layout> class on its own. |
| createLatticeTabsElement | ({ createTabs, createGrid? }) => class | The <lattice-tabs> class on its own. |
| createLatticeRouterElement | ({ createDataRouter }) => class | The <lattice-router> class on its own. |
| adoptLatticeStyles | (root, opts?) => () => void | Make a shadow root see Lattice's styles, and keep it seeing them as more are generated. Returns the undo. |
| latticeStyleSheets | (opts?) => CSSStyleSheet[] | Those stylesheets as constructable sheets, for a host managing its own adoptedStyleSheets. Shared and live; do not mutate them. |
The htmx module
| Name | Signature | Description |
|---|---|---|
| initWithin | (root) => Grid[] | Build every grid inside a fragment htmx just swapped in. |
| destroyWithin | (root) => void | Tear them down before the fragment goes. |
| gridElementsWithin | (root) => Element[] | The grid elements in a fragment, without building them. |
| rowsFromFragment | (fragment) => unknown[] | Read rows out of server-rendered markup. |
| rowsFromJson | (text) => unknown[] | Read rows out of a JSON payload. |
| ingestResponse | (grid, response) => void | Apply an htmx response to a grid, whichever of those two shapes it carries. |
| saveStateWithin | (root) => void | Persist the view state of every grid in a fragment before a swap. |
| restoreStateWithin | (root) => void | Put it back afterwards. |
| queryParams | (grid) => Record<string, string> | The grid's sort, filter and page as request parameters. |
| warnIfLargeHtmlPayload | (rows) => void | Warn once when a server-rendered payload is large enough that JSON would serve better. |
| QUERY_CHANGED_EVENT | string | Dispatched when the grid's query changes, for htmx to trigger on. |
| SCROLL_NEAR_END_EVENT | string | Dispatched as the viewport nears the end, for infinite scroll. |
| HTML_ROW_WARNING_THRESHOLD | number | The row count that warning fires at. |
The devtools module
| Name | Signature | Description |
|---|---|---|
| expose | (grid, name?) => void | Put a grid on globalThis under a name, so a console session can reach it. |
Windowed aggregates
The primitives behind grid.statistics.windowed, for a host that drives a live stream itself: a sliding window over timestamped values that re-reduces on demand and stamps every figure with the window it covered. Reduce over the last N ticks, the last N minutes, or the whole session.
| Name | Signature | Description |
|---|---|---|
| openWindow | (opts, now?) => Window | Build a window from a spec: { kind: 'count', span } for the last N ticks, { kind: 'time', minutes } for the last N minutes, or { kind: 'session' } for everything since it opened. |
| Window | class | A sliding window. push(v, t?) adds a value, reduce(fn) returns one named aggregate stamped with the window in over, and aggregate() returns them all at once. |
| WINDOW_KINDS | readonly ('count' | 'time' | 'session')[] | The three window kinds a caller may ask for. |
const { openWindow, Window, WINDOW_KINDS } = await import('../packages/core/src/index.js');
// openWindow builds one of the three kinds; here, the last 3 ticks.
const live = openWindow({ kind: 'count', span: 3 });
for (const v of [10, 20, 30, 40]) live.push(v); // 10 is evicted; 20, 30, 40 remain
const avg = live.reduce('avg'); // (20 + 30 + 40) / 3 = 30, stamped with the window it covers
// A Window can be built directly too; a session window never evicts.
const session = new Window('session');
session.push(5);
session.push(15);
return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')}; session ${session.reduce('avg').value}`;
Anomaly detection
Flag the rows that do not belong. Interpretable statistics with a written-down cut, never a black box: a robust per-column outlier score, Tukey's fences, and multivariate distance from the joint centre. These are the pure kernels behind grid.statistics.anomalies(...) and the anomalyScore/anomalyFlag shadow columns; a host can score a plain array the same way the grid scores a column.
| Name | Signature | Description |
|---|---|---|
| ANOMALY_METHODS | readonly ('modifiedZScore' | 'iqr' | 'mahalanobis')[] | The three methods a caller may ask for, named so a result can say which produced a flag. |
| modifiedZScores | (values, opts?) => { median, mad, threshold, scores, flags, flagged } | Per-row robust outlier score, 0.6745·(x − median)/MAD, flagged past threshold (default 3.5). Built on the median and MAD, so one wild reading cannot inflate the spread and hide - the masking effect that fools an ordinary z-score. A non-finite reading and a zero-MAD column yield a null score, not an invented one. |
| iqrFences | (values, opts?) => { q1, q3, iqr, lower, upper, k } | null | Tukey's fences, [Q1 − k·IQR, Q3 + k·IQR] (default k = 1.5), the same fence the box plot draws, on R type 7 quartiles. |
| mahalanobis | (matrix, opts?) => { center, df, cutoff, singular, used, distances, squared, flags, flagged } | null | Distance of every row from the joint centre in the metric of the data's own covariance, cut at a χ² quantile (default the 0.975 point). Catches a row impossible only in combination - heavy and short - that a per-column scan misses. A row with any missing coordinate is left unplaced; a singular covariance is ridge-regularised and reported as singular rather than throwing. |
| ROLLING_ANOMALY_METHODS | readonly ('rollingModifiedZScore' | 'rollingIqr')[] | The rolling (windowed) methods, named alongside ANOMALY_METHODS so a caller can enumerate every detector. Also accepted by grid.statistics.anomalies({ method, windowLen }). |
| rollingAnomalies | (values, opts?) => { method, windowLen, minPeriods, threshold, k, scores, flags, flagged } | Rolling (windowed) detection: judge every reading against a causal trailing window of windowLen ending at it, so a spike is caught against its recent neighbours and a drift never poisons a global baseline. rollingModifiedZScore (median + MAD) or rollingIqr (Tukey fences). With a window as long as the series the last point's score equals the static modifiedZScores one. |
| anomalyCondition | (opts) => (rows) => false | { method, field, flagged } | Build a Data Router alert condition from a detector: a (rows) => signal the router's existing router.alert(value, condition, handler) drives, so live anomaly monitoring reuses the router's partitioning, debounce and rising-edge re-arm rather than duplicating an alert engine. Reads one numeric field per row; latest: true signals only when the newest reading is the anomaly. |
const { modifiedZScores, iqrFences, mahalanobis, ANOMALY_METHODS } = await import('../packages/core/src/index.js');
// Univariate: the robust modified z-score flags the 500 among steady readings,
// where the mean and standard deviation an ordinary z uses would be dragged up
// by the outlier until it no longer looked like one.
const z = modifiedZScores([20, 21, 19, 20, 21, 19, 20, 500]);
// Tukey's fences say the same, drawn the way a box plot draws them.
const fence = iqrFences([20, 21, 19, 20, 21, 19, 20, 500]);
// Multivariate: height and weight move together; the last person is tall but
// very light, so the pair is impossible even though neither number is extreme.
const hw = [];
for (let i = 0; i < 12; i++) hw.push([160 + i, 60 + i]);
hw.push([182, 45]);
const m = mahalanobis(hw); // the χ² cut flags the off-line row
return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.upper}; joint ${m.flags[12]}`;
Rolling (windowed) detection and the Data Router bridge: judge each reading against its trailing window for live monitoring, and turn any detector into a router.alert(...) condition without a second alert engine.
const { rollingAnomalies, anomalyCondition, ROLLING_ANOMALY_METHODS } = await import('../packages/core/src/index.js');
// A steady stream that suddenly spikes. The trailing window catches the spike
// against its recent neighbours, where scoring against the whole run could let a
// long earlier drift hide it.
const stream = [10, 11, 9, 10, 11, 9, 10, 11, 9, 10, 40];
const roll = rollingAnomalies(stream, { windowLen: 6, minPeriods: 4 });
// The SAME detector, wired as a Data Router alert condition: (rows) => signal,
// exactly the predicate router.alert(value, condition, handler) already drives - // no second alert engine. `latest` fires only when the newest tick is the one out.
const condition = anomalyCondition({ field: 'temp', method: 'rollingModifiedZScore', orderBy: 't', windowLen: 6, minPeriods: 4, latest: true });
const rows = stream.map((temp, t) => ({ t, temp }));
const signal = condition(rows);
return `methods ${ROLLING_ANOMALY_METHODS.length}; spike ${roll.flags[10]}; alert ${signal ? signal.flagged.length : 0}`;
Forecasting
Project an ordered series forward, and carry a prediction band where a defensible closed form exists. Five methods behind one entry point: movingAverage and ses are flat forecasts (the trailing-window mean, the final smoothed level); holt adds a projected trend, holtWinters a projected trend and an additive seasonal; linear extrapolates an ordinary least-squares fit of the time axis. The exponential-smoothing bands are the innovations state-space forecast variances (Hyndman & Athanasopoulos) at the normal quantile; the linear and moving-average bands are the exact Student-t intervals, and linear also reports the narrower mean-response (confidence) band a trendline draws. These are the pure kernels the chart trendline overlay and the time-series grid forecast from; a host can forecast a plain array the same way.
| Name | Signature | Description |
|---|---|---|
| FORECAST_METHODS | readonly ('movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear')[] | The five methods a caller may ask for, named so a result can say which produced it. |
| forecast | (seq, opts?) => ForecastResult | null | Forecast an ordered series opts.horizon steps ahead by opts.method (default linear), at opts.confidence (default 0.95). Accepts a plain array of numbers (index is the time axis) or {at, value} rows. A smoothing factor absent from opts (alpha/beta/gamma) is fit by minimising the in-sample one-step SSE; holtWinters needs opts.period (≥ 2) and two whole periods of data. Each point carries mean and, where a band applies, lower/upper; linear adds lowerMean/upperMean. Null when the series is too short for the method. |
const { forecast, FORECAST_METHODS } = await import('../packages/core/src/index.js');
// Linear: fit the time axis, project one step, and carry the prediction band.
const lin = forecast([{ at: 1, value: 2 }, { at: 2, value: 4 }, { at: 3, value: 5 }, { at: 4, value: 4 }, { at: 5, value: 5 }], { method: 'linear', horizon: 1 });
const p = lin.points[0];
// Holt-Winters additive: a level, a trend and a two-step season, projected two
// steps ahead - the seasonal swing is carried into the forecast, not smoothed away.
const hw = forecast([10, 20, 30, 40], { method: 'holtWinters', period: 2, alpha: 0.5, beta: 0.5, gamma: 0.5, horizon: 2 });
return `methods ${FORECAST_METHODS.length}; next ${p.mean.toFixed(1)}; r2 ${lin.r2.toFixed(1)}; band ${p.lower.toFixed(2)}..${p.upper.toFixed(2)}; season ${hw.points[1].mean}`;
The same forecast off the standard stats surface: grid.statistics.forecast(colId, opts) reads the column over the filtered rows - ordered by opts.by when the time axis matters, exactly as grid.statistics.series(...) orders - and returns the same ForecastResult, so a host reaches a forecast the way it reaches grid.statistics.anomalies(...) rather than assembling the series itself.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }],
rows: [[1, 2], [2, 4], [3, 5], [4, 4], [5, 5]].map(([t, v], i) => ({ id: String(i), t, v })),
rowKey: 'id',
});
// Forecast the 'v' column one step ahead, ordered by 't', with the prediction band.
const f = grid.statistics.forecast('v', { by: 't', method: 'linear', horizon: 1 });
const p = f.points[0];
return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.lower > 0}`;
Type reference
Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.
The grid instance
The object `createGrid` returns, and the sub-APIs it exposes. Each surface lists its properties, its methods and the events it raises.
Grid
Properties
| Property | Type | Description |
|---|---|---|
| rows | RowsApi | The data: reading it, changing it, walking it. (read-only) |
| columns | ColumnsApi | The columns: order, width, visibility, grouping and pivoting. (read-only) |
| selection | SelectionApi | What is selected, and the range the user has marked. (read-only) |
| filters | FiltersApi | The filter tree, however it was set. (read-only) |
| sort | SortApi | The sort, in priority order. (read-only) |
| edit | EditApi | Editing sessions: starting, committing and cancelling them. (read-only) |
| scroll | ScrollApi | Where the viewport is, and moving it. (read-only) |
| export | ExportApi | CSV, Excel and clipboard. (read-only) |
| import | ImportApi | Bringing rows in from CSV/TSV text, a file, the clipboard or a drop. (read-only) |
| state | StateApi | Everything the user arranged, as a serialisable object. (read-only) |
| overlay | OverlayApi | The loading, empty and error surfaces drawn over the grid. (read-only) |
| history | HistoryApi | Undo and redo over edits and structural changes. (read-only) |
| views | ViewsApi | Saved arrangements the user can switch between. (read-only) |
| diff | DiffApi | What changed against a baseline, cell by cell. (read-only) |
| permissions | PermissionsApi | Who may see, edit and export what. (read-only) |
| ai | AiApi | A machine-readable description of the grid, for a model to read. (read-only) |
| messages | MessagesApi | Translation: the catalogue and the active locale. (read-only) |
| licence | LicenceApi | Licence state, and setting a key after construction. (read-only) |
| pagination | PaginationApi | Pages, where the grid is paged rather than scrolled. (read-only) |
| highlight | HighlightApi | Transient emphasis on a row, column or cell. (read-only) |
| find | FindApi | In-grid find: locate text without filtering, and step through the matches. (read-only) |
| redaction | RedactionApi | Values hidden from view and from export. (read-only) |
| annotate | AnnotationApi | Drawing over the grid, where the module is installed. (optional) |
| presentation | PresentationApi | Full screen, scaling and chrome suppression. (read-only) |
| pivotView | PivotViewApi | Expand and collapse the pivot presentation's axes; the state a view carries. (read-only) |
| updates | UpdatesApi | The live feed: pausing it, flushing it, and what it has done. (read-only) |
| timeline | TimelineApi | Replaying the changes the grid has seen. (read-only) |
| crossFilter | CrossFilter | Cross-filtering, a derived grid filtering the grid it derives from. (read-only) |
| facets | FacetsApi | Header distributions, and the filters clicking one creates. (read-only) |
| detail | DetailApi | The expandable panel beneath a row. (read-only) |
| comments | CommentsApi | Threads attached to rows and cells. (read-only) |
| presence | PresenceApi | Who else is looking, and where. (read-only) |
| diagnostics | DiagnosticsApi | What the grid is doing, for when it is doing it slowly. (read-only) |
| statistics | StatisticsApi | Reductions, profiles, correlations, capability and intervals. (read-only) |
| formatting | FormattingApi | Formatting a value as the grid would, outside a cell. (read-only) |
| validation | ValidationApi | Declarative column validation: why a write was refused, and clearing marks. (read-only) |
| maximise | MaximiseApi | Full-screen control, where it is enabled. (read-only, optional) |
| element | HTMLElement | null | The element you passed to `createGrid`, not the grid's own root. The grid builds its `.lattice` root *inside* that element, so `el.closest('.lattice')` never matches this, and a theme attribute set on it has no effect, the theme is read from the root within. Use `element.querySelector('.lattice')` for the grid's own root. (read-only) |
| destroyed | boolean | Whether `destroy` has run. Every other member is inert afterwards. (read-only) |
| ready | boolean | False until the first render has been laid out. (read-only) |
| form | RowFormApi | The row form. Declines when `rowForm` is not configured. (read-only) |
| icons | IconRegistryApi | The grid's icon registry, read-only. The same sprite set `registerIcon` writes to and every cell paints from, reachable from the grid instance so that code outside the grid bundle - an optional module drawing its own glyph, a network chart putting a `router` on a node - draws from the one registry rather than a second, empty copy of it. Register with `registerIcon` or `config.icons`, as before. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| capture | (opts?: CaptureOptions): Promise<Blob> | opts?: CaptureOptions | Promise<Blob> | An image of the grid as drawn, where the module is installed. (optional) |
| config | (): GridConfig | - | GridConfig | The resolved configuration, as one object. |
| get | <K extends keyof GridConfig>(key: K): GridConfig[K] | key: K | GridConfig[K] | One configuration value, as it stands after defaults and validation - not what was passed in. Typed by the key, so `get('rowHeight')` is a number without a cast. |
| set | <K extends keyof GridConfig>(key: K, value: GridConfig[K]): void | key: Kvalue: GridConfig[K] | void | Write one configuration value, doing only the work that key implies. Every key is settable at runtime - there is no "initial options" versus "live options" distinction to learn - and `config:changed` follows, after the grid has rebuilt. |
| setAll | (values: Partial<GridConfig>): void | values: Partial<GridConfig> | void | Apply several configuration changes as one update rather than several. |
| on | (event: EventName, handler: EventHandler): Unsubscribe | event: EventNamehandler: EventHandler | Unsubscribe | Listen. Returns the function that stops listening. |
| once | (event: EventName, handler: EventHandler): Unsubscribe | event: EventNamehandler: EventHandler | Unsubscribe | Listen until it fires once. |
| off | (event: EventName, handler: EventHandler): void | event: EventNamehandler: EventHandler | void | Stop listening. |
| emit | (event: string, payload?: Record<string, unknown>): void | event: stringpayload?: Record<string, unknown> | void | Raise an event of your own on the grid's bus. |
| setPinnedRows | (rows: unknown[], opts?: { edge?: 'top' | 'bottom' }): void | rows: unknown[]opts?: { edge?: 'top' | 'bottom' } | void | Pin rows above or below the scrolling body. The rows render through the ordinary column pipeline but are not part of the data: not counted, sorted, filtered, grouped, selectable or exported. Pass a new array rather than mutating the one you passed before: array identity is how the grid knows the pinned rows have changed. |
| getPinnedRows | (opts?: { edge?: 'top' | 'bottom' }): unknown[] | opts?: { edge?: 'top' | 'bottom' } | unknown[] | The objects currently pinned at one edge, as a copy. |
| getVersion | (): string | - | string | The library version. |
| destroy | (): void | - | void | Release everything: listeners, timers, workers and the DOM the grid made. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| ready | The grid has finished building and every API on it is ready to call; fires once, on the frame after `createGrid` returns. | no payload | no |
| destroy | `grid.destroy()` was called and is about to release everything, so a handler can still read the grid one last time. | no payload | no |
| render:first | The renderer has written its first frame into the host element. | no payload | no |
| render:done | A render pass has finished writing cells: the row window it drew, what caused the pass, and how long each phase took. | RenderDoneEvent | no |
| config:changed | A configuration key was written at run time through `grid.set(key, value)` or `grid.setAll(values)`, after the grid rebuilt. | ConfigChangedEvent | no |
| model:changed | The display model was rebuilt - rows reloaded, the tree re-flattened, a page fetched, a query re-run - with `reason` naming which. | ModelChangedEvent | no |
| source:error | A source could not fetch what was asked of it: a page, a group's children, a tree branch, or the stream itself. | SourceErrorEvent | no |
| source:total | A source that delivered its rows before counting them has finished counting; the exact total is in the payload. | SourceTotalEvent | no |
| stream:chunk | A streaming source applied a chunk of arriving rows. | StreamChunkEvent | no |
| stream:end | A streaming source reached the end of its feed; `promoted` says whether it handed over to an in-memory source. | StreamEndEvent | no |
| stream:evicted | A rolling-window stream dropped rows off the back of its window to stay inside its limit. | StreamEvictedEvent | no |
| rowDrag:started | A row drag passed the drag threshold and began, on the grid the row was picked up in. | RowDragEvent | no |
| rowDrag:moved | The pointer moved during a row drag, coalesced to one event per animation frame. | RowDragEvent | no |
| rowDrag:left | The pointer left a grid it had been dragging over; `over` names the grid just left. | RowDragEvent | no |
| rowDrag:ended | The row drag ended - released anywhere, inside a grid or outside every one; `dropped` says whether it is being acted on. | RowDragEvent | no |
| cell:changed | A cell's value was written: by an edit commit, by a revert, or by an undo/redo step. | CellChangedEvent | no |
| cell:pending | An optimistic cell edit was sent to the transport and is awaiting the server's answer. | CellPendingEvent | no |
| cell:confirmed | The server accepted a pending cell edit; `value` is what it confirmed, which may not be what was sent. | CellConfirmedEvent | no |
| cell:reverted | A pending cell edit was refused and the previous value put back. | CellRevertedEvent | no |
| cell:conflict | The server accepted a pending cell edit but returned a row that disagrees with what the grid holds. | CellConflictEvent | no |
| cell:clicked | A cell was clicked (primary button, single click). | CellPointerEvent | no |
| cell:dblclicked | A cell was double-clicked. | CellPointerEvent | no |
| cell:contextmenu | A context menu was requested on a cell, by the pointer or by the keyboard's menu key. | CellContextMenuEvent | no |
| cell:mouseover | The pointer entered a cell; crossing between two children of one cell is not a re-entry. | CellPointerEvent | no |
| cell:mouseout | The pointer left a cell; crossing between two children of one cell is not a departure. | CellPointerEvent | no |
| cell:mousedown | A pointer button was pressed on a cell, before any click is resolved. | CellPointerEvent | no |
| cell:mouseup | A pointer button was released on a cell. | CellPointerEvent | no |
| cell:edit:start | A cell editor opened, by double-click, by Enter, or by typing into the cell. | EditStartEvent | no |
| cell:edit:end | A cell editor closed: committed, cancelled, or refused by validation - `valid` and `cancelled` say which. | EditEndEvent | no |
| group:toggled | A group row was expanded or collapsed - one group, one branch, or all of them at once. | GroupToggledEvent | no |
| pivot:drill | A pivot measure cell was drilled into; the payload names the row and column paths behind it. | PivotDrillEvent | no |
| columngroup:changed | A banded header group was formed, renamed, moved, dissolved, removed or restored from state. | ColumnGroupChangedEvent | no |
| header:contextmenu | A context menu was requested on a column header. | HeaderContextMenuEvent | no |
| range:changed | The selected cell ranges changed. | RangeChangedEvent | no |
| clipboard:copy | A copy to the clipboard was attempted; `ok` says whether it reached the clipboard. | ClipboardCopyEvent | no |
| page:changed | The page or the page size changed. | PageChangedEvent | no |
| size:changed | The host element's box changed size, as reported by the `ResizeObserver` the grid watches it with. | no payload | no |
| toolpanel:focus | The keyboard asked for focus to move to the tool panel (Ctrl+Alt+P). | no payload | no |
| tree:loading | A tree branch was expanded and `tree.loadChildren` was called for it. | TreeLoadingEvent | no |
| tree:loaded | A tree branch's children arrived and were added. | TreeLoadedEvent | no |
| tree:loadFailed | A tree branch's `loadChildren` rejected; the branch is left unloaded so it can be retried. | TreeLoadFailedEvent | no |
| tree:loadAborted | A tree branch was collapsed before its children arrived, so the fetch was abandoned. | TreeLoadAbortedEvent | no |
| annotation:changed | The annotation overlay's marks changed: one was drawn, moved or erased, or the tool changed. | AnnotationChangedEvent | no |
| shortcuts:opened | The keyboard-shortcuts overlay was opened. | no payload | no |
| shortcuts:closed | The keyboard-shortcuts overlay was closed. | no payload | no |
| print:before | Print mode has been applied and the grid laid out un-virtualised, just before the print dialog. | PrintEvent | no |
| print:after | The print dialog has returned and print mode has been undone. | PrintEvent | no |
| beforeColumnMove | A user column move is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeColumnMoveEvent | yes |
| beforeColumnResize | A user column resize is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeColumnResizeEvent | yes |
| beforeColumnHide | A user column hide is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeColumnHideEvent | yes |
| beforeSelect | A user selection change is about to be announced; call `preventDefault(reason?)` to snap it back. | BeforeSelectEvent | yes |
| beforeRowAdd | A user row append is about to be sent; call `preventDefault(reason?)` to stop it. | BeforeRowAddEvent | yes |
| beforeDelete | A user row delete is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeDeleteEvent | yes |
| beforeRowMove | A user row reorder is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeRowMoveEvent | yes |
| beforeGroup | A user group expand or collapse is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeGroupEvent | yes |
| beforeRowReceive | A row dragged from another grid is about to be inserted here; call `preventDefault(reason?)` to refuse it. | BeforeRowReceiveEvent | yes |
| columnMove:cancelled | A `beforeColumnMove` handler vetoed the move. | ColumnMoveCancelledEvent | no |
| columnResize:cancelled | A `beforeColumnResize` handler vetoed the resize. | ColumnResizeCancelledEvent | no |
| columnHide:cancelled | A `beforeColumnHide` handler vetoed the hide. | ColumnHideCancelledEvent | no |
| rowAdd:cancelled | A `beforeRowAdd` handler vetoed the append. | RowAddCancelledEvent | no |
| delete:cancelled | A `beforeDelete` handler vetoed the delete, or the rows were gone by the time an async handler settled. | DeleteCancelledEvent | no |
| rowMove:cancelled | A `beforeRowMove` handler vetoed the reorder. | RowMoveCancelledEvent | no |
| group:cancelled | A `beforeGroup` handler vetoed the expand or collapse. | GroupCancelledEvent | no |
| rowReceive:cancelled | A `beforeRowReceive` handler refused the drop, or the drop went stale while an async handler was thinking. | RowReceiveCancelledEvent | no |
| * | Every event above, delivered to one handler; the payload is whichever event fired. | GridEvent | no |
RowsApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| load | (rows: unknown[]): void | rows: unknown[] | void | Replace the data. Sort, filters, grouping and column layout are kept. |
| apply | (change: RowChange): ChangeResult | change: RowChange | ChangeResult | Apply an incremental change - `add`, `update`, `remove` - against the rows already loaded, synchronously, and return what it touched. Rows the grid could not place come back under `rejected` rather than throwing. Needs a `rowKey`: without one, updates and removals cannot be matched to a row, and it says so. |
| queue | (change: RowChange): Promise<ChangeResult> | change: RowChange | Promise<ChangeResult> | The same change, coalesced with everything else arriving inside the batch window (50 ms by default) into one pipeline run and one repaint. The promise resolves with the flushed result. This is the call for a live feed; an over-deep queue flushes early rather than growing. |
| get | (index: number): Row | undefined | index: number | Row | undefined | The row at a display index - the index the grid draws at, so group headings, footers and the grand total are counted. `undefined` past the end. |
| byKey | (key: string): Row | undefined | key: string | Row | undefined | The row with this key, wherever it sits, or `undefined` when the grid does not hold it. |
| count | (): number | - | number | How many rows the grid is displaying, including group headings, footers and the grand total, and including optimistic rows an in-flight write has added. |
| totalCount | (): number | null | - | number | null | Rows in the source before filtering; under pagination, across every page. `null` while a source is still counting - see {@link RowsApi.totalPending}. Reporting the rows fetched so far in that gap would show a page length as a dataset size. |
| totalPending | (): boolean | - | boolean | Whether an exact total is still being counted for the current query. True only between the rows arriving and the total landing, on a source that defers the count because counting reads data. It is what separates "not counted yet" from "never going to be counted": `totalCount()` is `null` for both, and only one of them is going to become a number. |
| matchCount | (): number | - | number | Data rows matching the filters, excluding group, footer and total rows. |
| coverage | (): StatCoverage | - | StatCoverage | How much of the data a figure computed from this grid covers, so a statistic over a windowed source can say it is approximate. |
| data | (): unknown[] | - | unknown[] | Your own row objects, in source order, with the grid's furniture left out - group and total rows are not in the data and never appear here. |
| forEach | (fn: (row: Row, index: number) => void): void | fn: (row: Row, index: number) => void | void | Visit every display row in order: filtered, sorted and grouped as drawn, with collapsed rows left out. |
| forEachAll | (fn: (row: Row, index: number) => void): void | fn: (row: Row, index: number) => void | void | Every row in the data, before any filter. Leaf rows, in physical order. |
| forEachExcept | (colId: string, fn: (row: Row, index: number) => void): void | colId: stringfn: (row: Row, index: number) => void | void | Visit the rows surviving every filter except one column's own: the faceting question, asked of the rows. |
| value | (key: string, colId: string): unknown | key: stringcolId: string | unknown | A cell's resolved value, stored or computed, read through the same path the renderer uses so the API and the cell beside it can never disagree. |
| text | (key: string, colId: string): string | key: stringcolId: string | string | A cell's display text - the value after the column's format and any lookup label, exactly as the cell shows it. |
| values | (key: string): Record<string, unknown> | key: string | Record<string, unknown> | Every column's resolved value for one row, keyed by column id. Columns the current permissions hide from reading are left out. |
| refresh | (opts?: { rows?: string[]; columns?: string[]; force?: boolean }): void | opts?: { rows?: string[]; columns?: string[]; force?: boolean } | void | Repaint without re-running the sort, filter and group stages, discarding the memoised `compute` results for the named rows and columns (all of them when none are named). `force: true` also recomputes and rewrites pure computed values already materialised into the store - the call to make when an answer the grid cannot see has changed, such as a lookup table that has just arrived. |
| move | (key: string, to: number): { moved: boolean; from: number; to: number; reason?: string } | key: stringto: number | { moved: boolean; from: number; to: number; reason?: string } | Move a row to another position in the data. Refuses, with a reason, while a sort, filter or grouping is active. |
| groupHeadings | (index: number): Row[] | index: number | Row[] | The group headings enclosing a display row, outermost first. Empty when the grid is not grouped. |
| leavesOf | (key: string): Row[] | key: string | Row[] | The leaf rows beneath a group heading: the members it counts in `leafCount`, as rows, so you can roll up a field the grid was never told to total. Filtered members in display order. Computed per call, so call it when you draw a group row rather than in a loop over every row. |
| expand | (key: string, deep?: boolean): void | key: stringdeep?: boolean | void | Open a group or tree row. Note that `deep` expands *every* group in the grid, not only this row's descendants. |
| collapse | (key: string): void | key: string | void | Close a group or tree row, hiding everything beneath it. |
| expandAll | (): void | - | void | Open every group and tree row. An explicit call outranks `groupDefaultExpanded`, so the next rebuild does not re-close them. |
| collapseAll | (): void | - | void | Close every group and tree row, leaving only the outermost headings on screen. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| rows:changed | Rows were added, updated, removed or moved. `identified: true` means the payload names exactly which rows moved. | RowsChangedEvent | no |
| rows:queued | A change arrived while the feed was being batched and was put on the queue instead of applied. | RowsQueuedEvent | no |
| rows:deferred | A flush ran out of its frame budget and carried the rest of the change into the next one. | RowsDeferredEvent | no |
| rows:paused | `grid.changes.pause()` held the feed: changes keep arriving and stop being applied. | RowsFlowEvent | no |
| rows:resumed | `grid.changes.resume()` released the feed and applied what had been held. | RowsFlowEvent | no |
| row:received | A row dragged from another grid was accepted into this one, on the receiving grid. | RowReceivedEvent | no |
| row:sent | A row was dragged out of this grid into another one and removed from here (a move, not a copy). | RowTransferEvent | no |
| row:copied | A row was dragged out of this grid into another one and kept here as well (a copy). | RowTransferEvent | no |
| row:moved | A row was reordered within this grid, from one display index to another. | RowMovedEvent | no |
| row:edit:start | A whole-row editor opened, the row-edit counterpart of `cell:edit:start`. | EditStartEvent | no |
| row:edit:end | A whole-row editor closed, the row-edit counterpart of `cell:edit:end`. | EditEndEvent | no |
| row:clicked | A row was clicked, alongside the `cell:clicked` for the cell under the pointer. | RowPointerEvent | no |
| row:dblclicked | A row was double-clicked, alongside the `cell:dblclicked` for the cell under the pointer. | RowPointerEvent | no |
| row:pending | An optimistic row append or delete was sent to the transport and is awaiting the server's answer. | RowPendingEvent | no |
| row:confirmed | The server accepted a pending row append or delete; an append is rekeyed from its temporary key first. | RowConfirmedEvent | no |
| row:reverted | A pending row append or delete was refused: the optimistic append is discarded, the tombstoned row restored. | RowRevertedEvent | no |
| row:conflict | The server accepted a pending row append or delete but returned a row that disagrees with what the grid holds. | RowConflictEvent | no |
ColumnsApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| setTotal | ( id: string, fn: TotalName | TotalFn | null, opts?: { scope?: 'group' | 'grand' }, ): void | id: stringfn: TotalName | TotalFn | nullopts?: { scope?: 'group' | 'grand' } | void | Set or clear a column's totals-row reduction. With no `scope`, `fn` becomes the column's single `total`, applied to both group subtotals and the grand total, and any independent group/grand overrides are cleared - the same one-property behaviour as before. Pass `scope: 'group'` or `scope: 'grand'` to set just that scope's reduction independently, leaving the other and the base `total` untouched; the scope that has no override falls back to `total`. |
| aggregates | (id: string): TotalName[] | id: string | TotalName[] | The aggregate names meaningful for a column, honouring its type's `totals.supported` declaration (). What the aggregate chooser offers. |
| distinct | (id: string): unknown[] | id: string | unknown[] | Every distinct value in a column, from the dictionary where there is one. |
| get | (id: string): ResolvedColumn | undefined | id: string | ResolvedColumn | undefined | A resolved column by id, including the generated auto-group column - which `all()` and `state()` deliberately leave out, since it is the grid's and not yours. `undefined` when there is no such column. |
| all | (): ResolvedColumn[] | - | ResolvedColumn[] | Every leaf column in display order, hidden ones included, minus any the current permissions withhold. |
| visible | (): ResolvedColumn[] | - | ResolvedColumn[] | The leaf columns actually on screen, in display order - `all()` without the hidden ones. |
| state | (): ColumnState[] | - | ColumnState[] | The serialisable column state - order, width, pin, visibility, sort, grouping, totals, decoration - ready to store and hand back to `apply()`. The generated auto-group column is excluded. |
| apply | (state: ColumnState[]): void | state: ColumnState[] | void | Restore column state produced by `state()`: order, widths, pins, visibility and the rest, applied in one pass. |
| tags | (): string[] | - | string[] | Every distinct column tag, in the order first declared. |
| showTagged | (tags?: string | string[] | null): string[] | tags?: string | string[] | null | string[] | Show only the columns carrying one of these tags. **Columns with no tags are never hidden.** Pass nothing to show every tagged column again. Returns the ids that were hidden. |
| activeTags | (): string[] | - | string[] | The tags currently being shown, empty when all are. |
| show | (ids: string | string[]): void | ids: string | string[] | void | Show columns by id. Recorded on the undo timeline. |
| hide | (ids: string | string[]): void | ids: string | string[] | void | Hide columns by id. `beforeColumnHide` can cancel it, and a column marked `layout.lockVisible` refuses and warns. |
| move | (id: string, to: number): void | id: stringto: number | void | Move a column to a display position. `beforeColumnMove` can cancel it; a column that is not movable or is position-locked refuses and warns. |
| groupColumns | (ids: string | string[], opts?: { title?: string; at?: number; groupId?: string; id?: string }): string | null | ids: string | string[]opts?: { title?: string; at?: number; groupId?: string; id?: string } | string | null | Wrap leaf columns in a banded header, or add them to an existing band. Header banding, not row grouping (see {@link group}); the band is a {@link ColumnGroup} node so a drag-, keyboard- or config-built band is the same tree, and it round-trips through a saved view. Emits `columngroup:changed`. Pass `groupId` to add to the band already carrying that id, or `id` to create a new band with a caller-chosen stable id you can reference later; `groupId` wins if both are given and an `id` already in use warns and no-ops. |
| ungroupColumn | (id: string): void | id: string | void | Take a leaf out of its band; a band emptied by the move is dissolved. |
| renameGroup | (groupId: string, title: string): void | groupId: stringtitle: string | void | Rename a banded header. |
| dissolveGroup | (groupId: string): void | groupId: string | void | Dissolve a band, returning its columns to the enclosing level in place. |
| moveGroup | (groupId: string, to: number): void | groupId: stringto: number | void | Move a whole band among its siblings, its columns travelling as a block. |
| pin | (id: string, side: Edge | null): void | id: stringside: Edge | null | void | Freeze a column against the start or the end edge, or pass `null` to return it to the scrolling body. Recorded on the undo timeline. |
| resize | (id: string, px: number): void | id: stringpx: number | void | Set a column's width in pixels, clamped to its `min` and `max`. A column marked `resizable: false` refuses and warns. An explicit width clears the column's `flex`, so the next layout pass does not undo it. |
| decorate | (id: string, decoration: DecorationName | DecorationSpec | null, opts?: { variant?: VariantSpec }): void | id: stringdecoration: DecorationName | DecorationSpec | nullopts?: { variant?: VariantSpec } | void | Set, change or clear a column's decoration at runtime (). Pass `null` to clear it back to plain text. Presentation config: it is not on the undo timeline and is not carried in a saved view - use `grid.formatting` for durable, view-persisted conditional styling. |
| autoSize | (ids?: string | string[]): void | ids?: string | string[] | void | Size the named columns (or all of them) to the content they are actually showing, heading included. It measures the rows the renderer has mounted rather than the whole dataset, and settles any pending frame first so a call made straight after `rows.load()` sees the rows and not just the header. |
| fit | (): void | - | void | Size the visible resizable columns so that every column the grid draws, together, exactly fills the width the cells occupy: the body viewport's client width at the moment of the call, which excludes the vertical scrollbar when the grid draws one and is the full inner width when it does not. Columns it does not size keep their width and are taken out of that width first: `resizable: false` columns and the grid's own selection checkbox, detail expander, group and tree columns. The rest share what is left in proportion to their current widths, within each `min`/`max`. If that leaves less than their minimums, each is set to its minimum - the 40px floor when a column's own `min` does not set one of its own, an explicit `min: 0` included - never below, the grid scrolls horizontally, and a `[lattice]` warning says so. Rows given to `createGrid` or `rows.load()` before the call are counted. One-shot: it sets fixed widths once (a `flex` column included) and does not follow later changes; after a resize, rows arriving later bring a vertical scrollbar in, or a column group is expanded or un-grouped, call it again. |
| group | (ids: string | string[]): void | ids: string | string[] | void | Set the row-grouping columns, outermost first; pass an empty list to ungroup. Columns the current permissions withhold are dropped. Emits `column:grouped` and rebuilds the header, since grouping adds or removes the generated group column. |
| pivot | (ids: string | string[]): void | ids: string | string[] | void | Set the pivot columns, turning each distinct value into a column heading; pass an empty list to leave pivot mode. Columns the current permissions withhold are dropped. Emits `column:pivoted`. |
| totals | (ids: string | string[]): void | ids: string | string[] | void | Choose which columns carry a footer total. A column named here that has none is given `sum`; a column not named falls back to whatever its own definition asked for. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| column:moved | A column was moved to a different display position. | ColumnMovedEvent | no |
| column:resized | A column's width changed, by a header drag or by `grid.columns.resize()`. | ColumnResizedEvent | no |
| column:visible | Columns were shown or hidden. | ColumnVisibleEvent | no |
| column:pinned | A column was pinned to a side, or unpinned. | ColumnPinnedEvent | no |
| column:grouped | The row grouping changed: which columns the rows are grouped by. | ColumnGroupedEvent | no |
| column:pivoted | The pivot changed: which columns the rows are pivoted by, locally or pushed down to the backend. | ColumnPivotedEvent | no |
| column:filter:open | The header's filter affordance was activated and the column's filter popup should open. | ColumnMenuEvent | no |
| column:profile:open | The column menu's profile item was activated and the column's profile should open. | ColumnMenuEvent | no |
| column:menu:open | The header's menu affordance was activated and the column menu should open. | ColumnMenuEvent | no |
| columns:changed | The column set changed other than by moving, resizing, hiding or pinning - a type inference pass rewrote it. | ColumnsChangedEvent | no |
| columns:tagged | `grid.columns.showTagged()` chose which columns to show from their tags. | ColumnsTaggedEvent | no |
SelectionApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| clearRange | (): void | - | void | Drop every range, leaving the row and cell selection alone. |
| summary | (): { count: number; numeric: number; sum: number | null; min: number | null; max: number | null; avg: number | null; } | - | { count: number; numeric: number; sum: number | null; min: number | null; max: number | null; avg: number | null; } | The selected cells as a status bar states them: how many carry a value, how many of those are numbers, and the sum, extremes and mean of the numbers. Every figure but the two counts is null when nothing selected is numeric. {@link SelectionApi.statistics} is the fuller answer. |
| statistics | (): object | null | - | object | null | Everything worth knowing about the selected cells: what `summary()` reports plus median, quartiles, deviation, distinct and outliers. Over the cells rather than a column, so a rectangle spanning three columns is one set of numbers. Null with nothing selected. |
| rows | (): Row[] | - | Row[] | The selected rows, as row objects. |
| keys | (): string[] | - | string[] | The keys of the selected rows. |
| set | (keys: string[]): void | keys: string[] | void | Replace the row selection with exactly these keys and repaint. |
| all | (): void | - | void | Select every row currently on display - what the filters leave, detail rows excepted. Does nothing unless the selection mode is `'multiple'`. |
| clear | (): void | - | void | Drop the row selection, leaving any cell ranges alone. |
| headerState | (): boolean | 'partial' | - | boolean | 'partial' | The select-all checkbox's tri-state over the displayed rows: `true` when every one is selected, `'partial'` when some are, `false` when none are. Group and detail rows are furniture and do not count. |
| cells | (): { key: string; colId: string }[] | - | { key: string; colId: string }[] | Every cell inside the selected ranges, as row key and column id pairs. |
| ranges | (): CellRange[] | - | CellRange[] | The selected cell ranges, in the order they were added. |
| setRange | (range: CellRange): void | range: CellRange | void | Replace the range selection with this one range. |
| addRange | (range: CellRange): void | range: CellRange | void | Add a range without discarding the ones already selected - the API form of ctrl-clicking a second block. The added range becomes the anchor a following `extendRange` grows from. |
| startRange | (rowIndex: number, colId: string, opts?: { additive?: boolean }): void | rowIndex: numbercolId: stringopts?: { additive?: boolean } | void | Anchor a new range at a cell, which a drag or Shift+Arrow then extends. `additive: true` keeps the ranges already selected. |
| extendRange | (rowIndex: number, colId: string): void | rowIndex: numbercolId: string | void | Grow the live range out to a cell, leaving its anchor where it was. |
| corner | (): { row: number; colId: string } | null | - | { row: number; colId: string } | null | The bottom-right corner of the last range - where the fill handle sits - or `null` when nothing is selected. |
| inRange | (rowIndex: number, colId: string): boolean | rowIndex: numbercolId: string | boolean | Whether a cell falls inside any selected range. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| selection:changed | The row selection changed and was accepted (a `beforeSelect` veto raises `selection:cancelled` instead). | SelectionChangedEvent | no |
| selection:cancelled | A `beforeSelect` handler vetoed the selection change, which has been snapped back. | SelectionCancelledEvent | no |
FiltersApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| quickState | (): { text: string; mode: string } | - | { text: string; mode: string } | The quick filter's text and match mode, for restoring a control. |
| get | (): FilterSet | - | FilterSet | The filter tree in force - the structured conditions, not the quick filter or the `where` predicates. |
| set | (filters: FilterSet): void | filters: FilterSet | void | Replace the filter tree. `beforeFilter` may cancel it. A condition on a column the user may not read is dropped, since the row count alone would leak the value; a condition naming an operator the grid does not have is dropped too, and if that leaves nothing at all the call is a no-op rather than a clear - a typo must not quietly widen a row set someone had narrowed. |
| clear | (): void | - | void | Drop the condition tree, the quick filter, and every `where` predicate that was not registered `{ pinned: true }`. |
| quick | (text: string): void | text: string | void | Set the quick filter text, which matches against what the cells display. `mode` chooses how - `'contains'` (the default), `'words'`, `'fuzzy'` or `'regex'` - and persists until changed, so a host can set it once and pass text alone afterwards. `beforeFilter` may cancel it, and keystrokes coalesce into one undo entry. |
| where | (): string[] | - | string[] | The names of the `where` predicates in force, in registration order. |
| where | (name: string, predicate: ((row: any) => boolean) | null, opts?: WhereOptions): void | name: string - the name to register underpredicate: ((row: any) => boolean) | null, opts?: WhereOptions - the predicate, or null to remove it | void | Register, replace or remove a named row predicate composed with the filter set. Registering *is* activating: there is no companion "a predicate is present" flag to keep in sync, which is the failure mode this replaces. Several may be in force at once under their own names, ANDed with each other and with the declarative set, and removing one leaves the rest alone. The predicate is handed the **data row**. grid.filters.where('visibleToMe', row => row.owner === me); grid.filters.where('rateKnown', row => rates.has(row.ccy), { deps: ['ccy'], pinned: true }); grid.filters.where('visibleToMe', null); // remove Only the names reach `filters.get()` and `state.get()`; the functions never do. |
| reapply | (name?: string): boolean | name?: string | boolean whether anything was re-run | Re-run `where` predicates whose inputs changed where the grid could not see it - a rate table that arrived late, a permission set that refreshed. The out-of-band half of re-evaluation; `deps` is the half the grid observes for itself. Together they replace the manual "filter again" call. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| filter:changed | The filters changed: a structured condition, the quick filter's text, or a named host predicate. | FilterChangedEvent | no |
| beforeFilter | A user filter - structured or quick - is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeFilterEvent | yes |
| filter:cancelled | A `beforeFilter` handler vetoed the filter. | FilterCancelledEvent | no |
SortApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| get | (): SortEntry[] | - | SortEntry[] | The sort entries in force, outermost first. A copy - changing it sorts nothing. |
| set | (entries: SortEntry[]): void | entries: SortEntry[] | void | Replace the sort model; an entry naming no known column is dropped with a warning. |
| clear | (): void | - | void | Remove every sort entry and return the rows to their source order. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| sort:changed | The sort order changed, through `grid.sort.set()` or a header click. | SortChangedEvent | no |
| beforeSort | A user sort is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeSortEvent | yes |
| sort:cancelled | A `beforeSort` handler vetoed the sort. | SortCancelledEvent | no |
EditApi
Properties
| Property | Type | Description |
|---|---|---|
| pastePreview | boolean | Whether a bulk paste is previewed before it commits (`edit.pastePreview`,). (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| start | (key: string, colId: string): boolean | key: stringcolId: string | boolean | Open an editor on a cell. `false` when the cell is not editable, or is held by another peer under an advisory lock. |
| stop | (cancel?: boolean): void | cancel?: boolean | void | End the edit session, committing by default or discarding with `cancel: true`. A commit runs the full write path - parse, validate, apply - and records the change on the undo timeline. |
| undo | (): void | - | void | Undo the last action. Routed through the grid-wide timeline rather than an edit-only stack: with two stacks the first press after a sort would undo an edit made before it, which is not what the user did last. |
| redo | (): void | - | void | Redo the last undone action, through the same grid-wide timeline. |
| setCells | ( writes: { key: string; colId: string; value: unknown }[], type?: 'cell' | 'fill' | 'paste', opts?: { origin?: 'api' | 'ai' | 'user' }, ): number | Promise<number> | writes: { key: string; colId: string; value: unknown }[]type?: 'cell' | 'fill' | 'paste'opts?: { origin?: 'api' | 'ai' | 'user' } | number | Promise<number> | Write several cells as one undoable step (). `opts.origin` defaults to `'api'` - the ungated seam every existing caller uses (a fill, a paste, a kanban move), unchanged. Pass `{ origin: 'ai' }` (or `'user'`) to route the write through the cancellable `beforeEdit` gate, exactly as an interactive edit is: the AI writes through this so a host `beforeEdit` handler can veto it and nothing persists when it does. With a gated origin and an async (deferring) before-handler, the return is a `Promise<number>`. |
| bulkSet | (value: unknown, opts?: { cells?: { key: string; colId: string }[] }): number | value: unknownopts?: { cells?: { key: string; colId: string }[] } | number | Set one value across a block of cells as a single undoable step (, card 740). Defaults to the selected range; read-only and non-editable cells are skipped and every write runs the normal parse/validate path. |
| fill | (opts?: { direction?: 'down' | 'up' | 'left' | 'right'; series?: boolean; range?: CellRange }): number | opts?: { direction?: 'down' | 'up' | 'left' | 'right'; series?: boolean; range?: CellRange } | number | Fill a selected range from its leading edge as one undoable step (, card 740). The default copies the anchor across the range (Excel's Ctrl+D and its natural siblings); `series: true` extrapolates a numeric or date series from the first one or two cells of each line, falling back to a copy for types with no series. `direction` defaults to `'down'`. |
| pasteInto | (anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): number | anchor: { key: string; colId: string }text: stringextent?: { rows?: number; columns?: number } | number | Paste tab-separated text anchored at a cell, as one undoable step. Excel's shape rules apply: a single value fills the whole target, a smaller block tiles to fill it, and a block taller than the rows available is clipped rather than creating rows. Returns how many cells were written. |
| previewPaste | (anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): { changes: { key: string; colId: string; oldValue: unknown; newValue: unknown; changed: boolean }[]; rejected: { key: string; colId: string; value: unknown; reason: 'permission' | 'readOnly' | 'validation' | 'locked' | 'missing' }[]; } | anchor: { key: string; colId: string }text: stringextent?: { rows?: number; columns?: number } | { changes: { key: string; colId: string; oldValue: unknown; newValue: unknown; changed: boolean }[]; rejected: { key: string; colId: string; value: unknown; reason: 'permission' | 'readOnly' | 'validation' | 'locked' | 'missing' }[]; } | Compute what a paste would change, without committing (). The engine behind `edit.pastePreview`: `changes` are the accepted writes with their old and new values (and whether each actually differs), `rejected` are the cells a commit would refuse, each with a reason. |
| settle | ( id: string, ok: boolean, reason?: string, reconcile?: { value?: unknown; conflict?: { serverRow?: unknown } }, ): boolean | id: stringok: booleanreason?: stringreconcile?: { value?: unknown; conflict?: { serverRow?: unknown } } | boolean | Report the outcome of an in-flight write (;-5.2 reconcile). `reconcile` carries server truth on a successful settle: `value` is a server-authoritative value written back before `cell:confirmed` (`returning: 'row'`); `conflict.serverRow` surfaces a last-write-wins conflict via `cell:conflict`. Omit both to keep the optimistic value. |
| pending | (): OpenWrite[] | - | OpenWrite[] | Every optimistic cell write still awaiting an outcome, oldest first. Always empty when `edit.commit` is not configured. |
| status | (key: string, colId: string): 'pending' | null | key: stringcolId: string | 'pending' | null | Whether a cell has a write in flight: `'pending'`, or `null` when it is settled. |
| addRow | (row: object): string | null | row: object - the new row (it need not carry a key yet) | string | null the client temp key the row is tracked under, or null when append is not available on this source | Append a row to a remote source optimistically and persist it (), the structural analog of the cell edit path. The row shows immediately under a client temp key, and `adapter.mutate({ kind: 'append', rows: [row] })` is asked to persist it; when the server returns the real key the row is rekeyed everywhere the grid tracks it and `row:confirmed` fires, while a refused append is removed and fires `row:reverted`. Only wired when the source declares `mutate.append`; otherwise it warns once and returns null. |
| deleteRow | (key: string): string | null | key: string - the row key to remove | string | null the id the op is tracked under, or null when delete is not available | Delete a row from a remote source optimistically and persist it (). The row is tombstoned immediately and `adapter.mutate({ kind: 'delete', keys: [key] })` is asked to remove it; on confirmation the row is purged and `row:confirmed` fires, on refusal it is restored and `row:reverted` fires. Only wired when the source declares `mutate.delete`; otherwise it warns once and returns null. |
| deleteRows | (keys?: string | string[], opts?: { origin?: string }): string[] | Promise<string[]> | keys?: string | string[]opts?: { origin?: string } | string[] | Promise<string[]> the removed keys, or a Promise of them on the async path | Delete rows on a user gesture, through the cancellable `beforeDelete` event (, - what the built-in Delete-key and "Delete row" gestures call. Unlike {@link deleteRow}, `beforeDelete` fires on a memory-source grid too, so the row can be confirmed or vetoed there. Off until `config.rowDelete` opts in; the keys default to the row selection. Returns the keys removed (empty on a veto or when disabled), or a Promise of them when a `beforeDelete` handler deferred. |
| settleRow | (id: string, ok: boolean, reason?: string, reconcile?: { key?: string; row?: unknown; conflict?: { serverRow?: unknown } }): boolean | id: string - the op id from `row:pending`ok: boolean - true when the op reached the serverreason?: stringreconcile?: { key?: string; row?: unknown; conflict?: { serverRow?: unknown } } | boolean true when the id named an op still awaiting an outcome | Report the outcome of an optimistic structural write (), the counterpart to {@link settle} for `edit.confirm: 'manual'` over a backend that acknowledges an append/delete on a separate channel. The id arrives on `row:pending`. |
| rowStatus | (key: string): 'pending' | null | key: string - the row key | 'pending' | null `'pending'`, or null when the row is settled | Whether a row has a structural op in flight (). |
| pendingRows | (): OpenRowOp[] | - | OpenRowOp[] | Every structural op still awaiting an outcome (), oldest first; always empty when the source cannot append or delete. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| beforeEdit | A user or AI edit is about to be committed; call `preventDefault(reason?)` to stop it. | BeforeEditEvent | yes |
| edit:cancelled | A `beforeEdit` handler vetoed the commit, or it went stale while an async handler was thinking. | EditCancelledEvent | no |
ScrollApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| toRow | (row: string | number, align?: 'start' | 'center' | 'end' | 'auto'): void | row: string | numberalign?: 'start' | 'center' | 'end' | 'auto' | void | A row key, or a display index. A key survives a sort and is usually what a caller holds; resolving one scans the display order, so prefer an index when scrolling a very large grid repeatedly. The row lands fully visible in the part of the body the pinned strips (pinned rows, sticky group headings, a bottom grand total) do not cover: `end` puts it just above the bottom strip, `start` just below the top one. |
| toColumn | (id: string): void | id: string | void | Scroll sideways until a column is in view. Pinned columns need no scrolling and are already there. |
| toCell | (row: string | number, colId: string, align?: 'start' | 'center' | 'end' | 'auto'): void | row: string | numbercolId: stringalign?: 'start' | 'center' | 'end' | 'auto' | void | Scroll a cell into view, both axes in one call. |
| position | (): { top: number; left: number } | - | { top: number; left: number } | The body's current scroll offsets in pixels. Zeroes before the grid has been rendered. |
| to | (at: { top?: number; left?: number }): void | at: { top?: number; left?: number } | void | `left` is the logical offset, zero at the content's start in either direction. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| scroll | The viewport scrolled to a new offset; fires only when the offset actually moved, not on a refresh. | ScrollEvent | no |
| scroll:end | Scrolling settled: the last of a scroll gesture's frames has been drawn. | ScrollEvent | no |
ExportApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| rangeText | (opts?: object): string | opts?: object | string | The selected range as tab-separated text, the shape a spreadsheet pastes. |
| csv | (opts?: CsvExportOptions): string | Promise<Blob> | opts?: CsvExportOptions | string | Promise<Blob> | Export to CSV. Returns the text, or downloads a blob when the options ask for a file. |
| excel | (opts?: ExcelExportOptions): Promise<Blob> | opts?: ExcelExportOptions | Promise<Blob> | Export to a real `.xlsx` workbook, with the column formats, widths and any styling the options ask for. Large exports stream. |
| clipboard | (opts?: ClipboardOptions): Promise<void> | opts?: ClipboardOptions | Promise<void> | Copy to the clipboard as tab-separated text, which is what Excel, Numbers and Sheets paste as cells. With `rows: 'range'` it copies the selected rectangle. |
| (opts?: { maxRows?: number; unpin?: boolean; print?: boolean; }): Promise<{ printed: boolean; rows: number; reason?: string }> | opts?: { maxRows?: number; unpin?: boolean; print?: boolean; } | Promise<{ printed: boolean; rows: number; reason?: string }> | Switch the grid into print layout - every row in the document, no virtualisation, no paging, pinned columns released - let the layout settle, call the browser's print dialog, and put the grid back as it was. Refused above `maxRows` (5,000 by default, roughly a hundred printed pages), with a message pointing at the CSV and Excel exports. `unpin` keeps the pinned columns in place; `print: false` lays the grid out and restores it without calling the dialog, which is how a host paginates or photographs it. Resolves with what happened, so a caller can show the reason instead of guessing. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| export:progress | A streaming export wrote another chunk, with rows written, rows expected and bytes so far. | ExportProgressEvent | no |
| export:request | A remote export request is about to be handed to the host's `export.remote.fetch` hook. | ExportRequestEvent | no |
| export:done | A remote export came back and the file was handed over (or downloaded). | ExportDoneEvent | no |
ImportApi
Bringing rows in - the mirror of {@link ExportApi} (,.
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| preview | (text: string, opts?: object): ImportPreview | text: stringopts?: object | ImportPreview | Parse delimited text into a preview, changing nothing. |
| csv | (text: string, opts?: object): Record<string, unknown>[] | text: stringopts?: object | Record<string, unknown>[] | Parse delimited text into coerced records - the inverse of `export.csv`. |
| previewXlsx | (bytes: Uint8Array | ArrayBuffer, opts?: object): Promise<ImportXlsxPreview> | bytes: Uint8Array | ArrayBufferopts?: object | Promise<ImportXlsxPreview> | Parse an `.xlsx` file's bytes into a preview, changing nothing (,. Async: the archive is inflated with `DecompressionStream`. |
| xlsx | (bytes: Uint8Array | ArrayBuffer, opts?: object): Promise<Record<string, unknown>[]> | bytes: Uint8Array | ArrayBufferopts?: object | Promise<Record<string, unknown>[]> | Parse an `.xlsx` file's bytes into coerced records - the inverse of `export.excel`. |
| apply | ( input: string | ImportPreview | Record<string, unknown>[], opts?: { mode?: ImportMode }, ): ChangeResult | null | input: string | ImportPreview | Record<string, unknown>[]opts?: { mode?: ImportMode } | ChangeResult | null | Add or replace the grid's rows from text, a preview or records. |
Events
No events.
StateApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| get | (): GridState | - | GridState | Capture the grid's current view - sort, filters, column order, widths, visibility, grouping and the rest - as a plain, JSON-safe object. Columns the reader is not permitted to see are stripped, because the presence of a column is itself information. |
| apply | (state: GridState, opts?: { skip?: (keyof GridState)[] }): StateApplyReport | state: GridStateopts?: { skip?: (keyof GridState)[] } | StateApplyReport | Restore a captured state, skipping the sections named in `skip`. Nothing throws: a section that cannot be applied is listed in the report with a reason. The whole restore is one logical change, so it makes one undo entry and one state event. |
| baseline | (): GridState | null | - | GridState | null | The grid as configured, without `config.state` - captured once, before that seed is applied, so a view opened through `config.state` is never itself mistaken for the default `reset()` returns to. |
| reset | (): StateApplyReport | null | - | StateApplyReport | null | Put the grid back the way it started, as one undoable step. |
| modified | (): boolean | - | boolean | Whether anything has changed since construction. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| state:changed | One logical state change - a gesture, an apply, an undo or a reset - announced once, whatever routed it. | StateChangedEvent | no |
| state:reset | `grid.state.reset()` restored the arrangement the grid was built with. | StateResetEvent | no |
OverlayApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| show | (kind: 'loading' | 'empty' | (string & {}), message?: string): void | kind: 'loading' | 'empty' | (string & {})message?: string | void | Cover the grid body with an overlay: `'loading'`, `'empty'`, or a name of your own, with an optional message. |
| hide | (): void | - | void | Take the overlay away. |
Events
No events.
HistoryApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| undo | (): HistoryEntry | null | - | HistoryEntry | null | Undo the last action, whatever kind it was - an edit, a sort, a filter, a column move - and repaint. Returns the step that was undone, or `null` when there was nothing to undo. |
| redo | (): HistoryEntry | null | - | HistoryEntry | null | Redo the last undone action. Returns the step that was redone, or `null`. |
| canUndo | (): boolean | - | boolean | Whether there is anything on the timeline to undo. |
| canRedo | (): boolean | - | boolean | Whether anything has been undone that could be redone. |
| peek | (direction?: 'undo' | 'redo'): HistoryEntry | null | direction?: 'undo' | 'redo' | HistoryEntry | null | What undo or redo would apply next, for labelling a button. |
| list | (): HistoryEntry[] | - | HistoryEntry[] | The whole timeline, newest first. |
| transaction | (label: string, fn: () => void): HistoryEntry | null | label: stringfn: () => void | HistoryEntry | null | Group everything `fn` does into one undoable step. |
| clear | (): void | - | void | Drop the timeline. It announces the change like any other move, so an undo button does not keep offering a step that no longer exists. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| history:changed | The undo/redo stacks moved: what can now be undone or redone. | HistoryChangedEvent | no |
| history:applied | An undo or redo step was applied. | HistoryAppliedEvent | no |
ViewsApi
Properties
| Property | Type | Description |
|---|---|---|
| activeId | string | null | The id of the view currently applied, or `null` when the grid is not on a named view. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| list | (): SavedView[] | - | SavedView[] | Every saved view with its metadata - name, description, whether it is shared - but not its stored state. |
| get | (id: string): SavedView | undefined | id: string | SavedView | undefined | One view in full, state included. `undefined` when there is no such view. |
| save | (name: string, opts?: { id?: string; shared?: boolean; description?: string; isDefault?: boolean; }): SavedView | name: stringopts?: { id?: string; shared?: boolean; description?: string; isDefault?: boolean; } | SavedView | Save the grid's current state as a named view and make it the active one. `id` naming an existing view overwrites it; `id` naming none creates a view with that id, which is how a host with server-issued ids seeds the store; with no `id`, a name already taken is overwritten rather than duplicated. (`overwrite` was declared and read nowhere - that decision is the order above - and was removed in.) |
| apply | (id: string): SavedView | null | id: string | SavedView | null | Apply a view and make it active. It is a destination, not a patch: the grid returns to its baseline first, so the same view gives the same grid whatever was applied before it. The whole restore is one undo entry. Returns the state report, or `null` when there is no such view. |
| rename | (id: string, name: string): SavedView | null | id: stringname: string | SavedView | null | Give a view a new name. `null` when the name was refused - blank, or already taken. |
| duplicate | (id: string, name?: string): SavedView | null | id: stringname?: string | SavedView | null | Copy a view, optionally under a new name. `null` when there is no such view. |
| remove | (id: string): boolean | id: string | boolean | Delete a view. `false` when there was no such view; deleting the active one leaves the grid as it is, with no active view. |
| setDefault | (id: string | null): SavedView | null | id: string | null | SavedView | null | Mark the view applied on load; null clears it. |
| defaultView | (): SavedView | null | - | SavedView | null | The view marked as the one to apply on load, or `null` when none is. |
| diff | (id: string): Record<string, unknown> | null | id: string | Record<string, unknown> | null | What applying the view would change, without applying it. |
| export | (id?: string): ViewPayload | null | id?: string | ViewPayload | null | A shareable payload for one view, or for every view when no id is given - an object, not JSON text, so a host can add to it before sending it. `null` when the id names no view. The default marker never travels: it belongs to this user's store, not to the view. |
| import | (json: ViewPayload | SavedView | SavedView[] | string, opts?: { /** What a name already in the store does. `'rename'` (the default) keeps both. */ onConflict?: ViewConflictPolicy; /** Overrides the shared flag on every incoming view. */ shared?: boolean; }): ViewImportReport | json: ViewPayload | SavedView | SavedView[] | stringopts?: { /** What a name already in the store does. `'rename'` (the default) keeps both. */ onConflict?: ViewConflictPolicy; /** Overrides the shared flag on every incoming view. */ shared?: boolean; } | ViewImportReport | Take a shared payload - the object, its JSON text, a bare view or a bare array - and report what was imported, skipped and repaired. It reports rather than throwing: an import that fails silently is worse than one that says so, and one bad entry never rejects the rest of the file. |
| reload | (): void | - | void | Re-read from storage, after another tab or the server changed it. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| views:changed | The saved-view list changed, for any reason; the named `view:*` events say which view moved. | ViewsChangedEvent | no |
| view:applied | A saved view was applied to the grid. | ViewAppliedEvent | no |
| view:saved | A saved view was created, updated or imported. | ViewChangedEvent | no |
| view:removed | A saved view was deleted. | ViewChangedEvent | no |
| view:renamed | A saved view was renamed. | ViewChangedEvent | no |
| view:default | A saved view was made the default one. | ViewChangedEvent | no |
DiffApi
Properties
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether a baseline is loaded and the grid is diffing against it. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| swap | (): boolean | - | boolean | Exchange the baseline and the current rows. Returns false with nothing to swap. |
| setSnapshot | (rows: unknown[] | null): void | rows: unknown[] | null | void | Set the baseline every row is compared against. |
| clear | (): void | - | void | Drop the baseline and stop diffing. |
| summary | (): { added: number; removed: number; changed: number; unchanged: number } | - | { added: number; removed: number; changed: number; unchanged: number } | How many rows are added, removed, changed and unchanged against the baseline. |
| statusOf | (key: string): 'added' | 'removed' | 'changed' | 'unchanged' | key: string | 'added' | 'removed' | 'changed' | 'unchanged' | How one row stands against the baseline: `'added'`, `'removed'`, `'changed'` or `'unchanged'`. |
| cellStatus | (key: string, colId: string): 'changed' | 'unchanged' | key: stringcolId: string | 'changed' | 'unchanged' | Whether one cell differs from the baseline. |
| isChanged | (key: string, colId?: string): boolean | key: stringcolId?: string | boolean | Whether a cell differs from the baseline - or, with no column given, whether the row does. |
| changedColumns | (key: string): string[] | key: string | string[] | Which of a row's columns differ from the baseline. |
| before | (key: string, colId: string): unknown | key: stringcolId: string | unknown | The value a cell held in the baseline. |
| beforeRow | (key: string): unknown | key: string | unknown | The whole row as the baseline holds it. |
| removedKeys | (): string[] | - | string[] | The keys present in the baseline and gone from the current rows. |
| removedRows | (): unknown[] | - | unknown[] | The rows present in the baseline and gone from the current rows, as their original objects. |
| report | (): Record<string, unknown> | - | Record<string, unknown> | A full record of one row's changes - old value and new, per column - for an audit log. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| diff:changed | Diff mode was turned on against a snapshot, or turned off. | DiffChangedEvent | no |
| diff:swapped | The two sides of a diff were swapped. | DiffSwappedEvent | no |
PermissionsApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| levelOf | (column: string | ResolvedColumn): PermissionLevel | column: string | ResolvedColumn | PermissionLevel | The permission level resolved for a column against the current context: `'hidden'`, `'read'`, `'write'` or `'writeOnly'`. |
| isHidden | (column: string | ResolvedColumn): boolean | column: string | ResolvedColumn | boolean | Whether a column is withheld from this user entirely - it is not in `columns.all()` and its values never reach the row. |
| isReadable | (column: string | ResolvedColumn): boolean | column: string | ResolvedColumn | boolean | Whether this user may see the column's values. Not the same question as `isHidden`: a write-only column is on screen and editable and still fails this. |
| isEditable | (column: string | ResolvedColumn): boolean | column: string | ResolvedColumn | boolean | Whether this user may edit the column. |
| isSecret | (column: string | ResolvedColumn): boolean | column: string | ResolvedColumn | boolean | True only at `writeOnly`: writable, never shown or exported. |
| isExportable | (column: string | ResolvedColumn): boolean | column: string | ResolvedColumn | boolean | Whether the column may leave through an export or the clipboard. |
| levels | (): Record<string, PermissionLevel> | - | Record<string, PermissionLevel> | Every column's resolved level, keyed by column id. |
| setContext | (context: unknown): void | context: unknown | void | Change the context permissions are evaluated against, and re-evaluate. |
| invalidate | (): void | - | void | Re-resolve every column against the context as it stands - for a policy whose inputs changed without the context object being replaced. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| permissions:changed | The per-column permission levels changed. | PermissionsChangedEvent | no |
AiApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| schema | (opts?: { maxColumns?: number; maxRows?: number }): Record<string, unknown> | opts?: { maxColumns?: number; maxRows?: number } | Record<string, unknown> | A machine-readable description of the grid, for a model's context. |
| tool | (opts?: { maxColumns?: number; maxRows?: number }): Record<string, unknown> | opts?: { maxColumns?: number; maxRows?: number } | Record<string, unknown> | The same schema as a tool definition. |
| prompt | (opts?: Record<string, unknown>): string | opts?: Record<string, unknown> | string | The prompt describing this grid (its columns, types and operators) for sending to a model. It carries no row values. It does not take the user's question: compose that yourself alongside the text this returns, which is what `ask` receives as `schemaText`. |
| buildPrompt | (text: string, opts?: Record<string, unknown>): string | text: stringopts?: Record<string, unknown> | string | The complete prompt for a question: the schema, the rules the plan validator enforces, and the user's text. Use it when you want the shipped prompting; use `prompt()` and compose your own when you do not. |
| plan | (reply: string | Record<string, unknown>, opts?: Record<string, unknown>): Record<string, unknown> | reply: string | Record<string, unknown>opts?: Record<string, unknown> | Record<string, unknown> | Parse what the model returned into a plan. |
| apply | (plan: Record<string, unknown>): Record<string, unknown> | plan: Record<string, unknown> | Record<string, unknown> | Run a plan as one undoable step. |
Events
No events.
MessagesApi
The resolved message set for a grid: every user-visible string, in the grid's locale.
Properties
| Property | Type | Description |
|---|---|---|
| locale | string | The resolved BCP 47 tag. (read-only) |
| keys | ReadonlyArray<string> | Every key the catalogue defines. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| t | (key: string, params?: Record<string, unknown>): string | key: string - a key from `keys`params?: Record<string, unknown> | string | Format a message. |
| list | (items: string[], type?: 'conjunction' | 'disjunction'): string | items: string[]type?: 'conjunction' | 'disjunction' | string | Join parts the way this locale joins lists. |
| number | (value: number, opts?: Intl.NumberFormatOptions): string | value: numberopts?: Intl.NumberFormatOptions | string | Format a number for this locale. |
Events
No events.
LicenceApi
Properties
| Property | Type | Description |
|---|---|---|
| ready | Promise<LicenceInfo> | Settles when the licence check finishes. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| set | (key: string): LicenceInfo | key: string | LicenceInfo | Install a licence key for this page. Returns the provisional verdict at once; verification is asynchronous, and `licence:changed` fires again when it settles, so an optimistic watermark can come down. |
| info | (): LicenceInfo | - | LicenceInfo | The current verdict: who the licence is for, which domains it covers, when it expires, and whether it verified. |
| state | (): 'licensed' | 'localhost' | 'trial' | - | 'licensed' | 'localhost' | 'trial' | What this deployment is running as: `'licensed'`, `'localhost'` (free, never watermarked) or `'trial'`. |
| watermark | (): boolean | - | boolean | Whether the trial watermark should be drawn. The DOM layer reads this; a headless grid can too. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| licence:changed | A licence key was installed through `grid.licence.set(key)`, and again when its asynchronous verification settles. | LicenceChangedEvent | no |
PaginationApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| get | (): { page: number; pageSize: number; total: number | null; pageCount: number; counting: boolean; } | - | { page: number; pageSize: number; total: number | null; pageCount: number; counting: boolean; } | The current page, its size, the total rows and how many pages they make. Zeroes with one page on a source that does not page. `total` is `null` and `counting` is `true` while a source is still working out its exact total. A pager reading a total in that gap would offer a last page that is nowhere near the end; with none it degrades to "there is a next page", which is what it already does for a source that cannot count at all. |
| set | (next: { page?: number; pageSize?: number }): void | next: { page?: number; pageSize?: number } | void | Move to a page, change the page size, or both, recording one undo entry. A page size of 0 turns paging off and shows everything. |
| applyPage | (next: { page?: number; pageSize?: number }): void | next: { page?: number; pageSize?: number } | void | The same move without a separate undo entry - for a control that is already inside a transaction of its own. Does nothing when neither value changes; emits `page:changed` when one does. |
Events
No events.
HighlightApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| clear | (target?: { key?: string; colId?: string } | string): boolean | target?: { key?: string; colId?: string } | string | boolean | Clear one target, or every highlight when called with nothing. |
| list | (): { scope: string; key: string | null; colId: string | null; colour: string; duration: number }[] | - | { scope: string; key: string | null; colId: string | null; colour: string; duration: number }[] | Every highlight currently in force, with its scope, target, colour and duration. |
| colourFor | (key: string, colId: string): string | null | key: stringcolId: string | string | null | The colour a cell is painted by the highlights in force, or `null` when it is not highlighted. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| highlight:changed | The set of host-declared highlights changed. | HighlightChangedEvent | no |
FindApi
In-grid find: locate text and step through where it occurs without filtering anything away. Matches are a visual overlay - no row is reordered, removed or edited - and coexist with the quick filter.
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| open | (text?: string): void | text?: string | void | Show the bar with focus in its input, optionally seeding the text. |
| close | (): void | - | void | Hide the bar and clear every match. |
| clear | (): void | - | void | Clear the query and the highlights, leaving the bar as it is. |
| next | (): FindMatch | null | - | FindMatch | null | The next match, wrapping from the last to the first, scrolled into view and made the active cell unless an edit is open. |
| prev | (): FindMatch | null | - | FindMatch | null | The previous match, wrapping from the first to the last. |
| goTo | (index: number): FindMatch | null | index: number | FindMatch | null | Make the match at a position in `matches()` current. |
| matches | (): FindMatch[] | - | FindMatch[] | Every match, in display order: pinned-top rows, then the body, then pinned-bottom rows. |
| count | (): FindCount | - | FindCount | How many matches there are, which one is current, and how much of the data was actually searched. `windowed` is the honest-scope flag: over a source that pages, `loaded` is the rows the scan really read out of the `rows` the source reports, never a first-page count dressed as the whole. |
| current | (): FindMatch | null | - | FindMatch | null | The match the grid is standing on, or `null` when there is none. |
| state | (): FindState | - | FindState | The query as it stands - the text, the options - together with whether the find bar is open. |
| stateFor | (key: string, colId: string): 'current' | 'match' | null | key: stringcolId: string | 'current' | 'match' | null | How a cell is painted: the current match, another match, or nothing. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| find:changed | The find bar's query, open state or match count changed. | FindChangedEvent | no |
RedactionApi
Redaction obscures a column's values on screen. It is presentational: the values stay in the model, the DOM, the clipboard and every export. Use `permissions` with `writeOnly` for a value that must not be readable.
Properties
| Property | Type | Description |
|---|---|---|
| active | boolean | Whether at least one column is being obscured. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| has | (colId: string): boolean | colId: string | boolean | Whether a column's values are being obscured. |
| list | (): string[] | - | string[] | Every redacted column id. |
| toggle | (colId: string): boolean | colId: string | boolean | Redact a column, or stop redacting it; returns the state it is now in. Recorded on the undo timeline. |
| add | (colId: string): void | colId: string | void | Obscure a column's values while leaving its heading, its width and the shape of the data visible. This defeats a camera and a screen recorder, not a reader: the values remain in the DOM, the clipboard and every export. Use column permissions for anything that must not be read. |
| remove | (colId: string): void | colId: string | void | Stop obscuring a column's values. |
| set | (ids: string[]): void | ids: string[] | void | Replace the whole redacted set with these column ids. |
| clear | (): void | - | void | Stop obscuring every column. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| redaction:changed | The set of redacted columns changed. | RedactionChangedEvent | no |
AnnotationApi
Properties
| Property | Type | Description |
|---|---|---|
| tool | AnnotationTool | null | The drawing tool in use, or null when the layer is inert - which it is until a tool is chosen, so the grid takes the pointer as usual. (read-only) |
| count | number | How many marks the layer is holding, durable and drawn alike. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| use | (tool: AnnotationTool | null, opts?: { colour?: string }): string | null | tool: AnnotationTool | nullopts?: { colour?: string } | string | null | Choose a tool, or pass null to hand the pointer back to the grid. Asking for the tool already in use turns it off, so one button can toggle; `opts.colour` sets the colour to draw in, and changing only the colour keeps the current tool. Returns the tool now in use. |
| add | (mark: AnnotationMark): number | mark: AnnotationMark | number | Add a durable mark from a descriptor, without synthesising pointer input. The mark is painted, survives a presentation ending, and round-trips through `getState`. Returns the mark count. |
| list | (): AnnotationMark[] | - | AnnotationMark[] | Every mark on the layer, as descriptors - the shape `getState` persists. |
| undo | (): number | - | number | Remove the most recent mark and repaint. Returns how many are left; on an empty layer it does nothing and returns 0. |
| clear | (): void | - | void | Remove every mark, seeded and drawn alike - the explicit "clear all". Ending a presentation drops only the drawn ones. |
| redraw | (): void | - | void | Repaint every mark at the current scroll offset. Called for you on scroll and after each render; a host needs it only after moving the layer itself. |
Events
No events.
PresentationApi
Properties
| Property | Type | Description |
|---|---|---|
| active | boolean | Whether a presentation is running. The grid only dims for a spotlight while it is. (read-only) |
| scale | number | The enlargement in force, 1 being normal. Clamped to between 0.5 and 4. (read-only) |
| options | { scale?: number; chrome?: string[]; views?: string[]; from?: number; autoAdvance?: number } | The options the running presentation was started with. Empty once it stops. (read-only) |
| views | string[] | The saved view ids being stepped through - the slides. Empty when the presentation is just an enlargement. (read-only) |
| index | number | Where in the sequence the presentation is, from 0, or -1 when there is no sequence. (read-only) |
| viewId | string | null | The view currently shown, or null when there is no sequence. (read-only) |
| spotlight | { keys: string[]; colIds: string[] } | null | What is currently lit - rows by key, columns by id, or their intersection - or null when nothing is. Cleared on every step, because a spotlight belongs to the point being made. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| start | (options?: { scale?: number; chrome?: string[]; /** Saved view ids to step through. Views are the slides. */ views?: string[]; /** Where in that sequence to begin. */ from?: number; /** Milliseconds between automatic advances, for an unattended display. */ autoAdvance?: number; }): boolean | options?: { scale?: number; chrome?: string[]; /** Saved view ids to step through. Views are the slides. */ views?: string[]; /** Where in that sequence to begin. */ from?: number; /** Milliseconds between automatic advances, for an unattended display. */ autoAdvance?: number; } | boolean | Begin presenting: enlarge the grid, keep only the named chrome, and step through `views` from `from`. Returns true only when this call started it - calling it again while presenting reconfigures the running presentation and returns false. |
| stop | (): boolean | - | boolean | Stop presenting and put the grid back: the scale, the chrome, the sequence and any spotlight. Returns whether one was running. |
| setScale | (value: number): number | value: number | number | Set the enlargement, clamped to 0.5-4, and return the scale now in force. |
| nudge | (steps?: number): number | steps?: number | number | Move the enlargement by steps of 0.1 - the live adjustment between a laptop and a projector. Negative shrinks. Returns the scale now in force. |
| step | (by?: number): number | by?: number | number | Move through the sequence by this many views (1 by default, negative to go back). Clamped at both ends rather than wrapping, so pressing forward on the last slide stays there. Returns the position now shown, or -1 without a sequence. |
| goTo | (index: number): number | index: number | number | Show a numbered position in the sequence, clamped into range. Returns the position now shown, or -1 without a sequence. Stepping to a view clears the spotlight. |
| reset | (): boolean | - | boolean | Put the current view back exactly as it was saved, discarding the sorting and filtering done while answering a question. Returns whether a view was restored. |
| setSpotlight | (target?: { keys?: string[]; colIds?: string[] } | null): boolean | target?: { keys?: string[]; colIds?: string[] } | null | boolean | Light rows, columns or their intersection and let the rest recede; call it with nothing to clear. Returns whether anything is lit now. Calling it while no presentation is running warns, because nothing dims outside one. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| presentation:changed | Either the responsive presentation switched between the table and the card layout, or `presentation.start()` was called again while already running. | PresentationChangedEvent | no |
| presentation:started | `grid.presentation.start()` began presenting. | PresentationStartedEvent | no |
| presentation:ended | `grid.presentation.stop()` stopped presenting. | no payload | no |
| presentation:view | The presentation stepped to a view in its deck, including the first one. | PresentationViewEvent | no |
| presentation:scale | The presentation's enlargement changed. | PresentationScaleEvent | no |
| presentation:spotlight | The presentation's spotlight was armed over some rows and columns, or cleared. | PresentationSpotlightEvent | no |
| presentation:captured | A screenshot of the grid was captured (`grid.capture()`), with the image's size and type. | PresentationCapturedEvent | no |
PivotViewApi
Controls for the pivot presentation (,: expand or collapse an axis node, and read the collapse state a saved view carries. Every method is a no-op on a headless grid, which has no matrix to collapse.
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| expand | (axis: 'row' | 'column', path: string): void | axis: 'row' | 'column'path: string | void | Expand a collapsed node on the row or column axis. |
| collapse | (axis: 'row' | 'column', path: string): void | axis: 'row' | 'column'path: string | void | Collapse a node on the row or column axis, hiding its descendants. |
| toggle | (axis: 'row' | 'column', path: string): void | axis: 'row' | 'column'path: string | void | Toggle a node's collapse on the row or column axis. |
| state | (): { rowsCollapsed: string[]; columnsCollapsed: string[] } | - | { rowsCollapsed: string[]; columnsCollapsed: string[] } | The collapsed row-axis and column-axis paths, as a saved view carries them. |
Events
No events.
UpdatesApi
Properties
| Property | Type | Description |
|---|---|---|
| paused | boolean | Whether incoming updates are being held rather than applied. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| pause | (): boolean | - | boolean | Hold incoming updates so the rows stop moving. They keep merging while held, so a long pause costs one entry per changed row rather than one per update. `false` when they were already held. |
| resume | (): ChangeResult | - | ChangeResult | Apply everything held and go back to applying as changes arrive. Returns the rows added, updated and removed. |
| flush | (): ChangeResult | - | ChangeResult | Apply what is waiting without leaving the paused state - one step forward, which is what a scrubber wants. Returns the rows added, updated and removed. |
| stats | (): { paused: boolean; pending: number; queued: number; coalesced: number; coalescedTotal: number; rows: number; dropped: number; held: number; heldLimit: number; flushes: number; strategy: string; deferrals: number; maxQueued: number; budgetMs: number; span: { from: number; to: number } | null; } | - | { paused: boolean; pending: number; queued: number; coalesced: number; coalescedTotal: number; rows: number; dropped: number; held: number; heldLimit: number; flushes: number; strategy: string; deferrals: number; maxQueued: number; budgetMs: number; span: { from: number; to: number } | null; } | Counters for the feed and the buffer: what arrived, what is still waiting, how much coalescing saved, and what was dropped or held. |
| log | (opts?: { since?: number }): { at: number; change: RowChange; rows: number }[] | opts?: { since?: number } | { at: number; change: RowChange; rows: number }[] | The timestamped changes still held, oldest first. `since` narrows it to a time window. |
Events
No events.
TimelineApi
Moving the grid through recent data changes. Reads the change log rather than the undo history: history records what the *user* did, and the question on a live grid is what the *data* did. Nothing is scrubbable until `attach()`: what a value used to be is not recoverable after the fact.
Properties
| Property | Type | Description |
|---|---|---|
| attached | boolean | Whether the scrubber is recording. Nothing is scrubbable until it is: what a value used to be cannot be recovered after the fact. (read-only) |
| live | boolean | Whether the grid is showing the present rather than standing somewhere in the past. (read-only) |
| position | number | How many steps back from the present the grid is standing; 0 is live. (read-only) |
| depth | number | How many steps back it is currently possible to go - the length of the recorded window. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| attach | (): void | - | void | Start recording what each change replaces. The scrubbable window fills from this moment on; nothing before it is recoverable. |
| detach | (): void | - | void | Stop recording and return the grid to the present. |
| seek | (steps: number): number | steps: number | number | Stand a given number of steps back from the present, 0 being live. Returns where it now stands, which may be short of what was asked for. |
| step | (by: number): number | by: number | number | Move by a relative number of steps, negative going back in time. Returns where it now stands. |
| toLive | (): number | - | number | Return to the present, applying everything that was stepped over. Returns 0. |
| at | (): number | null | - | number | null | The timestamp of the moment being shown, or `null` when the grid is live. |
| span | (): { from: number; to: number } | null | - | { from: number; to: number } | null | The range of time the scrubber can move over, or `null` when nothing is recorded. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| timeline:attached | The timeline scrubber began recording what each change replaces. | TimelineAttachedEvent | no |
| timeline:detached | The timeline scrubber stopped recording and the grid returned to the present. | no payload | no |
| timeline:seek | The timeline finished moving and the grid now stands at that position. | TimelineSeekEvent | no |
| timeline:seeking | The timeline is about to move, with where it is coming from and going to. | TimelineSeekingEvent | no |
CrossFilter
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| enabled | (): boolean | - | boolean | Whether this grid can cross-filter a source. |
| column | (): string | null | - | string | null | The source column the filter is pushed onto. |
| get | (): string[] | - | string[] | The keys currently filtering the source. |
| set | (keys: string | string[] | null): void | keys: string | string[] | null | void | Filter the source to these derived rows. |
| toggle | (key: string): void | key: string | void | Add or remove one key, for click-to-filter. |
| clear | (): void | - | void | Take this grid's filter off its source. |
Events
No events.
FacetsApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| get | (colId: string): FacetState | null | colId: string | FacetState | null | The distribution behind a column's header histogram - its buckets, counts and whether they are stale - scheduling the count if it has not run. `null` when the column has no histogram. |
| suppression | (colId: string): string | null | colId: string | string | null | Why a column has no histogram - `'disabled'`, `'type'`, `'rows'`, `'streaming'`, `'cardinality'`, `'no-provider'` - or `null` when it has one. |
| config | (colId?: string): FacetConfig | colId?: string | FacetConfig | The facet settings in force for a column, with the grid's defaults folded in; the grid's own defaults when no column is named. |
| refresh | (opts?: { immediate?: boolean }): void | opts?: { immediate?: boolean } | void | Recount every column whose distribution has been asked for. `immediate: true` skips the debounce. |
| isExpanded | (colId: string): boolean | colId: string | boolean | Whether a column's chart is drawn full height rather than as a collapsed strip. |
| toggle | (colId: string, open?: boolean): boolean | colId: stringopen?: boolean | boolean | Expand or collapse a column's chart, toggling when no state is given. Returns the state it is now in. |
| select | (colId: string, from: number, to?: number, opts?: { additive?: boolean; gesture?: string }): boolean | colId: stringfrom: numberto?: numberopts?: { additive?: boolean; gesture?: string } | boolean | Filter by one bucket, or by a range across several. It sets an ordinary filter, so it undoes, saves and shows in the filter UI like any other. A range is written as a `between` condition rather than a set of buckets, so it stays meaningful when the bucketing changes. `additive` adds to a categorical set instead of replacing it. |
| clear | (colId: string): boolean | colId: string | boolean | Remove this column's own facet filter and leave every other filter in place. |
| selected | (colId: string): number[] | colId: string | number[] | Which of a column's buckets its current filter selects, as bucket indices. |
| expanded | (): string[] | - | string[] | Every column whose chart is expanded, for a saved view. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| facet:computed | A column's facet buckets finished computing, with how long it took and whether a worker did it. | FacetComputedEvent | no |
| facet:filtered | A facet histogram was used to filter its column, or that filter was cleared. | FacetFilteredEvent | no |
| facet:expanded | A facet panel section was opened or closed. | FacetExpandedEvent | no |
| facet:failed | A column's facet buckets could not be computed. | FacetFailedEvent | no |
DetailApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| enabled | (): boolean | - | boolean | Whether master-detail is configured on this grid. |
| isMaster | (target: string | Row): boolean | target: string | Row | boolean | Whether a row - by key or as a row object - can be expanded into a detail region. |
| isOpen | (key: string): boolean | key: string | boolean | Whether this master's detail region is open. |
| open | (key: string): void | key: string | void | Open a master's detail region. Does nothing for a key that is not a master. |
| close | (key: string): void | key: string | void | Close a master's detail region. |
| toggle | (key: string): boolean | key: string | boolean | Open a master's detail if it is closed and close it if it is open; returns the state it is now in. `false` for a key that is not a master. |
| closeAll | (): void | - | void | Close every open detail region. |
| keys | (): string[] | - | string[] | The keys of every master whose detail is open, in the order they were opened. |
| active | (): string | null | - | string | null | The one open master when the detail is drawn into a host element outside the grid, where only one can be open. `null` when nothing is open. |
| placement | (): 'inline' | 'target' | null | - | 'inline' | 'target' | null | Where the detail is drawn: `'inline'` as a row beneath the master, or `'target'` in a host element elsewhere on the page. `null` when master-detail is off. |
| config | (): DetailConfig | null | - | DetailConfig | null | The resolved master-detail settings, for a renderer building the region. `null` when master-detail is off. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| detail:toggled | A master-detail region was opened or closed. | DetailToggledEvent | no |
CommentsApi
Properties
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether comments can be used at all: there is a provider, and the grid's row identity is stable enough to file a comment against. (read-only) |
| openKey | string | null | The cell whose thread is open, as a composite key, or null when none is. (read-only) |
| thread | Comment[] | null | The open thread's comments, or null when no thread is open. A comment still awaiting the provider is in the list, flagged as pending. (read-only) |
| loading | boolean | Whether the open thread's bodies are still being fetched. (read-only) |
| complete | boolean | Whether the index covers every row rather than only the ones that have been on screen. The comments-only filter needs this first. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| unavailable | (): string | null | - | string | null | `'no-provider'`, `'no-row-identity'`, or null when available. |
| at | (rowId: string, colId: string): CommentDescriptor | null | rowId: stringcolId: string | CommentDescriptor | null | The counts for one cell, or null when it carries no comments. This is what the marker is drawn from. |
| request | (rowIds: string[], fields?: string[]): void | rowIds: string[]fields?: string[] | void | Ask the provider for index entries covering these rows - the visible columns unless others are named. Debounced, so scrolling costs one fetch rather than one per frame. |
| open | (rowId: string, colId: string): Promise<Comment[] | null> | rowId: stringcolId: string | Promise<Comment[] | null> | Open a cell's thread and resolve to its comments. The cell's current value is recorded with anything written next, so a later reader can be told the number moved. |
| close | (opts?: { reason?: string }): void | opts?: { reason?: string } | void | Close the open thread and drop its bodies. The optional reason is carried on the event. |
| add | (body: string, opts?: { parentId?: string; author?: object }): Promise<Comment | null> | body: stringopts?: { parentId?: string; author?: object } | Promise<Comment | null> | Add a comment to the open thread, optionally as a reply. It appears at once and is rolled back if the provider rejects it; resolves to the stored comment, or null when there is no open thread or no provider. |
| edit | (commentId: string, body: string): Promise<Comment | null> | commentId: stringbody: string | Promise<Comment | null> | Change a comment's body in the open thread. Resolves to the stored comment, or null when it could not be edited. |
| remove | (commentId: string): Promise<boolean> | commentId: string | Promise<boolean> | Delete a comment from the open thread. Resolves to whether it went. |
| resolve | (): Promise<boolean> | - | Promise<boolean> | Resolve the open thread, so its cells stop counting as outstanding. Resolves to whether it stuck. |
| unresolve | (): Promise<boolean> | - | Promise<boolean> | Reopen the open thread. |
| refresh | (): void | - | void | Re-fetch the index for the rows already known, for when the application learns of a change from elsewhere. The grid opens no transport of its own, so nothing tells it otherwise. |
| loadAll | (): Promise<boolean> | - | Promise<boolean> | Fetch the index for every row in the grid, not just the ones seen. Resolves to whether the index is now complete. |
| hiddenUnresolved | (): number | - | number | How many unresolved threads sit on rows the current filter is hiding - so a reader who filters and sees no markers is not left thinking there is nothing outstanding. 0 while the index is incomplete, because then it cannot be known. |
| filterToCommented | (opts?: { unresolvedOnly?: boolean }): boolean | opts?: { unresolvedOnly?: boolean } | boolean | Narrow the grid to rows carrying comments, or with `unresolvedOnly` to those carrying unresolved ones. Returns whether the filter could be applied - it needs a complete index. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| comment:added | A comment was added to a cell, or a reply added to a thread. | CommentAddedEvent | no |
| comment:edited | A comment's text was edited. | CommentEvent | no |
| comment:deleted | A comment was deleted. | CommentEvent | no |
| comment:failed | A comment operation could not reach the backend; `operation` names which one. | CommentFailedEvent | no |
| comment:resolved | A comment thread was marked resolved. | CommentResolvedEvent | no |
| comment:unresolved | A resolved comment thread was reopened. | CommentResolvedEvent | no |
| comment:threadOpened | A cell's comment thread was opened. | CommentThreadOpenedEvent | no |
| comment:threadClosed | A cell's comment thread was closed or dismissed. | CommentThreadClosedEvent | no |
| comment:indexLoaded | The comment index for the visible rows finished loading, with how many entries it carried. | CommentIndexLoadedEvent | no |
PresenceApi
Properties
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether a presence provider is attached. Without one the whole namespace is inert. (read-only) |
| me | Record<string, unknown> | null | This client's own identity as the provider gave it, or `null` when there is none. (read-only) |
| publishing | boolean | Whether this client is sending its own presence, as against only receiving others'. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| peers | (): Peer[] | - | Peer[] | Every peer, most recently active first, each carrying its cursor, idle state and a `hidden` flag. `hidden` means their row is not in this view - filtered out, on another page, or evicted from a window - not that they have gone. |
| hiddenCount | (): number | - | number | How many peers have their cursor on a row this view is not showing. |
| editorOf | (rowId: string, colId: string): Peer | null | rowId: stringcolId: string | Peer | null | The peer editing a cell, when one is and their claim is still fresh. `null` otherwise. |
| lockedBy | (rowId: string, colId: string): Peer | null | rowId: stringcolId: string | Peer | null | Advisory. Reduces collisions; does not eliminate them. |
| jumpTo | (peerId: string): boolean | peerId: string | boolean | Scroll this view to a peer's cursor, centring the row. `false` when the peer is unknown or their row is not in this view. |
| publish | (): void | - | void | Send local presence now, without waiting for the throttle. |
| setPublishing | (on: boolean): void | on: boolean | void | Stop or resume sending this client's own presence - an observer role that still receives everyone else's. |
| setPaused | (paused: boolean): void | paused: boolean | void | Suspend publishing entirely, as the DOM layer does when the tab is hidden. |
| connect | (provider: PresenceProvider | null): void | provider: PresenceProvider | null | void | Attach a presence provider after construction, or pass `null` to detach. |
| stats | (): Record<string, number> | - | Record<string, number> | Publish, receive, drop and error counters for the presence channel. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| presence:published | This grid published its own presence - the cell it is on, its selection - to the presence transport. | PresencePublishedEvent | no |
| presence:joined | A peer appeared in the presence channel for the first time. | PresencePeerEvent | no |
| presence:updated | A peer already present moved or changed what it is doing. | PresencePeerEvent | no |
| presence:left | A peer left the presence channel or timed out. | PresenceLeftEvent | no |
| presence:failed | A presence subscribe or publish could not reach the transport. | PresenceFailedEvent | no |
| presence:lockRefused | An edit was refused because a peer holds the cell's lock. | PresenceLockRefusedEvent | no |
DiagnosticsApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| snapshot | (): Record<string, unknown> | - | Record<string, unknown> | Everything below as one structure - the whole diagnostic picture in a single read. |
| renders | (): Record<string, unknown> | - | Record<string, unknown> | `dom.cellWrites` is the figure a DOM-write assertion reads. |
| store | (): Record<string, unknown> | - | Record<string, unknown> | How the column store is laid out and what it costs: encoding per column, dictionary sizes, bytes held. |
| operations | (): Record<string, unknown> | - | Record<string, unknown> | Timing history per kind of operation - sort, filter, group, ingest - sampled over recent calls. |
| providers | (): Record<string, unknown> | - | Record<string, unknown> | Call counts, errors, latency and recent calls for each provider the host supplied. |
| events | (): Record<string, number> | - | Record<string, number> | How many listeners are attached per event type. A count that grows without bound is almost always a subscription leak in the host application. |
| config | (): { effective: Record<string, unknown>; supplied: string[]; defaulted: string[] } | - | { effective: Record<string, unknown>; supplied: string[]; defaulted: string[] } | The configuration actually in force, and which keys you supplied as against which the grid defaulted. |
| warnings | (): DiagnosticWarning[] | - | DiagnosticWarning[] | Everything the grid has flagged this session, newest first - its own checks and every `[lattice]` warning - each with the stable identifier a support conversation can name. |
| dismiss | (id: string): void | id: string | void | Hide one warning for the rest of this session, by its identifier. Nothing is persisted. |
| bundle | (): Record<string, unknown> | - | Record<string, unknown> | Contains no row data, cell values or column values. |
| checkOptions | (options: unknown): boolean | options: unknown | boolean | Ask whether an options object keeps changing identity without its contents changing - the framework-wrapper mistake that rebuilds the grid on every parent render. True means the identity churned. |
| record | (kind: string, detail: { rows?: number; ms?: number; worker?: boolean }): void | kind: stringdetail: { rows?: number; ms?: number; worker?: boolean } | void | Record an operation your own code performed - its name, the rows it touched, how long it took - so it appears alongside the grid's in `operations()`. |
| render | (cause: string, phases?: Record<string, number>): void | cause: stringphases?: Record<string, number> | void | Record a repaint and what caused it, with optional per-phase timings. The DOM layer calls this; a custom renderer can too. |
| recordEvents | (on: boolean, limit?: number): void | on: booleanlimit?: number | void | Off by default; recording times every emit. |
| eventLog | (): Array<{ type: string; origin: string; listeners: number; payload: Record<string, string>; at: number; ms: number }> | - | Array<{ type: string; origin: string; listeners: number; payload: Record<string, string>; at: number; ms: number }> | The recorded events, oldest first, once `recordEvents(true)` has been called. Empty otherwise. |
| clearEventLog | (): void | - | void | Forget the recorded events while leaving recording switched on. |
| mark | (): Record<string, unknown> | - | Record<string, unknown> | Keep current store statistics so growth can be measured against them. |
| since | (): Record<string, unknown> | null | - | Record<string, unknown> | null | What has changed since `mark()` - the growth that shows a leak, which no single reading can. `null` when nothing is marked. |
| reset | (): void | - | void | Zero the counters, leaving the warnings and the configuration report alone. |
Events
No events.
StatisticsApi
Properties
| Property | Type | Description |
|---|---|---|
| maintenance | Readonly<Record<string, 'maintained' | 'rescan'>> | Which reductions can be maintained against a change, and which rescan. (read-only) |
| approximate | Readonly<Record<string, ApproximateEntry>> | The approximate tier: kernels a sketch maintains in constant time per tick, keyed by kernel name, each carrying the sketch that backs it and the error bound that sketch is verified to meet. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| shadow | (colId: string, kind: ShadowKind, rowKey: string, scope?: RowScope, spec?: object): unknown | colId: stringkind: ShadowKindrowKey: stringscope?: RowScopespec?: object | unknown | One shadow value for one row, by the column it shadows and the kind. For `kind: 'specStatus'`, `spec` carries the `{lower, upper, warnLower, warnUpper}` limits to judge the row's value against; other kinds ignore it. |
| fitShadow | (kind: 'fitPredicted' | 'fitResidual' | 'fitInfluence' | 'fitStdResidual' | 'fitLeverage' | 'fitCooksD', rowKey: string, spec: RegressionSpec): number | boolean | null | kind: 'fitPredicted' | 'fitResidual' | 'fitInfluence' | 'fitStdResidual' | 'fitLeverage' | 'fitCooksD'rowKey: stringspec: RegressionSpec | number | boolean | null | One regression shadow value for a row, by key: the predicted value, residual, or Cook's-distance influence flag from the fitted model, over the filtered rows. Null for a row outside the fit. |
| running | (colId: string, kind: 'total' | 'percent', rowKey: string): number | null | colId: stringkind: 'total' | 'percent'rowKey: string | number | null | A running total at one row, down the grid as it is currently ordered. |
| rebase | (colId?: string): void | colId?: string | void | Make the current values the new baseline: "mark all". |
| tracking | (): { columns: string[]; rows: number; forgotten: number } | - | { columns: string[]; rows: number; forgotten: number } | What the shadow histories are costing. |
| reduce | (colId: string, fn: string): unknown | colId: stringfn: string | unknown | Reduce a column by a named kernel over the filtered rows. |
| profile | (colId: string): ColumnProfile | null | colId: string | ColumnProfile | null | Everything worth knowing about one column, in one pass each. |
| anomalies | (opts?: { columns?: string[]; method?: OutlierMethod; threshold?: number; k?: number; p?: number; windowLen?: number; minPeriods?: number }): AnomalyReport | opts?: { columns?: string[]; method?: OutlierMethod; threshold?: number; k?: number; p?: number; windowLen?: number; minPeriods?: number } | AnomalyReport | The rows that do not belong: anomaly detection over the filtered rows by the robust modified z-score (`modifiedZScore`, the default), Tukey's IQR fences (`iqr`), or multivariate Mahalanobis distance over the chosen columns (`mahalanobis`). Every flagged row carries the score behind it and the reason for it, so a flag is explainable rather than a verdict from nowhere. Non-numeric columns are returned under `skipped`. |
| subsetVsPopulation | (opts?: { columns?: string[] }): SubsetComparison | opts?: { columns?: string[] } | SubsetComparison | Which columns differ most between the filtered subset and the whole population it was drawn from, ranked by effect size - never by a p-value. The measure is stated per column; a numeric and a categorical column are put on one bounded scale so they rank against each other. |
| datasetVsDataset | (other: Grid, opts?: { columns?: string[] }): DatasetComparison | other: Gridopts?: { columns?: string[] } | DatasetComparison | Which columns differ most between this grid and another, ranked by effect size - never by a p-value. The generalisation of {@link subsetVsPopulation} from subset-vs-population to dataset-vs-dataset: two independent grids, yoked by passing one in, no shared store. A numeric column reports a pooled standardised mean difference (Cohen's d, symmetric in the two peers where Glass's delta is not); a categorical column the total variation of its category mix; both land on one bounded scale. Both sides are read over their filtered rows. Only shared columns are ranked; a column on one side alone is returned under `unmatched`. |
| compareGroups | (colId: string, opts: TwoSampleSpec): GroupComparison | null | colId: stringopts: TwoSampleSpec | GroupComparison | null | Is the difference between two groups real? A two-sample test returned as data to interpret - never a verdict. The significance boundary the comparison story (653, 735) stopped short of: those rank by how *much* columns differ and return no p-value; this answers *how sure* for one chosen pair of groups and hands the p-value back as data. There is no `significant` flag, no badge, and no multiple-comparison correction. The rows are split by `opts.by`, the test is chosen by the column's family and named in the result (overridable with `opts.test`): Welch's t or Mann-Whitney U for a numeric column, chi-square for a categorical one. Every result pairs a confidence interval on the difference with the effect size, so it is always "how big and how sure". |
| correlation | (a: string, b: string): number | null | a: stringb: string | number | null | Pearson's correlation between two columns. |
| covariance | (a: string, b: string, opts?: { population?: boolean }): number | null | a: stringb: stringopts?: { population?: boolean } | number | null | Covariance, a correlation before the scales are divided out. |
| regression | (a: string, b: string): RegressionFit | null | a: stringb: string | RegressionFit | null | Least-squares fit of `b` on `a`: in finance, beta and alpha. |
| regressionModel | (spec: RegressionSpec): RegressionModel | null | spec: RegressionSpec | RegressionModel | null | Fit a multi-predictor linear model over the filtered rows and return the full diagnostic set - coefficients with standard errors, t and p; R² and adjusted R²; per-row fitted values, residuals, leverage and Cook's D; VIF per predictor; a Breusch-Pagan heteroscedasticity flag; and, for a single predictor, a pointwise confidence band. `method` is `ols`, `wls` (needs a `weights` column) or `robust`; `quantile` is reserved and the regularised families refuse. Null on degenerate input. |
| adf | (spec: { of: string; orderBy: string; maxlag?: number }): AdfResult | null | spec: { of: string; orderBy: string; maxlag?: number } | AdfResult | null | The Augmented Dickey-Fuller stationarity test over the `of` series in `orderBy` order, constant+trend form with the lag order chosen by AIC up to an optional cap. Returns the statistic, the lag used, MacKinnon's critical values, an approximate (interpolated) p-value and a plain-language verdict at the 5% level - a scalar readout, not a column. |
| acf | (spec: { of: string; orderBy: string; maxlag?: number }): AcfResult | null | spec: { of: string; orderBy: string; maxlag?: number } | AcfResult | null | The autocorrelation (ACF) and partial autocorrelation (PACF) of the `of` series in `orderBy` order out to `maxlag`, with the approximate ±1.96/√n band. A short-series readout; feed the arrays to a bar chart over explicit points with the band as reference lines. The lag-1 autocorrelation matches `series(...).autocorrelation`. |
| spearman | (a: string, b: string): number | null | a: stringb: string | number | null | Spearman's rank correlation, which one outlier cannot drag. |
| kendall | (a: string, b: string): number | null | a: stringb: string | number | null | Kendall's tau-b. Null past 5,000 rows: it is quadratic. |
| weightedQuantile | (colId: string, weightId: string, p?: number): number | null | colId: stringweightId: stringp?: number | number | null | A quantile of one column weighted by another; the median by default. |
| capability | (colId: string, opts?: { lower?: number; upper?: number; target?: number; by?: string; baseline?: number; /** Which rule set the violations are judged against. Western Electric by default. */ rules?: ControlChartRuleSet; /** The level for the capability interval. 0.95 by default. */ confidence?: number; }): ProcessCapability | null | colId: stringopts?: { lower?: number; upper?: number; target?: number; by?: string; baseline?: number; /** Which rule set the violations are judged against. Western Electric by default. */ rules?: ControlChartRuleSet; /** The level for the capability interval. 0.95 by default. */ confidence?: number; } | ProcessCapability | null | Process capability against the column's `spec`, with control limits and the Western Electric rule breaks. `baseline` fixes the limits over the first N readings, which is how a shift is found rather than hidden by the limits it widened. |
| interval | (colId: string, opts?: { kind?: SpcStatistic; confidence?: number; /** Which rows count as successes, for a proportion. Truthiness by default. */ where?: (value: unknown, row: Row) => boolean; }): ConfidenceInterval | ProportionInterval | null | colId: stringopts?: { kind?: SpcStatistic; confidence?: number; /** Which rows count as successes, for a proportion. Truthiness by default. */ where?: (value: unknown, row: Row) => boolean; } | ConfidenceInterval | ProportionInterval | null | A confidence interval for what a column measures, the range the estimate pins the figure down to, not a verdict about it. Reads the rows the filters left, so an interval narrows as the grid does: it describes the filtered population, not the whole table. |
| series | (colId: string, opts: { by: string; periodsPerYear?: number }): SeriesStats | null | colId: stringopts: { by: string; periodsPerYear?: number } | SeriesStats | null | How a column varies along an ordering. `by` is required and never guessed: kernels see rows in the order they arrived, which is not the grid's sort. |
| forecast | (colId: string, opts?: { method?: ForecastMethod; horizon?: number; confidence?: number; windowLen?: number; alpha?: number; beta?: number; gamma?: number; period?: number; /** The column to order by before forecasting - a date or numeric axis. */ by?: string; }): ForecastResult | null | colId: stringopts?: { method?: ForecastMethod; horizon?: number; confidence?: number; windowLen?: number; alpha?: number; beta?: number; gamma?: number; period?: number; /** The column to order by before forecasting - a date or numeric axis. */ by?: string; } | ForecastResult | null | Forecast one column forward: the stats-surface face of the {@link forecast} kernel. The column is read over the filtered rows in arrival order, or ordered by `opts.by` (a date or numeric column, as {@link series} orders) when the time axis matters, then projected `opts.horizon` steps ahead by `opts.method` (default `linear`) with a prediction band where one applies. Every kernel option passes through; returns the same {@link ForecastResult}, or null when the column is unknown or too short. |
| weightedAverage | (colId: string, weightId: string): number | null | colId: stringweightId: string | number | null | A weighted average of one column by another. |
| keyOf | (data: unknown): string | null | data: unknown | string | null | The key a row's data resolves to. |
| maintenanceTier | (fn: string): MaintenanceTier | fn: string | MaintenanceTier | The honest tier for one kernel across both the exact and approximate maps: its exact tier and, when one exists, the approximate alternative and bound. |
| windowed | (colId: string, fn: WindowedFn, opts: { kind: WindowKind; /** N ticks for a count window, or N ms for a time window. */ span?: number; /** N minutes for a time window, converted to ms. */ minutes?: number; /** A timestamp column; required for a time or session window. */ by?: string; }): WindowedResult | null | colId: stringfn: WindowedFnopts: { kind: WindowKind; /** N ticks for a count window, or N ms for a time window. */ span?: number; /** N minutes for a time window, converted to ms. */ minutes?: number; /** A timestamp column; required for a time or session window. */ by?: string; } | WindowedResult | null | A windowed aggregate - "the average lately" - over one column, stamped with the window it covers (`over`), so a windowed figure is never read without its window. Exact over the values inside the window. `kind: 'count'` takes the last `span` values in arrival order. `kind: 'time'` takes the values within the last `span` ms (or `minutes`) and `kind: 'session'` takes every value; both need a timestamp column, so `by` is required for them and never guessed. Returns null when the column, or the `by` column, is unknown. |
Events
No events.
FormattingApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| list | (scope?: FormattingScope): FormattingRule[] | scope?: FormattingScope | FormattingRule[] | The rules for one scope in evaluation order - a column id, or `'*'` for the rules that apply to every column. Copies, so editing them changes nothing. |
| all | (): Record<FormattingScope, FormattingRule[]> | - | Record<FormattingScope, FormattingRule[]> | Every rule keyed by scope: the shape a saved view carries and undo restores. Plain JSON, which is why a rule whose `style` is a function cannot live here. |
| scopes | (): FormattingScope[] | - | FormattingScope[] | Every scope that currently holds at least one rule. |
| add | (scope: FormattingScope, rule: FormattingRule, opts?: { at?: number }): FormattingRule | null | scope: FormattingScoperule: FormattingRuleopts?: { at?: number } | FormattingRule | null | Add a rule to a scope and repaint. Returns the stored rule with its id filled in, or `null` when the rule has no `when`, `scale`, `dataBar` or `iconSet` and so could never match. `at` inserts at a position instead of appending; order is meaning, since the first match wins. |
| remove | (scope: FormattingScope, which: string | number): boolean | scope: FormattingScopewhich: string | number | boolean | Remove a rule by its id or its index. `false` when there is no such rule. |
| update | (scope: FormattingScope, which: string | number, patch: FormattingRule): FormattingRule | null | scope: FormattingScopewhich: string | numberpatch: FormattingRule | FormattingRule | null | Change a rule's fields in place, leaving its identity and its position alone - an `id` in the patch is ignored. Returns the updated rule, or `null` when there is no such rule. |
| move | (scope: FormattingScope, which: string | number, to: number): boolean | scope: FormattingScopewhich: string | numberto: number | boolean | Reorder a rule within its scope. Order is meaning - the first matching rule wins - so a move can change which colour a cell takes. `false` when it did not move. |
| set | (scope: FormattingScope, rules: FormattingRule[]): FormattingRule[] | scope: FormattingScoperules: FormattingRule[] | FormattingRule[] | Replace every rule in one scope at once, dropping any that could never match. Returns the stored rules. |
| replaceAll | (rules: Record<FormattingScope, FormattingRule[]>): void | rules: Record<FormattingScope, FormattingRule[]> | void | Replace the whole store, scope by scope, as `all()` produced it. What a state restore and undo use. |
| clear | (scope?: FormattingScope): void | scope?: FormattingScope | void | Remove every rule in one scope, or in all of them when no scope is named. |
| styleFor | (colId: string, value: unknown): CellStyle | null | colId: stringvalue: unknown | CellStyle | null | The style one value would take from the runtime rules alone, for painting outside the grid - a server-side export, a preview. `null` when nothing matches. |
| restat | (): void | - | void | Re-derive the thresholds of distribution rules from the data as it stands. |
| distribution | (colId: string): ColumnDistribution | null | colId: string | ColumnDistribution | null | The five numbers a distribution rule resolves against for one column. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| formatting:changed | A conditional-formatting rule was added, changed, removed or replaced. | FormattingChangedEvent | no |
ValidationApi
The runtime face of declarative column validation.
Properties
| Property | Type | Description |
|---|---|---|
| active | boolean | Whether at least one column declares a rule. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| check | (colId: string, value: unknown, row?: unknown): { code: string; message: string } | null | colId: stringvalue: unknownrow?: unknown | { code: string; message: string } | null | Run a column's rules against a value, returning the first failure or null. |
| errorFor | (key: string, colId: string): ValidationError | null | key: stringcolId: string | ValidationError | null | The recorded error for one cell, or null when it is valid. |
| errors | (): ValidationError[] | - | ValidationError[] | Every cell that currently holds a validation error. |
| clear | (key?: string, colId?: string): boolean | key?: stringcolId?: string | boolean | Clear errors: one cell, a whole row, or all of them. |
| define | (colId: string, spec: ColumnValidation | null): void | colId: stringspec: ColumnValidation | null | void | Set or replace a column's rules at runtime; null removes them. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| validation:failed | A declared column rule refused an edit; the failures name the column and the message for each. | ValidationFailedEvent | no |
| validation:cleared | Recorded validation errors were cleared - for one cell, one row, or the whole grid. | ValidationClearedEvent | no |
MaximiseApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| enter | (): boolean | - | boolean | Fill the window with the grid, remembering where it came from and its scroll position. `true` once it is maximised, including when it already was. |
| exit | (): boolean | - | boolean | Put the grid back where it came from, scroll position included. `false` when it was not maximised. |
| toggle | (): boolean | - | boolean | Maximise, or restore when already maximised. Returns whether the grid is maximised afterwards. |
| active | (): boolean | - | boolean | Whether the grid is currently filling the window. |
Events
No events.
RowFormApi
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| open | (key: string): boolean | key: string | boolean | Open the form for a row. False when the form is not configured. |
| close | (): void | - | void | Close the form without saving, returning focus to wherever it came from. |
| save | (): boolean | - | boolean | Collect the fields, write the ones that map to columns as one undoable step, announce them all on `form:saved`, and close. `false` when a field failed validation - the offending field is scrolled into view and marked - or when there is nothing to save. |
| isOpen | (): boolean | - | boolean | Whether the form panel is showing. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| form:opened | The row form opened over a row. | FormOpenedEvent | no |
| form:closed | The row form was closed without saving. | FormClosedEvent | no |
| form:saved | The row form's values were saved back to the row. | FormSavedEvent | no |
| form:error | The row form could not load or save a row; `timedOut` distinguishes a slow backend from a refusal. | FormErrorEvent | no |
IconRegistryApi
Read access to the grid's icon sprite set (see {@link Grid.icons}).
Properties
No properties.
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| get | (name: string): IconGlyph | null | name: string | IconGlyph | null | One glyph, as a copy, or null when the name is not registered. |
| names | (): string[] | - | string[] | Every registered name, in registration order. |
Events
No events.
Configuration
The options object and everything reachable from it, in dependency order.
GridConfig
| Property | Type | Description |
|---|---|---|
| columns | (Column | ColumnGroup)[] | The columns, in order. A group nests columns under one heading. (optional) |
| columnGroups | ColumnGroup[] | Header groups declared separately from the columns they contain. (optional) |
| rows | unknown[] | The data, for a memory grid. Use `source` for anything fetched. (optional) |
| rowKey | string | string[] | ((row: unknown) => string | string[]) | What identifies a row. Everything that survives a refresh (selection, expansion, and edits in flight) is keyed on it, so it must be stable and unique. A derived grid defaults to its own derived key. Three shapes: a field name (`'id'`, dot paths allowed); an array of field names, joined into one composite key (`['tenantId', 'circuitId']`); or a function of the row (`row => \`${row.tenantId}#${row.circuitId}\``), itself allowed to return an array to the same effect. (optional) |
| source | SourceConfig | Where rows come from: memory, paged, remote, stream or derived. (optional) |
| ingest | IngestConfig | How rows are ingested into the column store. (optional) |
| columnDefaults | Column | Applied to every column before its own settings. (optional) |
| columnPresets | Record<string, Column> | Named bundles of column settings, referenced by a column's `preset`. (optional) |
| dataTypes | Record<string, DataType> | Your own data types, alongside the built-in catalogue. (optional) |
| sampleSize | number | Values sampled per undeclared column when inferring its type. Default 100. (optional) |
| targetSize | TargetSize 'default' | 'large' | Raise every interactive target to a comfortable size for touch, without changing the type. `'large'` asks for it; `'default'` opts out of the coarse-pointer rule that would otherwise apply it. (optional) |
| components | Record<string, RendererCtor | EditorCtor | FilterCtor> | Your own renderers, editors and filters, registered by name. (optional) |
| pipes | Record<string, (value: unknown, ...args: string[]) => string> | Named text transforms usable from a format mask or a template. (optional) |
| totalFns | Record<string, TotalFn> | Your own reductions, alongside the built-in ones. (optional) |
| variants | Record<string, VariantDefinition> | Named appearance variants a row or cell can be switched into by a rule. (optional) |
| icons | Record<string, IconDefinition> | Your own SVG glyphs, registered by name before the first paint. The same registry `registerIcon` writes to and every cell, header control, rail button and chart glyph is painted from, so a name given here 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. Read the result back through {@link Grid.icons}. (optional) |
| tree | TreeConfig | Hierarchical rows: where the parent link or the path lives. (optional) |
| detail | DetailConfig | The expandable panel beneath a row. (optional) |
| selection | SelectionConfig | 'single' | 'multiple' | 'none' | What the user may select, and how selection behaves across groups. The `'none'` shorthand is `{ mode: 'none' }` and behaves identically: no row selection, and no cell ranges or fill handle either. (optional) |
| edit | EditConfig | boolean | Editing, and how a change is committed and validated. (optional) |
| pagination | PaginationConfig | boolean | Page the rows rather than scrolling them. (optional) |
| locale | string | The BCP-47 locale everything the grid formats and collates uses - numbers, dates, text ordering, and the message catalogue. Falls back to the page's own `<html lang>` and then to `en-GB`. (optional) |
| messages | Record<string, string | Record<string, string>> | A partial message catalogue laid over the one `locale` resolves - every string the grid renders or announces. Supply a bundled catalogue (`FR_FR`, `AR`, …) or your own object; overrides merge over the default rather than replacing it, so translating part of the interface leaves the remainder in English rather than showing raw keys. Every valid key is listed in `MESSAGE_KEYS`; a key that is not is ignored with a warning. (optional) |
| direction | Direction 'ltr' | 'rtl' | 'auto' | Writing direction. An explicit `'ltr'` or `'rtl'` always wins. Omit it, or say `'auto'`, to settle it from the mount or the nearest ancestor carrying a `dir="ltr"`/`dir="rtl"` attribute, and only then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. A page-level `dir="ltr"` therefore wins over an RTL locale - a host that wants RTL inside an LTR page sets `direction: 'rtl'` explicitly. In a right-to-left grid the logical alignments `start`/`end` mirror while the physical `left`/`right` do not (see {@link Align}). (optional) |
| timeZone | string | IANA zone every date column formats in, e.g. 'Europe/London' or 'UTC'. Omit to use each viewer's own zone. A column's own `format.timeZone` wins. (optional) |
| theme | Theme 'light' | 'dark' | 'high-contrast' | 'terminal' | (string & {}) | The visual theme. (optional) |
| density | Density 'compact' | 'standard' | 'comfortable' | 'spacious' | number | Row height and padding as a named step, rather than pixel by pixel. (optional) |
| gridLines | boolean | 'both' | 'horizontal' | 'vertical' | 'none' | 'rows' | 'columns' | Which rules are drawn between cells. `'both'` by default. The two axes are separate decisions: horizontal rules help the eye track along a row, vertical ones stop adjacent values running together. `false` or `'none'` draws neither. Only the rules *between data* are affected, the header's underline, the pinned seams and the totals separator are structure, not grid lines. (optional) |
| cornerRadius | boolean | number | string | Round the grid's outer corners. Square by default. `true` adopts the theme's own radius; a number is pixels; a string is used as written, so a host can pass its own token or a relative unit. (optional) |
| stripedRows | boolean | Shade alternate data rows (zebra striping). Off by default, and strictly opt-in: an existing grid must look exactly the same on upgrade. When `true`, every other data row takes the theme's `--lattice-surface-alt` background, which every palette already defines, so dark, high-contrast and terminal stripe correctly without extra work. Parity follows the row's *logical* index, not its position in the DOM, so a row keeps its stripe across a scroll even though the rows are recycled. Structural rows - group headings, group footers and the grand total - are never striped, and both selection and hover still win over the stripe. (optional) |
| verticalAlign | VAlign 'top' | 'middle' | 'bottom' | Vertical alignment of cell content within a row, as a default for every column. `top`, `middle` or `bottom`; a column's own `verticalAlign` overrides it for that column. The horizontal counterpart is the per-column `align`. Omitted, the grid keeps its historical placement - content centred in a fixed-height row and top-aligned in an `autoHeight` row - so an existing grid is unchanged on upgrade. Setting a value aligns every column uniformly, including `autoHeight` rows, unless a column opts out. (optional) |
| tooltip | TooltipConfig | Defaults for the rich cell tooltip. The tooltip itself is declared per column, on `cell.tooltip`; this only carries the settings that are a house style rather than a per-column decision. It switches nothing on: a column with no `cell.tooltip` has no tooltip whatever is set here. (optional) |
| scrollbars | ScrollbarMode | { x?: ScrollbarMode; y?: ScrollbarMode } | How the scroll viewport's scrollbars are drawn,. `'auto'` (the default) is the platform's native behaviour, where overlay scrollbars fade when idle. `'always'` keeps that native bar shown whether or not the pointer is over the grid. `'custom'` makes the grid draw its own bar on each axis instead - always visible, the same in every browser, and sized by `--lattice-scrollbar-size` / `--lattice-scrollbar-thumb-min` rather than by the platform. Scrolling itself is unchanged in every mode. The object form controls each axis on its own - `{ y: 'always' }` pins the vertical bar while the horizontal one stays native. Note that `'custom'` on one axis hides the native bar on both, because no browser offers per-axis control of that; the grid warns once if the two axes disagree. Omitted, the grid is unchanged on upgrade. (optional) |
| columnTagFilter | boolean | { multiple?: boolean; label?: string } | Show a bar above the column headings for filtering columns by tag. Off by default, and it draws nothing unless some column carries a `tags` entry. `multiple: true` lets more than one tag be chosen at once. Only tagged columns are ever hidden, so an untagged account or total column stays visible whatever is selected. (optional) |
| anomalySummary | boolean | { column?: string; label?: string } | Show a small chip in the grid chrome that reads how many rows an anomaly shadow column has flagged, and filters the grid to exactly those when it is clicked. Off by default, and it draws nothing unless a column declares a `shadow: { kind: 'anomalyFlag' }`. The count and the filter both read that one shadow column, so the number on the chip is the number of rows the click reveals. `column` names the base column to summarise when more than one anomaly-flag shadow is present; `label` overrides the chip's wording. (optional) |
| typeOptions | Record<string, { weight?: string; significantFigures?: number; [option: string]: unknown; }> | Per-column options a data type reads. `ratio` and `percentRate` use `{ weight }` to name the column their average is weighted by. A unit type reads `{ significantFigures }` to render to a fixed precision rather than a fixed number of decimals. (optional) |
| rowTemplate | string | { template: string; /** A class of your own on every card, alongside the grid's. */ className?: string; /** The layer's role. `list` by default; `listbox` for a selectable set. */ role?: string; /** Each card's role. `listitem` by default. */ itemRole?: string; /** A fixed number of cards on a line. Does not reflow. */ cardsPerRow?: number; /** A ceiling on card width; the number on a line follows the container. */ maxCardWidth?: number; /** Space between tiles, in pixels. 8 by default. */ gap?: number; } | Draw each row as a card built from a declarative template instead of as a row of cells. The template compiles once into static and dynamic segments, so scrolling a thousand rows through a hundred pooled cards allocates nothing. A card is still a row - selection, the context menu, focus and drag reorder all work unchanged - but it has no columns, so the layer declares itself a list rather than a grid. (optional) |
| gallery | boolean | { /** The tile layout. Generated from the columns when omitted. */ template?: string; /** How wide a tile aims to be; the count across follows the container. 240 by default. */ tileWidth?: number; /** How tall a tile is. 180 by default. */ tileHeight?: number; /** A fixed number of tiles on a line, instead of `tileWidth`. Does not reflow. */ cardsPerRow?: number; /** Space between tiles, in pixels. 8 by default. */ gap?: number; /** A class of your own on every tile, alongside the grid's. */ className?: string; /** The layer's role. `list` by default; `listbox` for a selectable set. */ role?: string; /** Each tile's role. `listitem` by default. */ itemRole?: string; } | Present rows as a gallery of tiles (). The tiled card layout with a size-driven column count: tiles as wide as `tileWidth` allows, as many across as the container holds, laid out by the same 2-D virtualisation the grid already runs. `true` draws a tile per row generated from the columns; an object sizes them or supplies a template. Presentation only - sort, filter, group and the data pipeline are unchanged. (optional) |
| recordCard | boolean | { /** The card layout. Generated from the columns as label/value pairs when omitted. */ template?: string; /** How tall a card is. 200 by default; a form needs room per field. */ cardHeight?: number; /** A class of your own on every card, alongside the grid's. */ className?: string; /** The layer's role. `list` by default. */ role?: string; /** Each card's role. `listitem` by default. */ itemRole?: string; } | Present each row as a record card - a form of label/value pairs (). For a screen where reading one record matters more than comparing many. `true` draws a card per row generated from the columns, each column a labelled line in display order, showing the same text the table shows. An object supplies a template or sizes the card. A card list underneath, so it inherits the virtualisation and every interaction a card carries. Presentation only - sort, filter, group and the data pipeline are unchanged. (optional) |
| board | boolean | { /** The card layout. Generated from the columns as a tile when omitted. */ template?: string; /** How wide a lane is, in pixels. 280 by default. */ laneWidth?: number; /** How tall a card is, in pixels. 120 by default. */ cardHeight?: number; /** Space between lanes, in pixels. 16 by default. */ laneGap?: number; /** Space around a card within its lane, in pixels. 8 by default. */ gap?: number; /** A class of your own on every card, alongside the grid's. */ className?: string; /** The board's role. `list` by default; `listbox` for a selectable set. */ role?: string; /** Each card's role. `listitem` by default. */ itemRole?: string; } | Present rows as a board - a kanban of grouped lanes of cards (). The top-level group becomes a lane and every leaf under it becomes a card stacked in that lane: a pipeline by stage, a task list by status, a backlog by owner. `true` draws a card per row generated from the columns; an object sizes the lanes and cards or supplies a template. Group the grid to give the board its lanes; an ungrouped board is a single lane of every card. A card is drawn through the same code the other card presentations use, so a board card is still a row: it clicks, selects and drags through the grid's own handlers, masks protected columns, and shows the same text the table shows. Both axes are virtualised - the lanes across and the cards down each - so a board of many long lanes renders only what is on screen. Presentation only: sort, filter, group and the data pipeline are unchanged. (optional) |
| pivotView | boolean | { /** How wide one value column is, in pixels. 120 by default. */ cellWidth?: number; /** How tall one body row is, in pixels. 32 by default. */ cellHeight?: number; /** How wide the row-label gutter is, in pixels. 200 by default. */ headerWidth?: number; /** Degrade to cards at or below this container width. 640 by default. */ maxWidth?: number; /** The card layout the narrow fallback uses. The record card when omitted. */ fallbackTemplate?: string | object; /** A class of your own on the pivot root, alongside the grid's. */ className?: string; /** The pivot's role. `grid` by default - a pivot is a grid of cells. */ role?: string; } | Present the grid as a pivot - a cross-tab drawn as a matrix (,. The row dimensions (the grid's `group`) go down the left gutter, the column dimensions (the grid's `pivot`) go across the top, and each totalled column fills a cell with its reduction. `true` draws the matrix with the default geometry; an object sizes the cells and gutter or names the breakpoint below which it degrades to cards. **The numbers are the grid's own.** Every cell - body, subtotal, grand total - is the same aggregate kernel the totals row uses, run over the rows that feed the cell, so a pivot subtotal equals the grid's group total for that set by construction rather than being re-derived. Both axes expand and collapse, both are virtualised, and a cell click emits `pivot:drill` with the keys of the contributing rows. **Narrow-screen fallback.** A matrix cannot be read on a phone, so at or below `maxWidth` (the container width, not the viewport) the pivot degrades to a card list - the record card by default - exactly as the table does under `responsive`. Presentation only: sort, filter, group, pivot and the data pipeline are unchanged. (optional) |
| responsive | { /** Collapse at or below this container width. 640 by default. */ maxWidth?: number; /** The card layout, as `rowTemplate` takes it. */ template: string | object; /** How tall a collapsed card is. 64 by default, a table row is too short. */ rowHeight?: number; } | Present rows as cards when the grid's container is too narrow to be a table honestly, a phone, or a narrow panel on a wide screen. Measured on the container, not the viewport, so a grid in a sidebar collapses and a grid filling a small tablet does not. Sorting, filtering and export continue to work; the tool panel is where they live when there are no column headings to click. Emits `presentation:changed`. (optional) |
| rowForm | boolean | { mode?: RowFormMode; load?: (p: { row: Row; data: unknown; key: string; grid: Grid }) => unknown | Promise<unknown>; fields?: (string | { field: string; label?: string; /** Which editor to build, by registry name or constructor. Defaults to the column's, then the type's. */ editor?: string | (new () => object); type?: TypeName; props?: object; lookup?: LookupSpec; })[]; title?: string | ((p: { row: Row; data: unknown }) => string); width?: string; trigger?: false; /** How long to wait for `load`, in milliseconds. 2000 by default; `false` waits indefinitely. */ timeout?: number | false; /** * An element of your own to build the form in, instead of over the grid. * An element, a CSS selector or a function returning either; a selector is * resolved when the form opens, not when the grid is configured. A form in * your own container fills it, is a region rather than a modal dialog, and * does not trap Tab. */ container?: HTMLElement | string | (() => HTMLElement | string | null); } | Open a row on a form - a drawer or a centred dialog with one control per field, a Save and a Cancel - on a double-click or through `grid.form.open()`. By default the fields are the grid's own columns, edited with the same editors the cells use; supply `load` to show a fuller record than the grid displays, in which case you must also say which fields and in what order. The panel opens immediately with a loading state and offers a retry on failure or on a load that never answers, rather than closing. Save applies the changed fields that map to columns and announces the rest - persisting the record is yours. (optional) |
| showColumnFunctions | boolean | Draw the sort, filter and menu controls in the column headings. `true` by default. `false` leaves each heading as its label alone, which is what a dense grid wants: three affordances take roughly fifty pixels, and on an eighty-pixel column that leaves the heading nothing and the label disappears entirely. Only the furniture goes. Sorting, filtering and the column menu are still reachable through the API, the keyboard and the tool panel. (optional) |
| headerControls | HeaderControlsVisibility 'hover' | 'always' | 'hidden' | 'none' | When the per-column header controls - the sort arrow, the filter funnel and the menu button - are shown, as a default for every column. - `'hover'` (the default) reveals them when the heading is hovered or a keyboard user focuses into it, which is the historical behaviour: a wide header does not read as a row of identical icons. - `'always'` keeps them visible unconditionally, for a grid where the controls are the point and the discoverability of hover is not wanted. - `'hidden'` draws none of them, for a clean read-only heading; they leave the tab order with the elements that carried them. An active filter and a live sort are still reflected by the heading's state attributes, but no control furniture is built. A column that is actually sorted still shows a read-only sort arrow and multi-sort order number. - `'none'` is `'hidden'` with that last exception removed: the heading shows only its title, whatever the grid's state - no controls, no hover affordance, and no sort/filter/group badge even when the column is sorted, filtered or grouped programmatically or via a saved view. The column's own `aria-sort` still reports the truth; only the visual badge is gone. For a dashboard heading that must never change its own appearance, however the grid around it is driven. A column's own `headerControls` overrides this default for that column. Distinct from `showColumnFunctions: false`, which also drops the furniture but keeps sorting, filtering and the menu reachable from the keyboard; `'hidden'` and `'none'` are the read-only choices that remove them outright. (optional) |
| rowHeight | number | ((row: Row) => number) | Row height in pixels, or a function of the row. A function makes the grid measure rather than assume, which costs a pass over what is on screen: worth it for wrapped text, wasteful for a uniform grid. (optional) |
| title | string | A caption for the grid, drawn above the column headings. Inside the grid rather than an element the host places above it: a title outside does not scroll with the grid, is not in the region a screen reader announces, and is left behind by image capture and print. (optional) |
| showHeader | boolean | Draw the column headings at all. `true` by default. `false` removes the row, and removes it from the accessibility tree rather than only from view, a heading a screen reader still announces is invisible, not hidden. What a small dashboard tile wants when its `title` already says what the panel is. Distinct from `showColumnFunctions`, which keeps the headings and drops only the sort, filter and menu controls inside them. (optional) |
| headerHeight | number | Header height in pixels. Omitted, the header takes its height from the density-scaled `--lattice-header-height` token, so `density` sizes the header as it sizes the rows. A number names one explicitly and outranks the token. (optional) |
| overscan | number | How many rows to render beyond the viewport. More costs memory and smooths fast scrolling; fewer is lighter and can show a gap. (optional) |
| autoHeight | boolean | 'visible' | Size rows to their content rather than to the density token. Only rows that are actually rendered are ever measured, in both settings: the grid does not lay out rows you cannot see. The difference is what happens on a large grid: `true` gives up above ten thousand rows and falls back to fixed heights, because a cumulative offset array being patched as you scroll a million rows is not worth the result. `'visible'` keeps measuring at any size, accepting that the scrollbar shifts as rows are measured on the way past. The name is historical and reads as though it were about which rows are measured; it is about whether the ceiling applies. (optional) |
| state | GridState | Sort, filters, grouping, widths and the rest, restored at construction. Takes precedence over a saved view flagged `isDefault`: when both are present, this wins outright and the default view is never applied - the active view id stays `null`. (optional) |
| licence | string | Your licence key. Without one the grid renders in full and watermarks off localhost. (optional) |
| maximise | boolean | Offer a full-screen control. (optional) |
| formulaFunctions | Record<string, (args: unknown[]) => unknown> | Extra functions a formula may call, on top of the built-in library. (optional) |
| allowUnsafeTemplates | boolean | Permit raw HTML from a template without sanitising it. Off, and worth leaving off: a template usually interpolates data, and data is where injected markup arrives from. (optional) |
| updates | { logLimit?: number; logRows?: number; /** * When a queued batch applies. `frame` (default) lands on a paint * boundary, which is what makes one repaint per batch reliable; * `microtask` at the end of the current task; `interval` on the coalescing * window; `manual` only when you call `grid.updates.flush()`. */ flush?: UpdatesFlushMode; /** Queued rows that force an early flush regardless of strategy. */ maxQueued?: number; /** Milliseconds one flush may spend before deferring the rest. */ budgetMs?: number; } | Caps on the change log behind `grid.updates` and `grid.timeline`. Two caps, because an entry is not a fixed size: `logLimit` bounds how many changes are kept (default 2000) and `logRows` bounds the rows they account for between them (default 100,000). A feed delivering large batches reaches the second long before the first, and without it the log is unbounded in bytes while looking bounded in entries. (optional) |
| comments | CommentConfig | Threaded comments on individual cells. Requires a stable `rowKey`: comments outlive the values they annotate, and index identity would reattach every thread on the next sort. (optional) |
| presence | PresenceConfig | Collaborative presence. A display feature over a transport the grid does not own; without a provider it is inert. (optional) |
| facets | FacetConfig | boolean | Column header histograms and the filters clicking them creates. Off by default: the band roughly doubles header height, which is a cost no grid should pay without asking. Per-column settings layer over these. (optional) |
| hostFilter | { active(): boolean; passes(row: Row): boolean } | A filter your application owns, applied alongside the grid's own and invisible to its filter UI. (optional) |
| context | unknown | Anything of yours, passed untouched to renderers, editors and sources. (optional) |
| workerThreshold | number | Row count above which eligible work is computed in a Worker: column distributions, and a portable sort (a built-in collation with no custom comparator). Below it, everything runs on the main thread. (optional) |
| useWorker | boolean | Compute eligible work off the main thread: column distributions, and a portable sort above {@link GridConfig.workerThreshold} (a re-sort recomputes off-thread while the grid keeps showing the prior order, then swaps to the new one when it lands). Filtering and grouping still run on the main thread. (optional) |
| workerUrl | string | Where to load the worker kernel from, when hosting it yourself. (optional) |
| sharedMemory | boolean | Use a shared buffer for the worker, where the page's headers allow it. (optional) |
| groupFooter | boolean | A totals line at the foot of each group as well as the grid. (optional) |
| groupDefaultExpanded | boolean | number | ((group: GroupInfo) => boolean) | Which groups start expanded, before anyone has opened or closed one. `true` (the default) opens every group, `false` closes every group, a number opens the first N levels (`0` closes everything, a negative opens every level), and a predicate answers per group - the current sprint's section open while the rest start closed. Only ever consulted for a group nobody has touched: once the user or your code expands or collapses one, that decision stands. (optional) |
| grandTotalRow | boolean | 'bottom' | Where the grand total goes. `true` adds it as the last display row, counted by `rows.count()` like any other. `'bottom'` pins it beneath the viewport instead, so it stays in view while the rows scroll and is *not* part of `rows.count()`. Omitted or `false` means no grand total row. (optional) |
| pinnedTopRows | unknown[] | Rows pinned above the scrolling body. The objects are rendered through the ordinary column pipeline but are not part of the data: not counted by `rows.count()`, not sorted, filtered, grouped, selectable or exported. Use it for a totals line or a units row that must stay against the header. (optional) |
| pinnedBottomRows | unknown[] | Rows pinned below the scrolling body. As `pinnedTopRows`, at the other edge. (optional) |
| fullWidth | { when: (row: Row) => boolean; /** * Return a string for text, or a node for content. Return nothing and * write into `params.element` yourself. An HTML string is deliberately not * accepted: see `allowUnsafeTemplates` for that decision elsewhere. */ render: (params: FullWidthParams) => string | Node | void; } | Rows drawn as a single band across every column instead of being divided into them, a section banner, a note, a "load more" affordance. `when` picks the rows; `render` fills them. A full-width row is still one of your data rows: counted by `rows.count()`, sorted, filtered and exported like any other. Only its presentation changes. For a row that should *not* be part of the data, use `pinnedTopRows`. (optional) |
| totalFilteredOnly | boolean | Total what the filters left rather than the whole set. (optional) |
| totalOnlyChangedColumns | boolean | On a change, recompute only the totals whose column moved. (optional) |
| showTotalInHeader | boolean | Put the total in the header rather than a footer row. (optional) |
| aggregateChooser | boolean | Let the user pick a column's reduction from the column menu. On, the totalling entry becomes an "Aggregate" submenu offering the aggregates the column's type says are meaningful (); off, the menu keeps its plain "Total this column" toggle. Off by default, so an existing grid is unchanged. (optional) |
| columnVirtualisationAbove | number | Render only the visible columns once there are more than this many. (optional) |
| statusBar | boolean | { panels?: string[] } | The bar beneath the grid, and which panels it carries. (optional) |
| contextMenu | boolean | ((p: CellMenuParams, defaults: MenuItem[]) => MenuItem[] | void) | The cell right-click menu. A function supplies custom items; `false` suppresses it entirely, which is what a read-only grid wants, the default menu offers Paste, Clear and Fill down. (optional) |
| import | boolean | ImportSettings | Bringing rows in from a file, the clipboard or a drop (, the mirror of export). `true` adds a "Import rows from CSV…" item to the cell menu, makes the grid a drop target for `.csv`/`.tsv` files, and reads a pasted spreadsheet block, each opening a preview the user confirms. An object tunes the affordances. Off by default; the `grid.import` API is always present. Import is a client-side data operation, so it applies to a memory grid. (optional) |
| rowDelete | boolean | Enable the built-in row-delete gesture (, - the Delete/Backspace key on selected rows and a "Delete row" cell-menu item - and the `grid.edit.deleteRows` API. Off by default, because deleting data on a keystroke is destructive and opt-in. Every deletion flows through the cancellable `beforeDelete` event, so a handler can confirm or veto it, on a memory-source grid as well as a remote one. (optional) |
| columnMenu | boolean | ((p: ColumnMenuParams, defaults: MenuItem[]) => MenuItem[] | void) | The header's 3-dot menu, and the right-click menu on a column heading. `false` suppresses both. A function supplies custom items, receiving the grid's own so it can add to them rather than reproduce them. Default true. (optional) |
| rangeChart | | boolean | ((grid: Grid, range: CellRange) => void) | { onChart?: (grid: Grid, range: CellRange) => void } | Chart a selected cell range - the spreadsheet "chart this selection" gesture. Off by default, so a grid opts in. The DOM layer draws no charts itself - the charts module is optional and loaded by the host - so this is where the host wires the two together: a function, or an object carrying `onChart`, is called with the grid and the selected range when the reader chooses "Chart selection" from the cell menu. The handler typically calls `chartRange` from `lattice-grid/modules/charts`. `true` offers the item and emits nothing extra; supply a handler to have it actually draw. (optional) |
| shortcuts | boolean | The `?` keyboard shortcut overlay. `false` suppresses it, for a host that wants `?` for itself. Default true. (optional) |
| find | boolean | FindConfig | The in-grid find bar: Ctrl+F / Cmd+F with focus in the grid opens it; typing highlights every matching cell in place without filtering a row away; Enter and Shift+Enter step through the matches. `false` removes the bar and its shortcut; the `grid.find` API still works. Default true. (optional) |
| rowReorder | boolean | { column?: string } | Let a user reorder rows by dragging a handle, or with Alt+Shift+Up/Down. `true` puts the handle in the first visible column; `{ column }` names a different one. The move reorders your data and emits `row:moved`; persisting it is yours, and `rows.data()` afterwards is the new order. Refused, with a reason announced, while a sort, filter or grouping is active, the position a row is dropped at has no single meaning in the underlying order then. (optional) |
| rowTransfer | boolean | { send?: boolean; receive?: boolean; mode?: RowTransferMode; group?: string; } | Let rows be dragged out of this grid, into it, or both. Off by default: rows leaving a grid is a data change a host has to want, and a mis-drag that silently removed one has no gesture a user would think to undo. `send` and `receive` are both on when the option is present, so one-way is expressed by turning off the direction you do not want, a source grid is `{ receive: false }` and a target is `{ send: false }`. `mode: 'copy'` leaves the row where it was. `group` restricts exchange to grids sharing the same name, so two unrelated grids on a page do not accept each other's rows. The source needs `rowReorder` as well, since that is what draws the handle a drag starts from. (optional) |
| alignedGrids | unknown[] | Other grids to stay column-aligned with. Column widths, order, visibility and pinning are shared, and horizontal scrolling moves them together. Sort, filters, selection, grouping and the rows themselves stay independent: sharing those would make one grid with extra steps rather than two aligned ones. Declared on the grid created last, since it is the only one that can name the others; the link is peer-based once made. (optional) |
| stickyGroupHeaders | boolean | number | { depth?: number } | Keep the enclosing group headings pinned above the viewport while scrolling inside a group. Off by default - a deliberate product default; sticky group headers are opt-in. `true` turns it on, stacking at most two; a number, or `{ depth }`, sets how many may stack: each costs a row of viewport, so a deep grouping would otherwise spend the screen describing itself. `false` is off, the same as leaving it unset. (optional) |
| highlightOnChange | boolean | string | { colour?: string; color?: string; /** Milliseconds. `0` leaves the highlight until it is cleared. */ duration?: number; enabled?: boolean; } | Flash a cell when its value changes. `true` takes the defaults; an object names a colour, a duration in milliseconds, or both. (optional) |
| formatting | Record<string, FormattingRule[]> | Conditional formatting rules the grid holds as runtime state, keyed by column id or `'*'` for every column (spec 8.12). Seeds `grid.formatting`, which an end user can then change; the rules travel in saved views and undo like any other change. Config-time `cell.style` is unaffected. (optional) |
| rowClass | string | string[] | ((p: RowStyleParams) => string | string[]) | A class, or classes, for every row. Re-evaluated on each repaint. (optional) |
| rowStyle | CellStyle | ((p: RowStyleParams) => CellStyle) | Inline styles for every row. Camel-case or hyphenated property names. (optional) |
| toolPanel | boolean | { /** Built-in names: `columns`, `filters`, `views`, `quick`, `formatting`. */ panels?: ToolPanelName[]; openPanel?: string; /** Which edge to dock against. `left` is the icon rail; default `right`. */ side?: ToolPanelSide; /** Icon-only tabs. Defaults to true for `side: 'left'`, false otherwise. */ icons?: boolean; /** * Rail action buttons: `undo`, `redo`, `export`, `restore`. Defaults to all * four on the left rail and none on the right; `false` drops them. */ /** * Which action buttons the rail offers, in order. `false` drops them. * * The built-in names are `undo`, `redo`, `pause`, `restore`, `maximise`, * `export`, `excel`, `clipboard` and `print`, plus `'-'` for a divider. * A {@link RailAction} object places one of your own among them. */ actions?: false | (RailActionName | '-' | RailAction)[]; /** File name for the export action, without the extension. */ exportName?: string; /** * Put the native annotation tools - pen, arrow, rectangle, highlighter - on * the rail. Each is a real toggle button that shows pressed while it is the * tool in use and turns off when pressed again. Three states, not two: * `true` opts in and keeps the tools on the rail always, presentation or * not; `false` opts OUT and the tools are never added, not even for a * presentation; omitted keeps the default, where the tools are off until a * presentation starts, appear for its duration, and leave when it ends. */ annotate?: boolean; } | Dock the side panels against the grid: columns, filters, views, quick search and formatting. The columns panel is where row grouping, values and pivot are assembled by drag, which is why it carries three drop zones as well as the visibility list - a column opts out of each with `allowGroup`, `allowPivot` and `allowTotal`. (optional) |
| groupPanel | boolean | { /** Placeholder shown while nothing is grouped. */ hint?: string; } | A drag-and-drop group-by strip above the column header - the pattern AG Grid calls the row-group panel. Drag a column heading into it to group by that column; the active groups show as removable, reorderable chips, and reordering the chips changes the nesting order. It is keyboard-operable (arrows navigate, Shift+arrow reorders, Delete ungroups, and an add control groups any column), and every change is announced through the live region, which is why it also addresses the drag-only complaint. Off by default and non-breaking, matching `toolPanel`. It drives the same grouping model as `grid.columns.group()`; it reimplements nothing. (optional) |
| kpis | Array<Omit<StatConfig, 'grid' | 'container'>> | A built-in KPI/stat strip: a labelled band of {@link createStat} tiles the grid places for you, above the column header. Each entry is a stat spec - the same fields {@link StatConfig} takes, minus `grid` and `container`, which the grid supplies - so a strip tile and a hand-placed one are the same object. The tiles follow the grid's filters, recomputing on every change exactly as a stand-alone stat does. Off by default and non-breaking, matching `groupPanel`: no `kpis` means no band and no cost. It reuses `createStat` and reimplements no compute. (optional) |
| quickFilterText | string | The quick filter's initial text. (optional) |
| permissions | PermissionPolicy | Per-column read/write/hidden policy. A usability control, not a security boundary: hidden data is still resident in the store. Enforce the same policy server-side with `permittedColumns` / `permittedExport`. (optional) |
| diff | { snapshot?: unknown[] | Map<string, unknown>; strictNull?: boolean; addedColumns?: DiffAddedColumns; /** * Whether a row present in the snapshot but gone from the data is shown, * and whether it counts as data when it is. * * `false` (the default) leaves it out entirely. `'pinned'` shows it * beneath the rows, struck through: visible history that is not part of the * row set, so it is excluded from `rows.count()`, from exports and from * selection. `'data'` appends it to the row set instead, so it *is* * counted and exported. * * Neither is sorted or filtered among the live rows: a removed row's values * are the snapshot's, and ordering yesterday's numbers among today's would * present two data sets as one. Neither can be edited: there is nothing * left to write to. */ removedRows?: false | 'pinned' | 'data'; } | Prior state for diff and audit mode. (optional) |
| views | { storage?: { read(): unknown[]; write(views: unknown[]): void }; saved?: unknown[] } | Saved views: a storage adapter and any pre-loaded views. (optional) |
| historyBar | boolean | { element?: HTMLElement; timeline?: boolean } | The undo toolbar. `element` mounts it into the host's own chrome. (optional) |
| ai | { ask: (p: { /** The full text to send: the schema description and the question together. */ prompt: string; /** The grid's schema as data: columns, types and operators. No row values. */ schema: unknown; /** The same schema rendered as text, which is what `prompt` embeds. */ schemaText: string; /** What the user typed. */ message: string; context?: unknown; }) => Promise<unknown>; schemaOptions?: object; context?: unknown; element?: HTMLElement; placeholder?: string; } | The AI skill layer. The grid makes no network call of its own: `ask` is the host's, and owns the model, the key and the privacy decision. (optional) |
| pivot | { enabled?: boolean; /** * Add a column group totalling every value column across all pivot values, * the grand total beside the pivoted ones. `'before'` places it at the near * edge, `'after'` at the far edge. Omitted or `false` adds none. */ groupTotals?: PivotGroupTotalsPlacement | false; /** Heading for that group. Defaults to `Total`. */ totalsLabel?: string; maxColumns?: number; separator?: string; } | Pivot-mode settings: whether the grid starts pivoted, whether to add a group of grand-total columns beside the pivoted ones and what to call it, the separator joining a group's values in a generated column, and `maxColumns` - the ceiling on generated columns, 500 by default. Past it no pivot columns are produced at all and a clear error is reported, rather than a high-cardinality pivot locking the browser. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| environment | () => Record<string, unknown> | - | => Record<string, unknown> | Host environment for a support bundle. Supplied by the DOM layer; core cannot read `navigator` or `window` itself. (optional) |
| groupRenderer | (params: GroupRowParams) => string | Node | void | params: GroupRowParams | => string | Node | void | Draw the group row yourself. The grid's own group row is an expander, a label and a count. A host that needs more - a section header with a points rollup, a done/total count and a progress bar - supplies this instead, and owns the whole row: it is drawn as one band across every column, and no ordinary cells are mounted for it. Return an HTML string, or a node, or write into `params.element` and return nothing. Unlike `fullWidth.render`, a string here **is** inserted as markup, on the same footing as the board's `cardRenderer`: this is your own template for a row the grid synthesised, not a value out of your data. The chevron is yours to draw and yours to wire: give any element in your markup `data-lat-group-toggle` and a click on it expands or collapses the group, or call `params.toggle()` from a node you built yourself. (optional) |
Column
| Property | Type | Description |
|---|---|---|
| tags | string | string[] | Free-form labels for grouping columns together. A bare string is accepted for a single tag. Used by the column tag bar to show and hide sets of columns: tag sixty monthly columns with their year, and a user can switch to one year. (optional) |
| id | string | The column's own identity. Defaults to `field`; needed explicitly when two columns read the same field, as a value and its running total do. (optional) |
| field | string | The property to read from each row. Dotted paths reach into nested data. (optional) |
| title | string | The heading. Defaults to a readable form of `field`. (optional) |
| type | TypeName | false | The data type, which decides parsing, formatting, sorting, the default editor and the default filter together. `false` turns inference off and treats the values as opaque. (optional) |
| preset | string | string[] | Named column presets to merge in first, so a house style is declared once. (optional) |
| format | FormatSpec | string | How a value is rendered as text. A string is a shorthand mask. (optional) |
| lookup | LookupSpec | Display a stored code as a label, and edit it as a list. (optional) |
| value | ColumnValueSpec | A computed value, with the columns it depends on, in place of a stored one. (optional) |
| cell | ColumnCellSpec | string | The renderer, and what it is given. A string names a registered renderer. (optional) |
| edit | ColumnEditSpec | boolean | string | Whether and how the cell can be edited. A string names an editor. (optional) |
| validation | ColumnValidation | Declarative edit-validation rules. Each is checked against a value before it is written, through the `beforeEdit` before-event: a failing value cancels the commit and marks the cell. Distinct from and complementary to `edit.validate`, which is an imperative function. (optional) |
| sort | ColumnSortSpec | boolean | Whether the column sorts, and by what comparison. `false` refuses it. (optional) |
| filter | ColumnFilterSpec | boolean | FilterName | Whether the column filters, and with which filter. A string names one. (optional) |
| quickFilter | boolean | Whether the column takes part in the quick filter (the single search box across every column). Defaults to `true`, independently of `filter`: `filter.enabled` turns off the column's own funnel/menu control and has never governed quick search, which reads across columns rather than filtering one. Set this `false` to drop a column from quick search while leaving its funnel alone, or leave both alone for the common case. **Behaviour change:** before this, `filter: { enabled: false }` also removed the column from quick search as a side effect. A host that relied on that coupling to keep a column out of quick search must now set `quickFilter: false` explicitly; `filter.enabled` no longer touches quick search at all. (optional) |
| group | { enabled?: boolean; index?: number; explode?: boolean; granularity?: ColumnGroupGranularity; weekStart?: number; } | boolean | Row grouping by this column. `index` fixes its place among several; `explode` gives a multi-value cell one group per value rather than one group for the combination. `granularity` and `weekStart` apply to a `timestamp` column: it buckets by civil `day` (the default), `week` or `month` in the display zone, or `instant` for one group per exact moment. `weekStart` is the first weekday, 1=Monday (default) to 7=Sunday. (optional) |
| pivot | { enabled?: boolean; index?: number } | boolean | Use this column as a pivot dimension, and where it sits among several. (optional) |
| total | TotalName | TotalFn | The reduction shown in the totals row and in group footers. (optional) |
| groupTotal | TotalName | TotalFn | The reduction for group subtotals - group footers, tree-node rollups and pivot cells - where it should differ from the grand total. Overrides `total` for those scopes only; when omitted the column's `total` applies to both. Lets a column average within each group while the grand total sums, for example. (optional) |
| grandTotal | TotalName | TotalFn | The reduction for the pinned grand-total row, where it should differ from the group subtotals. Overrides `total` for the grand total only; when omitted the column's `total` applies. (optional) |
| shadow | ShadowKind | { of?: string; kind: ShadowKind; /** * For `kind: 'history'`, how many past readings to keep (20 by default). With * a time `window` this is instead how many buckets the span * divides into - `window: {kind: 'time', span: 60_000}, depth: 20` is sixty * one-second buckets. Ignored by every other kind. */ depth?: number; /** * For a positional kind, what to rank against. `'all'` (the default) uses * every tracked row, so a rank does not move when the grid is filtered; * `'filtered'` ranks within what the filters left. */ scope?: RowScope; /** * For `kind: 'anomalyFlag'`, the modified-z score a row must clear to be * flagged an anomaly. Default 3.5 (Iglewicz & Hoaglin). Ignored by * `anomalyScore`, which reports the raw score, and by the other kinds. */ threshold?: number; /** * For `kind: 'specStatus'`, the hard specification the row is judged * against. `lower`/`upper` are the pass limits (a value beyond either * fails); the optional `warnLower`/`warnUpper` are inner thresholds that * mark a still-in-spec reading `'WARN'`. Centred-target ± tolerance is a * deliberate follow-up and is not read here. */ lower?: number; upper?: number; warnLower?: number; warnUpper?: number; /** * For a rolling time-series kind (`rollingSum`/`rollingAvg`/`rollingMin`/ * `rollingMax`/`windowCoverage`/`cumulativeToDate`/`periodOverPeriod`, *, the column whose order defines the series - dates, * sequence numbers, timestamps. **Required**: the screen sort is never used, * because a rolling figure would then change on every header click, so a * rolling column with no `orderBy` reports null and warns. */ orderBy?: string; /** * For the rolling window kinds, the window to aggregate over: the last `span` * rows (`count`), the last `span` ms - or `minutes` - of the `orderBy` axis * (`time`), or everything so far (`session`). The first rows of a series * carry a partial window, stamped by a `windowCoverage` companion rather than * dressed as full. * * For `kind: 'history'`, only `{kind: 'time', span}` (or * `minutes`) applies, and it changes what `history` means rather than what it * aggregates: the `depth` buckets that span divides into are read once each, * carrying the row's last known value forward into any bucket in which it did * not change, so a static row still draws a flat, advancing line instead of * freezing - the plain count-based history (no `window`) is a count of * *changes* and stays exactly as it was. `count` and `session` are refused * here: a plain count is already what `depth` means, and a session has no * fixed span to divide into buckets. */ window?: { kind: WindowKind; span?: number; minutes?: number; }; /** * For a rolling kind, whether the series is computed per group (`'group'`, * the default - partitioned by the grid's active grouping) or across the * whole dataset (`'all'`). */ within?: ShadowWithin; /** * For `kind: 'rollingQuantile'` (and its `windowApproximate` companion), the * quantile in `[0, 1]`, defaulting to the median (`0.5`). Exact while the * window is small; past an internal span cap, and for a session window, the * value comes from a sketch and is stamped by a `windowApproximate` column. */ q?: number; /** * For a seasonal-decomposition kind (`tsTrend`/`tsSeasonal`/`tsResidual`/ * `tsCoverage`,, the season length - **required**, since * there is no auto-detection in v1: 7 for a weekly cycle in daily data, 12 * for a monthly cycle in monthly data. An integer of at least 2. */ period?: number; /** * For a decomposition kind, the classical model: additive by default, or * `multiplicative` (which is undefined on a non-positive series, so those * rows report null and the caller is warned). */ decomposition?: ShadowDecomposition; /** * For an exponential-smoothing kind (`tsSmoothed`/`tsSmoothingAlpha`/ * `tsSmoothingBeta`,, the model: single exponential * smoothing (`ses`, the default) or Holt's level+trend (`holt`). */ smoothing?: SmoothingMethod; /** * For a smoothing kind, the level factor in `[0, 1]`. Omit to fit it by * minimising in-sample SSE; the chosen value is reported by a * `tsSmoothingAlpha` column. */ alpha?: number; /** * For `smoothing: 'holt'`, the trend factor in `[0, 1]`. Omit to fit it; * reported by a `tsSmoothingBeta` column. */ beta?: number; /** * For a `fit*` kind, the regression model the shadow reads * - predictors, response, method and confidence. Its predictors/response may * also be given directly on this object. */ model?: RegressionSpec; predictors?: string[]; response?: string; method?: RegressionMethod; } | A value the grid maintains about this column's own history, rather than a field in the data. `{of: 'price', kind: 'delta'}`, or the bare kind to shadow the column it sits beside. (optional) |
| running | RunningTotalMode | { of?: string; kind?: RunningTotalMode } | A running total down the grid **as it is currently ordered**. The one derived value that depends on the display order: sort differently and every value changes. That is why it is not a shadow kind: every shadow reads the same however the rows are arranged. (optional) |
| spec | { lower?: number; upper?: number; target?: number } | The customer's tolerance, for process capability and control charts. Declared here rather than passed to each call so the capability figures, a control chart and any rule marking an out-of-tolerance cell cannot disagree about what the tolerance is. (optional) |
| layout | ColumnLayoutSpec | number | Width, pinning and flex. A bare number is the width in pixels. (optional) |
| header | ColumnHeaderSpec | string | The header cell: its text, tooltip, menu and any header chart. (optional) |
| contextMenu | boolean | MenuItem[] | ((p: CellMenuParams, defaults: MenuItem[]) => MenuItem[] | void) | The cell right-click menu for this column alone, in the same shapes the grid-level `contextMenu` takes plus a bare array for the common "just these items here" case. Declared where the column is declared rather than as another branch inside one grid-level callback: the menu logic for a column belongs beside the column it belongs to. It does not replace the grid-level menu - the three levels compose as a chain, built-in defaults then grid-level then this one, each handed the previous result as its `defaults`, so a column adding one item does not have to restate Paste, Clear and Fill down. `false` suppresses the menu on this column and leaves every other column alone: what a sensitive or read-only column wants. The more specific level wins, so a column may also declare a menu on a grid whose `contextMenu` is `false`. (optional) |
| headerControls | HeaderControlsVisibility 'hover' | 'always' | 'hidden' | 'none' | When this column's header controls - its sort arrow, filter funnel and menu button - are shown, overriding the grid-level `headerControls` default for this column alone. `'hover'` reveals them on hover or focus, `'always'` keeps them visible, `'hidden'` draws none of them and leaves them out of the tab order, but still shows a read-only sort badge when the column actually is sorted. `'none'` goes further: the heading shows its title and nothing else whatever the grid's state - no controls, and not even `'hidden'`'s read-only sort badge; the column's `aria-sort` still reports the truth, only the visual is gone. Omitted, the column follows the grid default, which is itself `'hover'`. (optional) |
| verticalAlign | VAlign 'top' | 'middle' | 'bottom' | Vertical alignment of this column's cell content within the row. Overrides the grid-level `verticalAlign` for this column alone; `top`, `middle` or `bottom`. Also accepted as `cell.verticalAlign`, the way `align` is. Omitted, the column follows the grid default. (optional) |
| showWhen | ShowWhen 'open' | 'closed' | 'always' | When this leaf column is shown, the same union `ColumnGroup` declares. A leaf reads its own `showWhen` exactly as a group reads its own - `open`/`closed` tie the leaf to an ancestor group's collapsed state, `always` (the default) shows it regardless - so tying a leaf's visibility to a group's open/closed state does not require wrapping it in a `ColumnGroup` of its own just to hold this setting; a wrapper is for grouping columns, not for this. (optional) |
| export | ColumnExportSpec | How the column leaves the grid, where that differs from how it is shown. (optional) |
| allowGroup | boolean | Whether the user may group by this column from the interface. (optional) |
| allowPivot | boolean | Whether the user may pivot on it. (optional) |
| allowTotal | boolean | Whether the user may put a total on it. (optional) |
| nullable | boolean | Whether an empty value is a legitimate value rather than a gap. (optional) |
ColumnGroup
| Property | Type | Description |
|---|---|---|
| id | string | A stable identity for the band, which `renameGroup`, `dissolveGroup` and `moveGroup` take and which a saved view keys the band's open state by. The grid generates one when you do not. (optional) |
| title | string | The heading drawn across the band. Also the fallback identity when the band has no `id`. |
| columns | (Column | ColumnGroup)[] | The band's children, in order: leaf columns, further bands, or a mix - the header nests as deeply as the tree does. |
| collapsible | boolean | Give the band a control that opens and closes it. Off by default; a band that is not collapsible passes its ancestor's open state straight down. (optional) |
| openByDefault | boolean | Whether a collapsible band starts open. Open unless set to `false`; once the user has toggled it, their choice stands. (optional) |
| showWhen | ShowWhen 'open' | 'closed' | 'always' | When this node is shown relative to the enclosing collapsible band: `'open'`, `'closed'`, or `'always'` (the default). This is how a band shows a detailed set of columns when open and a single summary column when closed. A leaf column may declare it too. (optional) |
| marryChildren | boolean | Keep this band's columns together: a move that would take one of them out of the band's run, or drop a column from outside into it, is refused with a warning. Off by default, in which case a drag, a keyboard move or the column menu may separate them. (optional) |
| header | { render?: string | RendererCtor; props?: Record<string, unknown>; class?: string | string[] } | Presentation for the band's own heading cell: a custom renderer, the props handed to it, and classes to add. (optional) |
| facet | ColumnFacetConfig | boolean | This column's histogram. `true` turns it on with the grid's settings. (optional) |
MemorySourceConfig
| Property | Type | Description |
|---|---|---|
| mode | 'memory' | Selects the in-memory source: the grid holds every row and answers sort, filter, group, pivot and totals itself. |
| columnarBelow | number | The row count below which the grid skips columnar storage and keeps plain row objects. 5,000 by default - columnarising a small grid costs more than it saves. (optional) |
| rows | unknown[] | The rows a memory source opens with; equivalent to top-level `rows`, which wins if both are given. (optional) |
PagedSourceConfig
| Property | Type | Description |
|---|---|---|
| mode | 'paged' | Selects the paged source: block-based lazy loading over a flat list, with sorting and filtering delegated to the server. |
| pageSize | number | How many rows are fetched per block. 100 by default. (optional) |
| maxCachedPages | number | How many blocks are kept before the least recently used ones outside the viewport are evicted. 32 by default. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| fetch | (req: { range: { start: number; end: number }; sort: SortEntry[]; filters: FilterSet; quick?: string; context: unknown; signal: AbortSignal; }): Promise<{ rows: unknown[]; total?: number }> | req: { range: { start: number; end: number }; sort: SortEntry[]; filters: FilterSet; quick?: string; context: unknown; signal: AbortSignal; } | Promise<{ rows: unknown[]; total?: number }> | Answer one block: the rows for the requested range, and the total when known. Changing the sort or the filters invalidates every block, since the server may return an entirely different window for the same range. |
RemoteSourceConfig
| Property | Type | Description |
|---|---|---|
| mode | 'remote' | Selects the remote source: the grid holds a window of rows and asks the server to sort, filter, group, pivot and total. |
| pageSize | number | How many rows are fetched per block. 100 by default. (optional) |
| maxCachedPages | number | How many blocks are kept before the least recently used ones outside the viewport are evicted. 32 by default. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| fetch | (req: RemoteRequest): Promise<RemoteResult> | req: RemoteRequest | Promise<RemoteResult> | Answer one block. The request is a published protocol - version, range, group path, sort, filters, pivot, totals and the abort signal - so a server implementation targets the documented shape rather than guessing. |
StreamSourceConfig
| Property | Type | Description |
|---|---|---|
| mode | 'stream' | Selects the streaming source: rows arrive over time and the grid keeps rendering as they land. |
| maxRows | number | The most rows to keep. A stream has no end, so an unbounded grid dies overnight; this makes it a sliding window and the oldest rows are dropped. Omit for no limit. Set on the source, not passed to `open`, it bounds what the grid retains rather than what the producer sends. (optional) |
| maxAge | number | The longest a row is kept, in milliseconds - a rolling *time* window, sitting beside `maxRows` as a second, independent bound. Rows older than the span are evicted through the same path, the same `evicted` counters and the same `stream:evicted` event as the count bound, so an existing readout keeps working. Set both and whichever bites first applies. Eviction continues on a low-frequency timer while the feed is idle, so "the last five minutes" keeps shrinking through a silent period rather than freezing - which is the thing `maxRows` cannot do. Retention is a *bound, not a guillotine*: rows live a little past the span before a block is dropped. Two things add to it. First the eviction slack, ten per cent of the span, exactly as `maxRows` overshoots its count, so the row permutation is rebuilt once per block rather than once per row. Second, when the feed is idle, up to one tick of the eviction timer, which runs at a quarter of the span clamped to between 50 ms and one second. So the real ceiling is roughly `span * 1.1 + tick`, and because the tick has a floor it is proportionally larger the shorter the window: negligible at a five-minute window (about 10%), around 1.25x at ten seconds, and as much as ~1.35x at three. That is the deliberate trade for an idle grid that costs no CPU. Omit for no age limit. (optional) |
| ageBy | string | ((row: unknown) => unknown) | Which clock `maxAge` reads: a column id (or dotted path), or a function of the row returning a `Date`, epoch milliseconds, or an ISO string. Given, the window follows the **data's own** clock, so it means what the producer means - and inherits the producer's clock skew. Omitted, `maxAge` falls back to **arrival time**: when the row reached this source. Arrival time needs no timestamp column and cannot be skewed, but it is not event time - a row delayed in transit counts as young. A row whose time value cannot be read is never aged out. (optional) |
| promoteToMemoryBelow | number | When the feed ends with fewer rows than this, the grid adopts them as an ordinary in-memory source, so later sorts and filters are local. Defaults to 250,000. A store-backed stream is never promoted. (optional) |
| coalesceMs | number | How long, in milliseconds, the grid may spend applying arriving chunks per frame. Defaults to 8 - roughly one frame's budget. Raise it to take more rows per frame at the cost of responsiveness. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| open | (req: { sort: SortEntry[]; filters: FilterSet; quick?: string; context: unknown; signal: AbortSignal; }): AsyncIterable<Chunk> | req: { sort: SortEntry[]; filters: FilterSet; quick?: string; context: unknown; signal: AbortSignal; } | AsyncIterable<Chunk> | Opens the feed and returns an async iterable of chunks. It is called with the grid's current sort, filters, quick-filter text and context, and an `AbortSignal` that fires when the query changes or the grid is destroyed - stop producing when it does. Backpressure is the iterator protocol's: the grid awaits the next chunk. |
DerivedSourceConfig
A grid whose rows are derived from another grid: aggregated, unnested, filtered, ranked or profiled. Read-only: write to the source instead.
| Property | Type | Description |
|---|---|---|
| mode | 'derived' | Selects the derived source: this grid's rows are computed from another grid's, and re-derived when that one changes. |
| from | Grid | UnionSourceOptions[] | The grid to read, or several to combine into one row set before the rest of the pipeline runs. A bare `Grid` is shorthand for a `UnionSourceOptions` with no `label`/`follow`/`map` override, so an existing `from: <grid>` keeps meaning exactly what it always has. Given an array, every source is read (each narrowed by its own `follow`, defaulting to `'filtered'` as a lone `from` does today), concatenated in **declaration order** - deterministic, not interleaved - and only then does `unnest`/`join`/`where`/`bucket`/`groupBy`/`select`/`sort`/`limit`/ `limitPer`/`cumulative` run, over the combined set, so "the worst performers across both" is one derivation rather than a hand-merge. The output carries the **union of the sources' fields**: a field present on only one source is `undefined` on rows from the others. Sources are **not** type-reconciled - if two disagree on what a field means or holds, that is not resolved for you; give each source a `map` to project it into a common shape first. Every row also carries `__source` (the entry's `label`, or its declaration index when unlabelled), which is required - not optional - because without it a combined list cannot be read, filtered or grouped by where it came from; it is an ordinary field to `where`, `groupBy` and `select`. And because the derived key (`__key`) would otherwise collide across sources sharing the same identifiers, it is namespaced by the same source tag when nothing is grouped (a grouped union's `__key` is the group value, exactly as today, and rows from different sources correctly land in the *same* group when their group values agree - that merging is the point of grouping a union, not a collision to guard against). This is **not** a join: there is no dedup or merge-on-key, and it draws no UNION/UNION ALL distinction - overlapping rows from two sources simply both appear. Reach for `join` when two sides share a key and you want them matched rather than stacked. An empty source contributes nothing and the rest still combine; a source that fails to read is named in a `warnOnce` and skipped for that pass rather than silently dropped, because a silently missing source would make "worst across both" quietly wrong. A source list that includes the grid being derived, directly or through a chain, is refused when the source is built (naming the offender) rather than recursed into. `crossFilter` has no single target once there is more than one parent, so it is not supported alongside a union `from` (ignored, with a `warnOnce`, rather than guessing which parent to push onto). |
| follow | DerivedFollow 'filtered' | 'all' | 'selected' | 'grouped' | Which of its rows to read. `filtered` by default. Ignored - with a `warnOnce` - when `from` is a union array: each entry there carries its own `follow` instead. (optional) |
| unnest | string | An array property to expand, one row per element, before anything else. (optional) |
| join | DerivedJoin | Match each row against a second grid on a shared key, and bring some of its fields across. Runs after `unnest` and before `where`, so a condition: and a grouping, and a total: can read a field the join produced. (optional) |
| bucket | { of: string; by: 'day' | 'week' | 'month' | 'quarter' | 'year' } | Round a date column down to a period, and group on that. (optional) |
| groupBy | string | string[] | The dimension, or dimensions, to group by. Omit to pass rows through. (optional) |
| select | Record<string, DerivedSelect> | The reduced columns, by output id. (optional) |
| sort | { col: string; dir?: 'asc' | 'desc' }[] | How to order the derived rows before limiting them. (optional) |
| limit | number | Keep at most this many rows. (optional) |
| limitPer | string | Apply `limit` within each distinct value of this column, not overall. (optional) |
| cumulative | { of: string; upTo: number } | Keep rows until their running share of the total reaches `upTo`, 0 to 1. (optional) |
| profile | string | string[] | One row per column, with the statistics as columns. Replaces the pipeline. (optional) |
| orient | DerivedOrient 'columns' | 'metrics' | With `profile`, emit one row per statistic instead of one per column. (optional) |
| statistics | DerivedStatistics | Project a **relational** statistic into rows: the figures that need two or more columns, or a second grid, and so cannot be reached through `select`. Every *single-column* statistic already has a route and this is not it - the derived `select` reduces a group by any kernel the totals row uses, and that table is a superset of the statistics one, so `select: { p95: { of: 'amount', fn: 'p95' } }` (or `gini`, `stddev`, `median`, `trimmedMean`, …) works today. Reach for `statistics` only when the answer is a correlation, a series summary or a comparison against another dataset. **A terminal producer, like `profile`, not a pipeline stage.** A correlation is one row per column *pair*, a series summary one row per *metric*, a comparison one row per compared *column* - none of which is one row per group, so there is no position in `unnest → where → bucket → groupBy → select → sort → limit` for it to occupy. It replaces the pipeline, and those keys are ignored with a warning naming them rather than silently discarded. Sort, filter or limit the derived grid itself instead, or chain a second derived grid whose `from` is this one. **`profile` and `statistics` are mutually exclusive** and declaring both is refused, by name, when the source is built. **Not supported alongside a union `from`** - a relational statistic reduces one grid's own columns and a union has no single set of them; also refused by name. **Cost.** Like every terminal producer this never patches incrementally: a change on the parent re-derives the whole thing. `correlation` additionally scans the rows once *per pair*, so N columns cost N·(N−1)/2 passes. Use `refresh` (`'idle'` is the default; `'manual'` or a debounce in ms for an expensive analysis over a live feed; under `'manual'` the host re-derives by calling `rows.load()` on the derived grid) - see `docs/api-detail.html` for the measured figures. Every row carries `n`, the rows the figure covered, because a derived statistic travels into an export or a chart without its grid and "r = 0.98 over eleven rows" is a different claim from the same number over eleven thousand. It does NOT carry a windowed/approximate flag: whether a source held fewer rows than matched its filters is decided from the source's own counters, which a derived source cannot reach, so that signal stays where it already works - the `stat.windowed:*` console warning the parent grid emits. (optional) |
| refresh | DerivedRefresh | number | When to re-derive. `idle` by default: coalesced to a frame. A number debounces by that many milliseconds; `live` re-derives on every change. `manual` never re-derives on its own: the host triggers it by calling `rows.load()`, with no argument, on the derived grid - from a Refresh button, say. Each call re-reads `from` there and then and replaces the rows; a derived grid takes its rows from `from`, so anything passed to `load` is not used. Executed example: `docs/api-detail.html#derived-manual-refresh`. (optional) |
| crossFilter | boolean | string | { col?: string } | Let this grid filter the grid it derives from. `true` cross-filters through whatever it groups by; a string names a different source column. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| where | (row: unknown) => boolean | row: unknown | => boolean | A row predicate, applied before grouping. (optional) |
IngestConfig
How rows are ingested into the column store.
| Property | Type | Description |
|---|---|---|
| retainSource | boolean | Retain the caller's row objects by reference so identity round-trips. Default `true`, the historical behaviour: `rows.data()` returns the exact objects you supplied, `row === sourceObject` holds, and a custom renderer reading `row.sourceObject` works. Set `false` to keep only the packed columns and reconstruct a plain row object from them on demand. This drops roughly half the resident footprint, but changes three behaviours: `rows.data()` returns freshly reconstructed objects (new object each call, so `row === sourceObject` no longer holds), a custom renderer that reaches for `row.sourceObject` gets a reconstruction rather than the original, and equality against a row becomes value-based. The stored values are unchanged, so `get()`, `byKey()`, `value()` and `values()` are unaffected. (optional) |
| dropSourceRows | boolean | Release the caller's row objects from the *source layer* once the column store has been built, so the columns become the sole resident copy of the data. Default `false`, which keeps today's behaviour. `retainSource:false` stops the {@link https://en.wikipedia.org/wiki/Column-oriented_DBMS column store} from holding the caller's objects, but the memory source and the grid config still retain the supplied array by reference - so the objects stay alive and the resident footprint does not actually fall. This flag closes that gap: it clears `MemorySource`'s retained array and drops the array from the grid config, leaving nothing on the heap but the packed columns. That is where the large reduction comes from (roughly an order of magnitude at a million rows), not from `retainSource` on its own. Implies `retainSource:false`: dropping the caller's objects while the store still expects to read through them would leave the source with no data at all, so setting this on forces the store to reconstruct rows from columns. Every read is therefore served from the columns - `at()`, `byKey()`, `get()`, `value()`, `values()`, filtering, sorting, grouping, totals and export are all unaffected in their values. What changes is the same three identity behaviours `retainSource:false` documents: `rows.data()` returns freshly reconstructed objects (so `row === sourceObject` no longer holds), a custom renderer reaching for `row.sourceObject` gets a reconstruction, and equality against a row becomes value-based. One consumer cannot be served from the columns: an *impure computed column* (a shadow, or a rank/positional column) is deliberately never materialised into the store, so its handle is built by reading the source objects. Under `dropSourceRows` those objects are gone, so such a column reduces over nothing and warns once rather than returning a silently wrong figure. Do not enable `dropSourceRows` on a grid that sorts, filters, groups or totals on a shadow or a positional column. (optional) |
| useWorker | boolean | Columnize `stream`-source ingest on a Worker so a large load does not block the main thread. Default `false`. When on, an arriving chunk that clears {@link IngestConfig.workerThreshold} is packed into typed column buffers on the Worker; the main thread merges the finished buffers into the store and renders, without running the per-field extraction pass that otherwise dominates ingest. This makes **stream** ingest non-blocking (remote sources already are). Memory and paged sources cannot be made non-blocking this way - the main thread must read the caller's own row objects - and are unaffected. The effect composes with `retainSource: false`: with it off the source keeps no caller-object array on the main thread at all, so the load is both non-blocking and lighter on memory. A column that reads through a closure - a `date` column's storage conversion, or a computed column - cannot cross the Worker boundary, so a grid with any such column columnizes on the main thread and says so once. Falls back silently to the main thread wherever a Worker cannot be created. (optional) |
| workerThreshold | number | Row count in a single stream chunk at or above which columnization is offloaded to the Worker when {@link IngestConfig.useWorker} is on. Default `10000`. A smaller first chunk is packed on the main thread, where the cost is trivial and the postMessage round trip would only add latency to time-to-first-row. (optional) |
DataType
| Property | Type | Description |
|---|---|---|
| base | DataTypeBase 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object' | The storage family the type belongs to, which decides how a value is held, compared and exported. Inherited through `extends`, and `'text'` when neither says. |
| extends | TypeName 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object' | 'lookup' | 'image' | The type this one inherits from. `defaults` are merged rather than replaced, so a derived type can override one default and keep the rest. A circular chain fails at construction. (optional) |
| compare | Comparator | Order two values of this type. Used for sorting unless the column supplies `value.compare` or is lookup-backed. (optional) |
| defaults | { filter?: FilterName; editor?: EditorName; total?: TotalName; align?: Align; /** The cell renderer this type's values are drawn with by default. */ render?: RendererName; } | What a column of this type gets for free - its filter kind, editor, footer total, alignment and cell renderer - unless the column, a preset or `columnDefaults` says otherwise. `type: false` on a column turns this layer off. (optional) |
| storage | DataTypeStorage 'float64' | 'int32' | 'bitset' | 'dictionary' | 'object' | How the column store holds these values in bulk: a typed array, a bitset, a dictionary of codes, or plain objects. `'object'` when unset. (optional) |
| totals | { /** * The aggregates that mean something. A column of this type configured * with any other fails at construction rather than rendering a confident * wrong number. */ supported?: TotalName[]; /** * The type's own reduction for an aggregate, replacing the built-in * arithmetic. Receives the values index-aligned with their rows, and a * context carrying `column` and `valueAt(colId, i)` for reading another * column of the same row. */ implement?: Record<string, TotalFn>; } | Which aggregates are meaningful for this type, and how. Omit it and every aggregate is allowed, which is what every type that shipped before this does. (optional) |
| excel | string | The Excel number format an export writes for this type, when the column's own formatter supplies none. `'General'` when neither does. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| matches | (value: unknown) => boolean | value: unknown | => boolean | Recognise a value as belonging to this type, for type inference from a sample. A type is inferred only when every sampled value matches. (optional) |
| format | (p: FormatParams) => string | p: FormatParams | => string | Turn a value of this type into display text. Last in the display chain, behind the column's `value.format` and its `format` spec. (optional) |
| parse | (p: ParseParams) => unknown | p: ParseParams | => unknown | Read the formatted form back into a value - what a paste and a text editor go through. It is a *text* parser: the grid does not put an already-typed editor value through it. (optional) |
| toClipboard | (v: unknown) => string | v: unknown | => string | Turn a value of this type into the text a copy puts on the clipboard. First in that chain, ahead of the grid-wide hook and the plain display text. (optional) |
| fromClipboard | (s: string) => unknown | s: string | => unknown | Read pasted text back into a value of this type. First in that chain, ahead of the grid-wide hook and taking the text unchanged. (optional) |
Renderer
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| init | (p: CellParams): void | p: CellParams | void | Prepare the renderer for a cell, before `element()` is asked for. Called once per mounted cell. |
| element | (): HTMLElement | - | HTMLElement | The DOM the grid should put in the cell. Called once, straight after `init`. |
| refresh | (p: CellParams): boolean | p: CellParams | boolean | Update the existing DOM for a new value and return `true`. Returning anything else - or not implementing it - makes the grid tear the cell down and build it again, which is the single biggest cost on an update-heavy screen. (optional) |
| attached | (): void | - | void | Called once the element is in the document, for anything that needs real layout - a measurement, a chart draw, focus. (optional) |
| destroy | (): void | - | void | Release whatever the renderer holds - timers, observers, listeners - as the cell is recycled. A throw here is reported once per column and does not stop the recycle. (optional) |
Editor
| Property | Type | Description |
|---|---|---|
| popup | boolean | Mount the editor in the grid's overlay layer, positioned over the cell, so it escapes the cell's clipping. Otherwise it is mounted inside the cell. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| init | (p: EditorParams): void | p: EditorParams | void | Prepare the editor for one edit session, before `element()` is asked for. It receives the cell, the current value, the column and the merged `edit.props`. |
| element | (): HTMLElement | - | HTMLElement | The DOM the grid mounts for the session - inside the cell, or in the overlay layer when `popup` is set. |
| value | (): unknown | - | unknown | The value to commit, in the column's stored form. Read when the session ends without a cancel. |
| attached | (): void | - | void | Called once the element is in the document. Where a popup is shown and focus is placed, rather than in `init`, so nothing is mounted for an editor that refuses to open. (optional) |
| cancelBeforeStart | (): boolean | - | boolean | Return `true` to refuse to open on this cell, so keyboard navigation moves on instead of opening an editor the user cannot use. The built-in editors refuse a read-only cell this way. (optional) |
| cancelOnClose | (): boolean | - | boolean | Return `true` when the session ends to discard the edit instead of committing it - the counterpart of `cancelBeforeStart`, asked once, on the commit path every route ends at (Enter, Tab, and clicking away). The built-in editors answer `true` after their own `cancel()`; an editor may also discard at any moment by calling `params.stop(true)`. (optional) |
| destroy | (): void | - | void | Release whatever the editor holds as the session ends, however it ended. (optional) |
Filter
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| init | (p: FilterParams): void | p: FilterParams | void | Set the filter up for a column, once, before its element is asked for. The params carry the column, the grid, a value getter and the filter's own `props`. |
| active | (): boolean | - | boolean | Whether the filter is actually excluding anything. A set filter with everything ticked is not filtering, and says so. |
| passes | (p: { row: Row; data: unknown }): boolean | p: { row: Row; data: unknown } | boolean | Whether a row survives. Evaluated through the same condition the server would be sent, so local and remote agree on what the filter means. |
| get | (): unknown | - | unknown | Serialise the filter to a condition node for the filter set - what travels to a server and into a saved view. `null` when the filter is not active. |
| set | (state: unknown): void | state: unknown | void | Restore the filter from a condition node, so a saved view puts the control back where it was. A null node resets it. |
| element | (): HTMLElement | - | HTMLElement | The filter's own UI, which the grid mounts in the column's filter popup. |
| onRowsChanged | (): void | - | void | Called when the rows changed, for a filter whose control is built from the data - a set filter's value list. A list that came from a lookup or the host is left alone. (optional) |
Row
| Property | Type | Description |
|---|---|---|
| key | string | What identifies the row. Selection, expansion and edits are all keyed on it. |
| data | unknown | null | The object you supplied. Null on a group heading, which is a product of the grouping rather than a record. |
| level | number | Depth in a tree or a grouping. Zero at the top. |
| parent | Row | null | The row above it in a tree or grouping, or null at the top. |
| children | Row[] | Every child, before filtering. (optional) |
| filteredChildren | Row[] | The children the filters left. (optional) |
| sortedChildren | Row[] | The children in display order. (optional) |
| group | boolean | Whether this is a group heading rather than a record. A heading carries no data and must be skipped when totalling. |
| expanded | boolean | Whether its children are showing. |
| leafCount | number | How many records sit beneath it, at any depth. |
| totals | Record<string, unknown> | The group's own reductions, by column id. (optional) |
| detail | boolean | Whether this row is the expanded detail panel of the one above. (optional) |
| master | boolean | Whether this row has a detail panel. (optional) |
| height | number | The row's height in pixels, as measured or configured. |
| index | number | null | Position in the display order, or null when off screen. |
| selected | boolean | 'partial' | Selection state. `partial` is a group some but not all of whose children are selected. |
| physical | number | null | Physical index into the ColumnStore. Null for synthetic rows. (optional) |
| groupColumn | string | Group rows only: the column id this level groups on, and the group value. (optional) |
| groupValue | unknown | The value this group heading stands for. (optional) |
| groupPath | string[] | Stable path of group keys from root to this row. (optional) |
| hasChildren | boolean | Whether children exist, which a lazily loaded tree knows before it has them. (optional) |
| pinned | RowPin 'top' | 'bottom' | Which sticky strip this row is pinned in, when it is one the host pinned through `setPinnedRows`. Absent on every row that is part of the data. (optional) |
VariantDefinition
| Property | Type | Description |
|---|---|---|
| light | { fill: string; text: string; border: string } | The three colours the token uses in a light theme: the `fill` behind it, the `text` on it, and the `border` round it. The border doubles as the strong marker colour a dot or a bar takes. |
| dark | { fill: string; text: string; border: string } | The same three colours for a dark theme. Applied both when the grid is explicitly dark and when the reader's own system preference is. |
TreeConfig
| Property | Type | Description |
|---|---|---|
| parentKey | string | ((row: unknown) => unknown) | Name each row's parent by key, which is the shape a join or a document store produces. Every node is then a real row; a parent that is not in the data makes an orphan, and a cycle is broken rather than followed forever. (optional) |
| orphans | 'root' | string | Where a row whose parent is missing goes. `'root'` (the default) leaves it at the top level; any other string is the title of a synthesised bucket that gathers them, so they are visibly gathered rather than silently promoted. Rows caught in a parent cycle go the same way, and are reported. (optional) |
| label | string | ((data: unknown, row: Row) => unknown) | Where the generated tree column takes its text from: a field or a function. (optional) |
| title | string | The tree column's heading. Defaults to the label column's own title. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| path | (row: unknown) => string[] | row: unknown | => string[] | Read a row's own ancestry - `['EMEA', 'UK', 'Colchester']`. Levels with no row of their own are synthesised, so a row can sit several levels deep without its parents existing in the data. (optional) |
| hasChildren | (row: unknown) => boolean | row: unknown | => boolean | Say that a row has children the grid has not seen yet, so it draws an expander for a branch that is still to be fetched. Without it, only rows with children already in the data can be opened. (optional) |
| loadChildren | (row: Row, signal: AbortSignal) => Promise<unknown[]> | row: Rowsignal: AbortSignal | => Promise<unknown[]> | Fetch a branch's children when it is first opened, with an abort signal for a branch closed before they arrive. Fetched once: a branch reopened shows what it already has. The rows are added to the grid like any others, so they sort, filter and export normally. A failure is announced and the branch is left unmarked, so the user can retry by opening it again. (optional) |
DetailConfig
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Turn master-detail on. `detail: true` is the shorthand; `enabled: false` turns it off without removing the rest of the block. (optional) |
| render | string | RendererCtor | Draw the detail region yourself - a renderer, or the name of a registered one. One of `render` or `rows` is required, or the feature stays off and says so. (optional) |
| config | GridConfig | The grid configuration for the nested grid the detail region builds from `rows`. Columns, formatting, everything a grid takes. (optional) |
| height | number | 'auto' | ((row: Row) => number) | How tall the detail region is, in pixels: a number, a function of the master row, or `'auto'` to measure it once it has content. 240 px by default. (optional) |
| cacheLimit | number | How many nested grids stay alive after their master is closed, so collapsing and reopening does not refetch. 10 by default; beyond it the least recently opened are destroyed, because one grid per row is a memory leak with a friendly name. (optional) |
| target | string | HTMLElement | Render the detail into this element instead of into a row beneath its master. A selector or an element. Exactly one detail is open at a time in this placement. (optional) |
| path | string | The property of the master's record the detail rows live on, so an edit in the detail is reported as a path on the master: `ports.1.vlan`. Inferred by identity when `rows(row)` returns an array already on the record, which is the usual shape; set this when it does not. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| rows | (row: Row) => unknown[] | Promise<unknown[]> | row: Row | => unknown[] | Promise<unknown[]> | Produce the detail rows for a master, synchronously or as a promise. The grid puts them in a nested grid built from `config`. (optional) |
| isMaster | (data: unknown, row: Row) => boolean | data: unknownrow: Row | => boolean | Decide which rows can be expanded. Every data row can when this is absent; group rows and detail rows never can. (optional) |
| onCreate | (grid: Grid, masterRow: Row) => void | grid: GridmasterRow: Row | => void | Handed the nested grid as it is created, for whatever the forwarded events do not cover. (optional) |
SelectionConfig
| Property | Type | Description |
|---|---|---|
| mode | SelectionMode 'none' | 'single' | 'multiple' | `'none'` also turns off `ranges` and `fillHandle` unless either is set explicitly alongside it. (optional) |
| checkbox | boolean | Add a column of checkboxes down the start of the grid. The grid generates it: it is not one of your columns, so it is never exported, never in the tool panel, and it disappears when the option is turned off. (optional) |
| headerCheckbox | boolean | Put a select-all checkbox in that column's heading, showing the tri-state over the displayed rows. (optional) |
| checkboxOnly | boolean | Only the `checkbox` column may change row selection - a click anywhere else in the row, and Space with focus anywhere but the checkbox, leave selection untouched. Range and cell selection are unaffected either way. For a host whose row click is bound to its own action (opening a record): without this, that click also selects the row, so a later bulk action can reach rows nobody chose. Off by default. `mode: 'none'` already refuses every selection path regardless of this flag. (optional) |
| groupSelectsChildren | boolean | Selecting a group row selects every row beneath it, and a group shows as partially selected when only some of its children are. Off by default. (optional) |
| groupSelectsFiltered | boolean | Extend that cascade to children the filters have excluded. Off by default, so selecting a group selects what the user can see. (optional) |
| ranges | boolean | Allow rectangular cell-range selection by drag and by Shift+Arrow. On unless `mode: 'none'` turns it off, which an explicit `true` overrides. (optional) |
| fillHandle | boolean | Show the drag handle at a range's corner that fills from it. On unless `mode: 'none'` turns it off; it needs ranges to be usable at all. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| fill | (p: { source: unknown[]; target: { row: Row; column: ResolvedColumn }[]; direction: string }) => unknown[] | p: { source: unknown[]; target: { row: Row; column: ResolvedColumn }[]; direction: string } | => unknown[] | Produce the values a fill writes, given the source cells, the target cells and the direction. Without it the grid copies the anchor and extrapolates numeric and date series. (optional) |
EditConfig
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Turn editing on for the grid. `edit: true` is the shorthand; over a remote source with nothing to persist the write, it warns loudly rather than letting cells change on screen and never save. (optional) |
| mode | EditMode 'cell' | 'row' | `'cell'` (the default) edits one cell at a time - moving to another commits the first. `'row'` keeps one session open across the row, so the whole row commits as one step. (optional) |
| start | EditStartGesture 'single' | 'double' | 'key' | Which mouse gesture opens an editor: `'double'` click, the default, or `'single'`. `'key'` binds no mouse gesture at all, for a grid that is read with the mouse and written with the keyboard. The keyboard path (Enter, F2, typing over a cell) is live under all three. (optional) |
| enterMovesDown | boolean | Whether Enter commits and moves to the cell below, as a spreadsheet does. On by default; Shift+Enter moves up. (optional) |
| undoDepth | number | How many steps the undo timeline keeps. 10 by default. (optional) |
| confirm | EditConfirmMode 'auto' | 'manual' | How an optimistic write settles: `'auto'` (the default) on what `commit` returns, or `'manual'` when the acknowledgement arrives on another channel and you will call `edit.settle` yourself. `'manual'` without a `commit` hook warns and turns tracking off; an unrecognised value warns and is treated as `'auto'`. (optional) |
| pendingTimeout | number | How long, in milliseconds, a write may stay unsettled before the grid warns that it is stuck. 15,000 by default. (optional) |
| pastePreview | boolean | Show a preview of what a bulk paste will change before it commits (), with confirm/cancel. Off by default: a paste commits straight away, exactly as it always has. When on, a paste into more than one cell first opens a dialog listing every cell that changes (old → new) and every cell that would be rejected (permission, data-type, read-only); confirm commits precisely that set through the ordinary edit path, cancel commits nothing. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| commit | (write: PendingWrite) => unknown | write: PendingWrite | => unknown | Send a committed edit to your backend. Supplying it turns on optimistic write-back: the cell changes at once, `cell:pending` fires, and the promise's outcome confirms or reverts it. (optional) |
PaginationConfig
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Show the grid a page at a time rather than as one scrolling list. (optional) |
| pageSize | number | How many rows a page holds. 0 turns paging off and shows everything. (optional) |
| pageSizes | number[] | The sizes the page-size control offers the user. (optional) |
TooltipConfig
Grid-level defaults for the rich cell tooltip, set once for every column rather than repeated on each. Defaults only: it switches nothing on. A tooltip exists because a column declares `cell.tooltip`, and a grid whose columns declare none has no tooltips whatever is set here.
| Property | Type | Description |
|---|---|---|
| delay | number | How long the pointer or the keyboard cursor must rest on a cell before the tooltip is built, in milliseconds. 400 by default. The delay is why a pointer sweeping across the grid mounts nothing: a tooltip that built a chart on every cell it crossed would be unusable, and `0` asks for exactly that. (optional) |
| maxWidth | number | string | How wide the tooltip may grow. A number is pixels; a string is used as written. (optional) |
LookupSpec
| Property | Type | Description |
|---|---|---|
| options | Option[] | (() => Option[] | Promise<Option[]>) | The dictionary: a list of options, or a function returning one, synchronously or as a promise. Loaded once per column, not per cell, and cached against a version so a refresh invalidates every dependent cell in one pass. Options may nest through `children` for the tree editor; every other consumer sees the flattened list. (optional) |
| valueKey | string | Which property of an option holds the stored value. `'id'` by default. A bare string or number is accepted as an option and becomes its own value and label. (optional) |
| labelKey | string | Which property of an option holds the displayed text. `'label'` by default; an option with no label falls back to its value. (optional) |
| groupKey | string | Which property of an option names the option group it belongs to, for an editor that shows headings. `'group'` when unset. (optional) |
| multiple | boolean | The cell holds a list of values rather than one. The display joins their labels with `separator`, and grouping keys on the whole combination. (optional) |
| allowCustom | boolean | Accept a value that is not in the option list, without complaint. Off by default, in which case an unknown value still renders - never blanked - but is reported once per column, since a blank cell hides a data problem. (optional) |
| unknownLabel | string | ((v: unknown) => string) | What to show for a value the option list does not contain: fixed text, or a function of the value. The raw value itself when unset. (optional) |
| sortBy | LookupSortBy 'label' | 'value' | 'optionOrder' | 'count' | How the editor and the set filter order the options: `'label'` (the default, collated for the locale), `'value'`, `'optionOrder'` to keep them exactly as declared, or `'count'` to put the commonest first. (optional) |
| separator | string | What joins the labels of a multi-value cell, in the display and in an export. `', '` by default. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| search | (query: string, signal: AbortSignal) => Promise<Option[]> | query: stringsignal: AbortSignal | => Promise<Option[]> | Ask the server for matching options as the user types, with an abort signal for the request this one supersedes. Results are shown in the editor but never merged into the cached dictionary. Without it the editor filters the loaded options by label, case-insensitively. (optional) |
GridState
| Property | Type | Description |
|---|---|---|
| version | number | The state format this snapshot was written in. A snapshot from a newer build is applied field by field with the unknown ones reported and skipped, rather than refused. |
| columns | ColumnState[] | Each leaf column's width, visibility, pin, sort, grouping and total, in display order. (optional) |
| columnOrder | string[] | The column ids in display order - the same order `columns` is in, held separately so a restore can reorder without reading every entry. (optional) |
| columnGroups | ColumnGroupState[] | The banded-header tree, when the grid has one. (optional) |
| filters | FilterSet | The structured filter tree that was in force, or `null` when nothing was filtered. (optional) |
| where | string[] | The `where` predicates that were in force, as names only. A predicate is host code: it cannot be serialised into a view or restored from one. `apply` reconciles these against what the host has registered and reports every name it cannot honour rather than restoring a view that silently shows more rows than the one that was saved. Absent when none is registered. (optional) |
| quick | string | The quick-filter text. Absent when there was none. (optional) |
| quickMode | QuickFilterMode 'contains' | 'words' | 'fuzzy' | 'regex' | How the quick filter matched. Saved with the text and only when it is not the default, because a view restored as `contains` when it was saved as `words` or `regex` shows a different set of rows than the one it captured. (optional) |
| sort | SortEntry[] | The sort entries that were in force, outermost first. (optional) |
| group | string[] | The ids of the columns the rows were grouped by, outermost first. (optional) |
| pivot | { enabled: boolean; columns: string[] } | Whether the grid was in pivot mode, and the columns it pivoted on. (optional) |
| pivotView | { rowsCollapsed: string[]; columnsCollapsed: string[] } | The pivot presentation's collapse state (,: which row-axis and column-axis nodes are collapsed. Absent when the matrix is fully expanded, and tolerated as "expand all" when applied. (optional) |
| formatting | Record<string, FormattingRule[]> | Every conditional-formatting rule, keyed by scope. Always present when the grid has a formatting model - even empty - so that clearing every rule is an action redo can reproduce. (optional) |
| annotations | AnnotationMark[] | Durable annotation marks: seeded from here on first paint, and written back by `getState` so a host can persist and restore them. In content coordinates, so they track scroll and resize. (optional) |
| redaction | string[] | The ids of the redacted columns. Always present when the grid has a redaction model - even empty - so that "stop redacting" is an action redo can reproduce. (optional) |
| facets | string[] | The ids of the columns whose histogram is open. Always present when the grid has a facet model, for the same reason `redaction` is. (optional) |
| expanded | string[] | The keys of the group and tree rows that were open. (optional) |
| selection | string[] | The keys of the selected rows. (optional) |
| scroll | { top: number; left: number } | Where the body was scrolled to. Absent on a headless grid, which has no scroll position worth saving. (optional) |
| pagination | { page: number; pageSize: number } | The page being shown and its size. Absent when the grid does not page. (optional) |
CommentConfig
| Property | Type | Description |
|---|---|---|
| provider | CommentProvider | Without one the feature is inert and no error is raised. (optional) |
| debounce | number | Milliseconds a viewport change waits before the index is fetched. (optional) |
| indexLimit | number | Cell descriptors held before the oldest are dropped. (optional) |
| mode | CommentDisplayMode 'anchored' | 'docked' | `'anchored'` floats beside the cell; `'docked'` uses a side panel. (optional) |
| markdown | boolean | Restricted markdown in bodies: emphasis, code and links only. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| rowLabel | (row: Row) => string | row: Row | => string | Label for the row, so the panel says what is being commented on. (optional) |
PresenceConfig
| Property | Type | Description |
|---|---|---|
| provider | PresenceProvider | Without one the feature is inert and raises nothing. (optional) |
| me | { id: string; name?: string; colour?: string; avatarUrl?: string; initials?: string } | The local identity, echoed in everything published. (optional) |
| throttleMs | number | Milliseconds between published updates. Throttled, not debounced. (optional) |
| idleMs | number | Silence after which a peer is shown idle. (optional) |
| removeMs | number | Silence after which a peer is dropped. (optional) |
| lockMs | number | Silence after which a peer's edit claim is disregarded. (optional) |
| lock | boolean | Refuse local editing of a cell a peer is editing. Advisory only: the authoritative resolution is the conditional write in `edit.commit`. (optional) |
| palette | string[] | Override the peer colour palette. (optional) |
| roster | boolean | { side?: 'start' | 'end' } | Suppress the roster, or place it. (optional) |
| announce | boolean | Suppress join and leave announcements to assistive technology. (optional) |
FacetConfig
Grid-level histogram settings.
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Off unless asked for: header space is tight and this doubles its height. (optional) |
| collapsed | boolean | Start as a one-line density strip that opens on hover or click. (optional) |
| height | number | Band height in pixels. (optional) |
| rowCeiling | number | Rows above which histograms are suppressed. (optional) |
| debounce | number | Milliseconds a filter change waits before charts recount. (optional) |
| whilePaused | boolean | Whether a paused stream re-enables histograms. Defaults to true. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| provider | (request: { colId: string; column: unknown; filters: unknown; quick: string; bounds: FacetBounds | null; buckets: number; strategy: string; granularity?: string; }) => Promise<{ bounds?: FacetBounds; buckets?: FacetBucket[]; counts: ArrayLike<number>; unfiltered?: ArrayLike<number>; kind?: string }> | request: { colId: string; column: unknown; filters: unknown; quick: string; bounds: FacetBounds | null; buckets: number; strategy: string; granularity?: string; } | => Promise<{ bounds?: FacetBounds; buckets?: FacetBucket[]; counts: ArrayLike<number>; unfiltered?: ArrayLike<number>; kind?: string }> | Bucket counts for a source the client cannot compute over. (optional) |
GroupRowParams
What `groupRenderer` is handed.
| Property | Type | Description |
|---|---|---|
| row | Row | The group row itself. |
| key | string | The group's key, as `rows.expand`/`rows.collapse` take it. |
| column | string | The id of the column this level groups on. (optional) |
| value | unknown | The value this group stands for. |
| level | number | Depth of the group. Zero is the outermost level. |
| expanded | boolean | Whether the group is currently open. |
| leafCount | number | How many records sit beneath it, at any depth. |
| totals | Record<string, unknown> | The group's own reductions, by column id - whatever `total` asked for. (optional) |
| grid | Grid | The grid instance, for anything the parameters above do not carry. |
| element | HTMLElement | The element to fill. Write into it directly, or return content instead. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| leaves | (): Row[] | - | Row[] | The rows beneath this group, computed when you call it. A function rather than an array because a group is unbounded and this runs per paint: a host that only needs the count should read `leafCount` and never call this. |
| toggle | (): void | - | void | Expand the group if it is closed, collapse it if it is open. |
GroupInfo
Which group `groupDefaultExpanded` is being asked about.
| Property | Type | Description |
|---|---|---|
| key | string | The group's key, the same string `Row.key` carries and `rows.expand` takes. |
| column | string | The id of the column this level groups on. (optional) |
| value | unknown | The value this group stands for. (optional) |
| level | number | Depth of the group. Zero is the outermost level. (optional) |
| path | string[] | The group path from the root down to this group. (optional) |
FullWidthParams
What `fullWidth.render` is handed.
| Property | Type | Description |
|---|---|---|
| row | Row | The grid's row wrapper for this row. |
| data | unknown | Your original row object. |
| index | number | Display index of the row. |
| grid | Grid | The grid instance. |
| element | HTMLElement | The element to fill. Write into it directly, or return content instead. |
CellMenuParams
What a cell-menu builder and a host item's `action` are handed.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row the menu opened on. |
| colId | string | null | The column under the pointer, or `null` when the row belongs to no column: a right-click in the empty tail of a row beyond the last column, or on a group row, pivot group row or full-width row. The grid-level menu stands in that case. |
| value | unknown | The cell's value; `undefined` when there is no column. |
| row | Row | The row wrapper. |
| data | unknown | Your original row object. |
| column | ResolvedColumn | undefined | The resolved column; `undefined` when `colId` is `null`. |
| index | number | The row's display index. |
| grid | Grid | The grid instance, so an item's action can do whatever it needs to. |
MenuItem
| Property | Type | Description |
|---|---|---|
| name | string | The item's label. Omit it on a separator. (optional) |
| icon | string | An icon shown in the slot before the label. Three forms, told apart without a second option so existing definitions keep working: a registered sprite name (`'download'`), a single character or emoji (`'↑'`), or author-trusted element markup (`'<i class="fa-light fa-download"></i>'`), which is rendered as an element rather than shown as text. Markup is inserted into the icon slot only - never the label - at the same trust as `action`. (optional) |
| shortcut | string | Keyboard hint shown right-aligned in the item. Display only - the grid does not bind the key for you. (optional) |
| disabled | boolean | Show the item greyed out and unusable, rather than hiding it, so the menu keeps its shape. (optional) |
| separator | boolean | Draw a divider instead of an item. Everything else on the entry is ignored. (optional) |
| children | MenuItem[] | Nested items, turning this entry into a submenu. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| action | () => void | - | => void | What choosing the item does. An item with children opens the submenu instead. (optional) |
ImportSettings
| Property | Type | Description |
|---|---|---|
| file | boolean | Add the cell-menu item and open a file picker for CSV/TSV. Default true. (optional) |
| drop | boolean | Make the grid a drop target for `.csv`/`.tsv` files. Default true. (optional) |
| paste | boolean | Read a pasted spreadsheet block into a preview. Default true. (optional) |
| mode | ImportMode 'append' | 'replace' | How a confirmed import lands: append (default) or replace the dataset. (optional) |
ColumnMenuParams
What a column menu's item builder and its actions are handed.
| Property | Type | Description |
|---|---|---|
| colId | string | The id of the column whose menu opened. |
| column | ResolvedColumn | The resolved column, including any properties you defined on it. |
| grid | Grid | The grid instance, so an item's action can reach the rest of it. |
CellRange
| Property | Type | Description |
|---|---|---|
| startRow | number | The display index the range starts at. It is an index, not a key: a range is a rectangle on screen, so sorting or filtering changes what it covers. |
| endRow | number | The display index the range ends at, inclusive. |
| columns | string[] | The ids of the columns the range spans, in display order. |
FindConfig
The in-grid find bar's settings. `find: true` or an omitted key mounts the bar with these defaults; `find: false` removes the bar and its shortcut while `grid.find` keeps working programmatically.
| Property | Type | Description |
|---|---|---|
| shortcut | boolean | Bind Ctrl+F (Cmd+F on a Mac) while focus is in the grid. The browser's own find is untouched while focus is anywhere else on the page. Default true. (optional) |
| debounce | number | Milliseconds of typing quiet before the bar searches. Default 120. (optional) |
FormattingRule
One rule. A condition and the styling it produces, a colour scale, a data bar or an icon set. A rule held as runtime state must be JSON, so `style` may not be a function there (config-time `cell.style` still accepts one) and a data bar / icon set / scale is the JSON way to say the same visual intent.
| Property | Type | Description |
|---|---|---|
| id | string | The rule's identity, which `remove`, `update` and `move` accept in place of an index. The grid fills one in (`rule-1`, `rule-2`) when you do not. (optional) |
| when | FormattingCondition | The condition the cell must meet for the rule's `style` to apply. A rule with none of `when`, `scale`, `dataBar` or `iconSet` is refused, since it could never match. (optional) |
| style | CellStyle | ((p: CellParams) => CellStyle | null) | The style to apply when `when` holds - the same shape `cell.style` takes. A function is allowed but cannot be saved: only plain objects survive a saved view. (optional) |
| scale | FormattingScale | Colour the cell by where its value sits between two bounds. Two colours give a gradient, three or more give a piecewise scale so a mid colour is actually reached at the middle. Values outside the bounds clamp to the end colours. (optional) |
| dataBar | DataBarSpec | An in-cell proportional bar. (optional) |
| iconSet | IconSetSpec | A per-band glyph beside the value. (optional) |
| stopIfTrue | boolean | Whether a match ends the evaluation. True by default, so an ordered list reads top to bottom like a sentence; set it `false` to let a later rule add to this one - bold from one, colour from another. (optional) |
| enabled | boolean | Turn the rule off without deleting it. On by default. The grid also disables a distribution rule it could not resolve, rather than colouring by a guess. (optional) |
| label | string | A human name for the rule, for your own panel to show. The grid stores it and hands it back but never acts on it. (optional) |
RowStyleParams
| Property | Type | Description |
|---|---|---|
| row | Row | The grid's row wrapper, carrying whether this is a group heading, a footer, a grand total or a detail row. |
| key | string | The key of the row being styled. |
| index | number | Where the row sits on screen, counting the grid's own rows. |
| data | unknown | Your own row object. `null` on a row the grid produced itself, such as a group heading. |
| grid | Grid | The grid instance. |
| context | unknown | Whatever `config.context` holds. |
RailAction
| Property | Type | Description |
|---|---|---|
| name | string | The action's identity, used to place it in the rail's order and, when `icon` is absent, tried as the icon name - so an action named after a built-in glyph needs no separate icon. |
| title | string | (() => string) | The hover and accessible label. A function form is re-read on every repaint, so a toggle can change what it says with its state. |
| icon | IconName | (() => IconName) | A glyph name from the icon registry (see {@link IconName}) - a built-in name, or one registered with `registerIcon`/`registerIcons`, `config.icons` or `grid.icons`. A function form is re-read on every repaint, the same as `title`, so a toggle can swap its glyph with its state. When omitted, the rail tries `name` as the icon name instead (so an action named after a built-in, e.g. `'undo'`, needs no separate `icon`); an unrecognised name - from either `icon` or the `name` fallback - draws a blank glyph, and only an explicitly-given unrecognised `icon` warns once in the console. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| run | (params: RailActionParams): void | params: RailActionParams | void | What pressing the button does. It receives the grid and the selection as it stood when the button was pressed. |
| enabled | (): boolean | - | boolean | Whether the button is usable. Re-read on every repaint, so a button greys out as the selection changes. Always enabled when absent. (optional) |
| active | (): boolean | - | boolean | Marks the action as a toggle and reports whether it is currently on. When present the rail renders `aria-pressed` and a pressed style, re-read on every repaint; a one-shot action omits it and is unchanged. This is the hook the native annotation tools use, and it is available to a host button that is itself a toggle. (optional) |
StatConfig
| Property | Type | Description |
|---|---|---|
| grid | Grid | The grid the tile reads from, follows and resolves its container selector against. A tile with a literal `value` needs none. (optional) |
| container | HTMLElement | string | An element, or a CSS selector resolved against the grid's document. |
| title | string | The label above the value. Omitted, the label element is hidden rather than left empty. (optional) |
| icon | string | An optional leading icon beside the title and value, using the same value contract as a menu item: a registered sprite name, a single character or emoji, or author-trusted element markup (`'<i class="fa-light fa-bolt"> </i>'`, an `<img>`). It lays out to the side without disturbing the change indicator, threshold bands or confidence interval; omit it for the plain tile layout. (optional) |
| value | unknown | StatValueSpec | ((grid: Grid) => unknown) | A literal value, a spec to reduce, or a function of the grid. (optional) |
| footer | string | ((value: unknown, grid: Grid) => string) | Text under the value, or a function of it. (optional) |
| baseline | number | ((grid: Grid) => number) | What the value is compared against, for the change indicator. (optional) |
| goodWhen | StatGoodDirection 'up' | 'down' | 'neither' | Whether a rise is good news. `up` by default. (optional) |
| bands | { good?: number; warn?: number; direction?: 'up' | 'down' } | ((value: unknown, grid: Grid) => 'good' | 'warn' | 'bad' | null) | Thresholds the value itself is judged against, setting `data-tone` on the tile. Separate from `goodWhen`, which judges the *change*: a Cpk of 0.9 is bad news whether it rose or fell to get there. (optional) |
| scope | StatFollowScope 'filtered' | 'all' | 'selected' | Which rows feed the value. `filtered` by default. (optional) |
| live | boolean | `false` stops the tile following the grid; `refresh()` still works. (optional) |
| empty | string | Shown when there is no value. `, ` by default. (optional) |
| decimals | number | Fraction digits for a value whose reduction changed the unit. 2 by default. (optional) |
| class | string | Extra class names for the tile's root. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| interval | (value: unknown, grid: Grid) => { lower: number; upper: number; confidence?: number } | null | value: unknowngrid: Grid | => { lower: number; upper: number; confidence?: number } | null | An interval to show under the value: how much to trust it. Return whichever of the grid's intervals belongs to this tile. (optional) |
| format | (value: unknown, grid: Grid) => string | value: unknowngrid: Grid | => string | Override the formatting the column's type would apply. (optional) |
ResolvedColumn
A column after presets, type defaults and grid defaults are folded in.
| Property | Type | Description |
|---|---|---|
| id | string | This column's identity, which every API that names a column uses. The definition's `id`, or its `field` when no `id` was given, or a generated `col…` name when it has neither. |
| field | string | null | The path into a row's data this column reads and writes, dotted for a nested value. `null` on a column whose value is computed rather than read. |
| title | string | The heading text. Defaults to the field (or the id) made human: `unitPrice` becomes `Unit Price`, and a dotted path uses only its last segment. |
| type | TypeName 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object' | 'lookup' | 'image' | The data type actually in force. Not always the name that was asked for: an unknown `type` falls back to `text` for every behaviour, and reads back here as `text`. |
| dataType | DataType | The resolved data type itself - the object supplying the parser, comparator, editor, Excel format and defaults this column behaves by. |
| nullable | boolean | Whether an empty value is allowed in this column. True unless the definition said `nullable: false`. |
| align | Align 'start' | 'center' | 'end' | 'left' | 'right' | 'centre' | The horizontal alignment in force, after the column's own `align` or `cell.align`, the grid's column defaults and the data type's default have been weighed in that order. `'start'` when nothing sets one. |
| verticalAlign | VAlign 'top' | 'middle' | 'bottom' | The resolved vertical alignment, or `undefined` when neither the column nor the grid set one - in which case the cell keeps the grid's historical vertical placement (centred, or top for `autoHeight`). (optional) |
| value | Required<Pick<ColumnValueSpec, 'pure'>> & ColumnValueSpec | The value hooks with defaults filled in. `pure` is always present and is `true` unless the column said otherwise - the grid forces it `false` on a shadow or running-total column, whose answer does not depend on the row alone. |
| cell | ColumnCellSpec | The resolved cell spec - renderer or template, decoration, variant, classes, tooltip. `cell.align` is always filled in, from the column's own `cell.align` or from `align`. |
| edit | ColumnEditSpec | The resolved editing spec. `enabled` is `false` unless the column turned editing on, so a column is read-only until it says otherwise. |
| sort | ColumnSortSpec | The resolved sort spec. Sorting is enabled, with no direction, order 0 and nulls last, unless the column or a preset says otherwise. |
| filter | ColumnFilterSpec | The resolved filter spec. Filtering is enabled by default, with the filter kind the data type asks for - `'text'` when it names none, and disabled entirely when the type declares `filter: 'none'`. |
| quickFilter | boolean | Whether this column takes part in the quick filter. |
| group | { enabled: boolean; index: number; explode: boolean; granularity?: ColumnGroupGranularity; weekStart?: number } | The resolved grouping spec: whether this column is a grouping key, its place in the group order (`-1` when it is not one), whether a multi-value cell explodes into one group per value, and the date granularity to group by. |
| pivot | { enabled: boolean; index: number } | The resolved pivot spec: whether this column is a pivot key and its place in the pivot order (`-1` when it is not one). |
| total | TotalName | TotalFn | null | The footer aggregate: a built-in name, or the function a custom or type-specific total resolved to, or `null` for none. A total the data type declares meaningless for the column (the mean of a bearing, the sum of decibels) is refused at configuration time rather than shown as a wrong number. |
| groupTotal | TotalName | TotalFn | null | The group-subtotal override, or null when group subtotals follow `total`. |
| grandTotal | TotalName | TotalFn | null | The grand-total override, or null when the grand total follows `total`. |
| layout | ColumnLayoutSpec | The resolved layout: width, min and max, flex, pin, hidden, and the resize, move and lock flags. Width defaults to 150 px and min to 40 px; `fit: 'content'` drops the default width so the measurement can supply one. |
| header | ColumnHeaderSpec | The resolved header spec. `header.align` is always filled in and follows the cell alignment, so a right-aligned number column gets a right-aligned heading. |
| contextMenu | boolean | MenuItem[] | ((p: CellMenuParams, defaults: MenuItem[]) => MenuItem[] | void) | null | This column's own cell-menu declaration, or null when it makes none and the grid-level menu stands alone. Carried onto the resolved column so a column preset or `columnDefaults` can supply one. |
| export | ColumnExportSpec | The resolved export spec: by default a lookup column exports its label, and the column appears in both the CSV and the Excel export. |
| lookup | LookupSpec | null | The compiled lookup for a dictionary column - value-to-label mapping, its comparator and group keys, and its loading state for an asynchronous dictionary - or `null` when the column has none. |
| allowGroup | boolean | Whether the user may group by this column from the menu or the tool panel. True unless the definition said `allowGroup: false`. |
| allowPivot | boolean | Whether the user may pivot on this column from the menu or the tool panel. True unless the definition said `allowPivot: false`. |
| allowTotal | boolean | Whether the user may put a footer total on this column from the menu. True unless the definition said `allowTotal: false`. |
| def | Column | The column definition exactly as it was given, before presets, `columnDefaults`, data-type defaults and the grid's own defaults were folded in. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| formatValue | (value: unknown, row?: Row, data?: unknown): string | value: unknownrow?: Rowdata?: unknown | string | Compiled display-text producer. |
| getValue | (data: unknown, row?: Row): unknown | data: unknownrow?: Row | unknown | Resolve the value for a row, through the computed-value graph. |
NumberFormat
| Property | Type | Description |
|---|---|---|
| type | 'number' | Marks this as the number format, so the grid compiles it with the number formatter. (optional) |
| style | NumberFormatStyle 'decimal' | 'currency' | 'percent' | `'decimal'` (the default), `'currency'` or `'percent'`. `'percent'` multiplies by 100 and appends the locale's percent sign, so store 0.12 for 12%. (optional) |
| currency | string | The ISO currency code for `style: 'currency'` - `'GBP'`, `'EUR'`. Defaults to `'USD'`. (optional) |
| currencyDisplay | CurrencyDisplay 'symbol' | 'code' | 'name' | 'narrowSymbol' | How the currency is shown: `'symbol'` (£1.00), `'narrowSymbol'`, `'code'` (GBP 1.00) or `'name'` (1.00 British pounds). The locale's own default when unset. (optional) |
| decimals | number | Shorthand for a fixed number of decimal places: it sets the minimum and the maximum to the same figure, so 2 always shows two. (optional) |
| minDecimals | number | The fewest decimal places to show, padding with zeros. Ignored when `decimals` is given. (optional) |
| maxDecimals | number | The most decimal places to show, rounding beyond it. Raised to `minDecimals` if it would fall below. (optional) |
| thousandsSeparator | boolean | string | `false` turns grouping off entirely; a string replaces the locale's group separator with your own. Grouping is on by default. (optional) |
| decimalSeparator | string | Replaces the locale's decimal separator with your own. The locale's is used when unset. (optional) |
| notation | NumberFormatNotation 'standard' | 'compact' | 'scientific' | `'standard'` (the default), `'compact'` - 1,234,567 as `1.2M` - or `'scientific'`. (optional) |
| compactDisplay | CompactDisplay 'short' | 'long' | Which compact form `notation: 'compact'` uses - `'short'` (the default) gives `1.2M`, `'long'` gives `1.2 million`. Ignored under any other notation. (optional) |
| negative | NumberFormatNegative 'minus' | 'parentheses' | 'suffix' | How a negative number reads: `'minus'`, the default, gives `-1,234`; `'parentheses'` gives `(1,234)`, the accounting form; `'suffix'` gives `1,234-`. (optional) |
| negativeClass | string | A class name put on the cell when the value is negative, so the stylesheet can colour it. Nothing is added when unset. (optional) |
| signed | boolean | Show a leading `+` on a positive value (`+5`, `+£5.00`, `+12%`). A negative value keeps whatever `negative` says regardless of this flag, and zero shows no sign either way. Off by default. (optional) |
| prefix | string | Free text placed before the number - and after the currency symbol when there is one, so `£~1,234` rather than `~£1,234`. (optional) |
| suffix | string | Free text placed after the number, after any percent sign or currency code. (optional) |
| zeroDisplay | string | The text shown instead of a formatted zero - `' - '`, `'free'`. Zero is formatted normally when unset. Tested after `scale` is applied. (optional) |
| nullDisplay | string | The text shown for null, undefined, an empty string, and anything that is not a number. Empty by default. (optional) |
| locale | string | The locale for number, date and text formatting. The page's by default. (optional) |
| messages | Record<string, string | Record<string, string>> | A partial message catalogue laid over the built-in British English one. Every valid key is listed in `MESSAGE_KEYS`; a key that is not is ignored with a warning. Import a bundled locale (`FR_FR`, `AR`, …) or supply your own object. Merged rather than replacing, so an incomplete translation leaves the remainder in English rather than showing raw keys. (optional) |
| scale | number | A multiplier applied before formatting, for showing stored units in another magnitude - `0.001` to read a column of pounds as thousands. 1 by default. It changes only the display: sort, filter and totals still use the stored number. (optional) |
DateFormat
| Property | Type | Description |
|---|---|---|
| type | 'date' | Marks this as the date format, so the grid compiles it with the date formatter. |
| pattern | string | A token pattern - `'dd/MM/yyyy HH:mm'`, `'dd MMM yyyy'`. Numeric fields are assembled by hand so the output is stable across browsers; only month and weekday names come from the locale. An unsupported letter is rendered as text and warns; quote it to silence that. (optional) |
| dateStyle | DateStyle 'short' | 'medium' | 'long' | 'full' | A locale-chosen date form - `'short'`, `'medium'`, `'long'`, `'full'` - used when no `pattern` is given. Medium is the default when neither is set. (optional) |
| timeStyle | TimeStyle 'short' | 'medium' | 'long' | A locale-chosen time form - `'short'`, `'medium'`, `'long'` - shown alongside `dateStyle`. No time is shown when unset. (optional) |
| timeZone | string | The IANA zone the instant is rendered in - `'Europe/London'`, `'UTC'`. The browser's own zone when unset. It changes only the display; the stored instant is untouched. (optional) |
| relative | boolean | { threshold?: number } | Render as `yesterday`, `in 3 hours` while the date is within the threshold, falling back to the absolute form beyond it. The threshold is 7 days unless `{ threshold: n }` names another number of days. (optional) |
| nullDisplay | string | The text shown for null, undefined and anything that will not read as a date. Empty by default. (optional) |
| locale | string | The BCP-47 locale this column formats in, overriding the grid's. The page's locale by default. (optional) |
BooleanFormat
| Property | Type | Description |
|---|---|---|
| type | 'boolean' | Marks this as the boolean format, so the grid compiles it with the boolean formatter. |
| display | BooleanDisplay 'checkbox' | 'switch' | 'text' | 'icon' | Which text stands in for each state: `'text'` (the default) uses the labels below; `'checkbox'` and `'switch'` both render the glyph pair ☑ / ☐ as text; `'icon'` uses `trueIcon` and `falseIcon`. An interactive tick box is the `checkbox` cell renderer, which is a separate setting. (optional) |
| trueLabel | string | The text for true. `'Yes'` by default. (optional) |
| falseLabel | string | The text for false. `'No'` by default. (optional) |
| nullLabel | string | The text for a value that is neither - null, undefined or empty, which the grid keeps distinct from false. Empty by default. (optional) |
| trueIcon | string | The text used for true under `display: 'icon'` - a character or emoji, placed in the cell as written. Falls back to `trueLabel`. (optional) |
| falseIcon | string | The text used for false under `display: 'icon'`. Falls back to `falseLabel`. (optional) |
TextFormat
| Property | Type | Description |
|---|---|---|
| type | 'text' | Marks this as the text format, so the grid compiles it with the text formatter. |
| transform | TextTransform 'none' | 'upper' | 'lower' | 'title' | Change the case for display: `'upper'`, `'lower'`, `'title'`, or `'none'` (the default). Casing is locale-aware, which matters for Turkish `i`. (optional) |
| truncate | number | { chars: number; ellipsis?: string } | Cut the text to a number of characters and append an ellipsis. Give a number for the character count, or `{ chars, ellipsis }` to choose the trailing mark; `…` by default. (optional) |
| nullDisplay | string | The text shown for null and undefined. Empty by default. (optional) |
| emptyDisplay | string | The text shown for an empty string, which the grid keeps distinct from null. Empty by default. (optional) |
ColumnValueSpec
| Property | Type | Description |
|---|---|---|
| deps | string[] | '*' | Which columns `compute` reads, so an edit to one of them invalidates just this column. `'*'` means the whole row. Leaving it out is treated as `'*'` and warns, because the grid then has to recompute on every change. (optional) |
| pure | boolean | Whether `compute` is a function of its dependencies alone. True by default, which lets the value be materialised and read back cheaply. Set it `false` for a value that depends on anything else - history, sort position - or the first answer is frozen for good. (optional) |
| compare | Comparator | Order two of this column's values. Wins over the lookup's comparator and over the data type's, for sorting and for the diff view. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| compute | (deps: DepValues, ctx: ValueContext) => unknown | deps: DepValuesctx: ValueContext | => unknown | Produce this column's value from its declared dependencies rather than from a field. Called with the dependency values and a context carrying the row, the grid and your own context. (optional) |
| format | (p: FormatParams) => string | p: FormatParams | => string | Turn this column's value into the text a cell shows. First in the display chain: it wins over the column's `format` and over the data type's own formatter. (optional) |
| apply | (p: ApplyParams) => boolean | p: ApplyParams | => boolean | Write an edited value back into the row's data yourself, instead of the grid writing through `field`. Return `false` to decline the write; a throw is reported once and the edit discarded. (optional) |
| parse | (p: ParseParams) => unknown | p: ParseParams | => unknown | Turn an editor's output into the stored value. Always honoured - the data type's own text parser is used only when the editor emitted a string and you supplied no `parse`. (optional) |
| key | (p: KeyParams) => string | p: KeyParams | => string | The identity a value groups and set-filters by, when the stringified value is the wrong answer (an object, a pair of coordinates). Wins over a lookup's own group key. (optional) |
| quickFilterText | (p: ValueParams) => string | p: ValueParams | => string | The text the quick filter searches for this cell. By default it matches the display text, which is what the reader can see; supply this to search something else. (optional) |
ColumnCellSpec
| Property | Type | Description |
|---|---|---|
| decoration | DecorationName | DecorationSpec | Draw the formatted text as something richer - a pill, a proportional bar, an icon - by name or as a full spec. Ignored, with a warning, on a column that also sets `template` or `render`, since those own the cell's content. (optional) |
| variant | VariantSpec | Which semantic token a cell's decoration takes its colour from: `neutral`, `info`, `success`, `warning`, `danger`, `accent`, or `none` to suppress it. Give a fixed token, a value-to-token map, ordered `when` clauses, or a function of the row - it always resolves to a token, never to a colour. (optional) |
| template | string | A declarative cell template compiled once per column: `{{ value }}`, `{{ text }}`, `{{ index }}`, `{{ data.field }}` and anything in `cell.props`. Only a safe subset of tags survives; a binding that resolves to nothing renders empty and warns. (optional) |
| render | string | RenderFn | RendererCtor | A custom cell renderer: a function, a component class with a `render` method, or the name of a registered renderer. It owns the cell's content, so a decoration set alongside it is ignored. (optional) |
| props | Record<string, unknown> | Values passed on to the renderer as `params.props`, and reachable from a template as `{{ name }}`. (optional) |
| class | string | string[] | ((p: CellParams) => string | string[]) | Class names put on every cell of the column, or a function asked per cell. Composed with the decoration and `classWhen` classes rather than replacing them. (optional) |
| classWhen | Record<string, string | ((p: CellParams) => boolean)> | Conditional classes: each class name is mapped to a condition - a comparison written as text (`'>= 100'`, `"= 'Open'"`) or a predicate on the cell - and every class whose condition holds is added. (optional) |
| style | CellStyle | ((p: CellParams) => CellStyle) | Inline style properties for every cell of the column, as an object or a function of the cell. Merged with any runtime formatting rule in one write, the rule winning on the properties it names. (optional) |
| tooltip | string | ((p: CellParams) => string) | ColumnTooltipSpec | A tooltip for this column's cells. A string or a function is the plain-text case and becomes the browser's own `title`. An object is a {@link ColumnTooltipSpec}: a tooltip the grid draws, which can carry structure, markup or live content and which a keyboard user can reach. (optional) |
| align | Align 'start' | 'center' | 'end' | 'left' | 'right' | 'centre' | Horizontal alignment of this column's cell content. Accepted at the top level of the column too; falls back to the data type's default and finally to `'start'`. (optional) |
| verticalAlign | VAlign 'top' | 'middle' | 'bottom' | Vertical alignment of this column's cell content, overriding the grid-level `verticalAlign` for this column alone. Accepted at the top level of the column too, as `align` is. (optional) |
| wrap | boolean | Let a cell's text wrap onto further lines instead of being clipped to one. Off by default. Pair it with `autoHeight` on the grid for rows that grow to fit. (optional) |
| flash | boolean | string | { colour?: string; color?: string; /** Milliseconds. `0` leaves the highlight until it is cleared. */ duration?: number; enabled?: boolean; } | Flash this column's cells when their value changes - the per-column half of the grid-level `highlightOnChange`, for a grid where one column is the one worth watching. `true` takes the default colour and duration; an object takes the same `{ colour, duration }` the grid-level key accepts. Grid-level `highlightOnChange` covers every column and wins where both are set. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| css | (p: CellParams) => CellStyle | p: CellParams | => CellStyle | An escape hatch that computes style properties for every cell. It leaves the class fast path and warns once per column; unsuitable for large datasets, where a decoration and a variant do the same job from the stylesheet. (optional) |
| spanColumns | (p: SpanParams) => number | p: SpanParams | => number | Make this column's cell span several columns, as a function of the cell. Return 1 or less for no span; the span is floored and clamped to the columns remaining to its right, and a spanned cell is drawn in its own layer so row recycling cannot clip it. (optional) |
| spanRows | (p: SpanParams) => number | p: SpanParams | => number | Make this column's cell span several rows, as a function of the cell. Return 1 or less for no span. The renderer looks back 50 rows above the viewport for a span's origin: a span taller than that is reported once and drawn from the first row in range, and one whose origin sits further above the viewport than that is not found, so its cells draw individually. (optional) |
ColumnEditSpec
| Property | Type | Description |
|---|---|---|
| enabled | boolean | ((p: CellParams) => boolean) | Whether this column's cells can be edited. `false` by default; a function is asked per cell, and a throw is reported once and the cell treated as read-only. (optional) |
| editor | string | EditorCtor | Which editor opens on this column: the name of a registered editor, or a class of your own. The data type's default editor is used when this is absent. Naming one without enabling editing leaves the column read-only, and says so. (optional) |
| props | Record<string, unknown> | Values handed to the editor as `params.props` - the option list for a select, the step for a number. (optional) |
| popup | boolean | Open the editor in a popup over the cell rather than inside it. Defaults to whatever the editor class declares. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| validate | (p: ValidateParams) => true | string | p: ValidateParams | => true | string | Check an edited value before it is written. Return `true` to accept, or a message to reject and show. A throw is reported once and the edit rejected. (optional) |
ColumnValidation
Declarative edit-validation rules for a column. Rules are checked in a fixed order - `required` first, then the value-shape rules, then the functions - and the first failure wins. A blank but optional value passes everything after `required`: an empty cell is empty, not "below the minimum". A failure vetoes the commit through `beforeEdit` and marks the cell; the cancellation carries `reason: 'validation:<code>'`.
| Property | Type | Description |
|---|---|---|
| required | boolean | string | The value may not be blank. A string is used as the message. (optional) |
| min | number | Minimum, for a number or a date. (optional) |
| max | number | Maximum, for a number or a date. (optional) |
| minLength | number | Minimum text length. (optional) |
| maxLength | number | Maximum text length. (optional) |
| pattern | string | RegExp | A pattern the whole value must match. A string is a RegExp source. (optional) |
| oneOf | unknown[] | The value must be one of these. (optional) |
| message | string | A default message for any rule without its own. (optional) |
| messages | Record<string, string> | Per-rule messages, keyed by rule name (`required`, `min`, `pattern`, …). (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| crossField | (value: unknown, row: unknown, ctx: { key: string; colId: string; changes: unknown[] }) => true | string | void | value: unknownrow: unknownctx: { key: string; colId: string; changes: unknown[] } | => true | string | void | A cross-field rule: return `true` to pass, or a message string to fail. The row is passed so a rule can compare against its siblings. (optional) |
| validate | (value: unknown, row: unknown, ctx: { key: string; colId: string; changes: unknown[] }) => true | string | void | value: unknownrow: unknownctx: { key: string; colId: string; changes: unknown[] } | => true | string | void | A free-form check, the same contract as `crossField`. (optional) |
ColumnSortSpec
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether this column can be sorted. True by default, and forced off on a running-total column, whose value is defined by the display order. (optional) |
| direction | SortDirection | null | The sort direction this column starts in - `'asc'`, `'desc'`, or `null` for unsorted, which is the default. (optional) |
| order | number | This column's place in a multi-column sort, lowest first. 0 by default. (optional) |
| nullsFirst | boolean | Put empty values before the rest instead of after them. Off by default, so nulls sort last whichever direction is in force. (optional) |
ColumnFilterSpec
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether this column offers a filter. True by default. (optional) |
| type | FilterName | FilterCtor | Which filter the column uses: a built-in name, or a filter class of your own. Defaults to the kind the data type asks for, and to `'text'` when it names none. (optional) |
| props | Record<string, unknown> | Values handed to the filter as `params.props` - the option list for a set filter, the step for a number range. (optional) |
RegressionSpec
The specification of a multi-predictor model.
| Property | Type | Description |
|---|---|---|
| predictors | string[] | The predictor column ids. |
| response | string | The response column id. |
| method | RegressionMethod 'ols' | 'wls' | 'robust' | 'quantile' | `ols` (default), `wls` or `robust`. `quantile` is reserved (coming next). (optional) |
| weights | string | A weights column id, required for `wls`. (optional) |
| confidence | number | The confidence level for the band; 0.95 by default. (optional) |
ColumnLayoutSpec
| Property | Type | Description |
|---|---|---|
| width | number | string | A pixel width, or a percentage of the grid's inner width as a string, `'25%'`. A percentage is a share of the *whole* grid. `flex` divides only the space left over after fixed columns, so the two are not interchangeable: `flex: 25` on four columns is a quarter of the remainder, which is a quarter of the grid only when nothing else is fixed. (optional) |
| fit | 'content' | `'content'` sizes the column to what it is actually showing, the way `columns.autoSize()` does, and keeps doing it: on the first paint, and again whenever the rows change, the columns are shown, hidden, reordered or pinned, or the grid is resized. It is the declarative form of the imperative call, so a host no longer has to re-issue `autoSize()` after every data change. Sized to the *visible* content, not to the widest value in the dataset: the measurement reads the rows the renderer has mounted, because measuring a million rows is not a plan. It measures the heading too, so a column whose title is longer than its values widens to show the title. **Anything the caller states outranks it.** A declared `width` wins, and so does a width the user drags to - a resize is recorded as a `width`, so from that moment the column is that wide and the fit no longer touches it. `min` and `max` clamp the fitted width as they clamp any other. `flex` is resolved before this and wins, the two being contradictory instructions: `flex` fits the column to the *grid*, this fits it to the *content*. Not re-measured on scroll, deliberately: different rows mount as the grid scrolls, and re-fitting against them would make the columns jitter under the reader. (optional) |
| min | number | The narrowest this column may be, in pixels; 40 by default. It clamps a drag, a flex share and a content fit alike. (optional) |
| max | number | The widest this column may be, in pixels. No maximum by default. (optional) |
| flex | number | A share of the space left over once the fixed-width columns are laid out; 0 (no share) by default. Resizing the column by hand sets it back to 0, so the drag is not immediately undone. (optional) |
| pin | Edge | null | Freeze the column against the start or the end edge, so it stays put while the rest scroll sideways. `null`, the default, leaves it in the scrolling body. Start and end rather than left and right, so a right-to-left grid needs no change. (optional) |
| hidden | boolean | Keep the column out of the grid without removing it. Off by default; `columns.show()` and `columns.hide()` move it. (optional) |
| resizable | boolean | Whether the user may drag this column's width. True by default; a resize of a column that says `false` is ignored and warns once. (optional) |
| movable | boolean | Whether the user may drag this column to another position. True by default; a move of a column that says `false` is ignored and warns once. (optional) |
| lockVisible | boolean | Refuse to hide this column: a hide is ignored and warns once, and the AI layer is told the column cannot be hidden. Off by default. (optional) |
| lockPosition | boolean | Hold this column where it is: it cannot be dragged, it cannot be moved by the keyboard, and it cannot be pulled into a header band. Off by default. This locks a column; it does not place one. `'start'` and `'end'` were declared and never implemented - every reader tested truthiness, so either one pinned the column wherever it already sat - and the union was narrowed to the boolean the grid actually honours. Put a column at an edge by ordering `columns`, or pin it with `layout.pinned`. (optional) |
ColumnHeaderSpec
| Property | Type | Description |
|---|---|---|
| template | string | Not read by the header renderer; use `render` to draw a custom heading. (optional) |
| render | string | RendererCtor | A custom heading renderer: a function, or a component (a class with a `render` method). A string names a registered renderer. Either form draws the same two ways and they are interchangeable - it may append to the passed label element itself and return nothing, or return an `Element` (attached for you) or a `string` (used as the heading text). (optional) |
| props | Record<string, unknown> | Props passed to `render` as `params.props`. (optional) |
| class | string | string[] | A class, or classes, added to the heading cell. A string may hold several space-separated tokens (`'a b'`), each applied individually. (optional) |
| tooltip | string | Declared, but not drawn: the header renderer never reads it, so a heading's only hover text is the drag hint. Put the text in the heading itself with `header.render`, or on the cells with `cell.tooltip`. (optional) |
| align | Align 'start' | 'center' | 'end' | 'left' | 'right' | 'centre' | Horizontal alignment of the heading text. Follows the cell alignment when it is not set, so a right-aligned number column gets a right-aligned heading. (optional) |
ColumnExportSpec
| Property | Type | Description |
|---|---|---|
| lookup | ColumnExportLookup 'label' | 'value' | 'columns' | How a lookup column leaves in an export: `'label'` (the default) writes what the reader sees, `'value'` writes the stored code, `'columns'` writes both in a pair of columns. (optional) |
| csv | boolean | Include this column in a CSV export. True by default. (optional) |
| excel | boolean | Include this column in an Excel export. True by default. (optional) |
ColumnFacetConfig
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether this column draws a histogram under its heading. It layers over the grid's `facets` settings rather than replacing them, and `facet: true` on the column is the shorthand for turning it on without restating anything else. (optional) |
| buckets | number | How many buckets to cut the values into. 20 by default. On a date column it is a target rather than an exact count, since the granularity has to land on real calendar units. (optional) |
| strategy | FacetBucketStrategy 'equal' | 'quantile' | 'log' | How numeric buckets are placed: `'equal'` width (the default), `'quantile'` so each holds roughly the same number of rows, or `'log'`. A log scale over values reaching zero or below falls back to equal width rather than drawing nothing. (optional) |
| granularity | FacetDateGranularity 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | The calendar unit a date column buckets by - hour through year. Chosen automatically from the span and the wanted bucket count when unset. (optional) |
| order | FacetBarOrder 'count' | 'alpha' | How a categorical column's bars are ordered: `'count'`, commonest first (the default), or `'alpha'`. Fixed against the unfiltered column so bars do not reorder themselves under the pointer. (optional) |
| cardinalityLimit | number | How many distinct values a categorical column may have before `aboveLimit` applies. 50 by default - beyond that a bar chart stops telling anyone anything. (optional) |
| aboveLimit | FacetOverflowMode 'suppress' | 'topN' | What to do with a categorical column past the cardinality limit: `'suppress'` (the default) draws no chart, `'topN'` draws the commonest values and gathers the rest into one `Other` bar. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| bucketFn | (handle: unknown, indices: Uint32Array | null, count: number) => FacetBounds | handle: unknownindices: Uint32Array | nullcount: number | => FacetBounds | Replace the built-in bucketing entirely. (optional) |
| format | (bucket: FacetBucket, count: number, unfiltered: number) => string | bucket: FacetBucketcount: numberunfiltered: number | => string | Label a bucket for its tooltip and accessible name. (optional) |
SortEntry
| Property | Type | Description |
|---|---|---|
| col | string | The id of the column to sort by. An entry naming a column the grid does not have is dropped with a warning rather than stored. |
| dir | SortDirection 'asc' | 'desc' | `'asc'` or `'desc'`. |
| nullsFirst | boolean | Put empty values before the rest instead of after them. Off by default, so nulls sort last in either direction. (optional) |
FilterGroup
| Property | Type | Description |
|---|---|---|
| op | FilterOp 'and' | 'or' | 'not' | How the children combine: `'and'`, `'or'`, or `'not'` to negate them. |
| conditions | FilterSet[] | The children - conditions, or further groups, so a filter set is a tree of any depth. |
Condition
| Property | Type | Description |
|---|---|---|
| col | string | The id of the column this condition reads. |
| type | TypeName 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object' | 'lookup' | 'image' | The column's type, which decides how both sides are normalised before comparison - a `date` or `dateString` condition brings the value and the operand to the same calendar day first, so a `Date`, an epoch number and an ISO string all line up. (optional) |
| op | Operator | The comparison to make: `eq`, `contains`, `between`, `in`, `blank` and the rest, each with an exact negation (`ne`, `notContains`, `notBetween`, `notIn`, `notBlank`). |
| value | unknown | The operand: a single value, a pair for `between`, or a list for `in`. Left out by the operators that need none. (optional) |
| bounds | IntervalBounds '[]' | '[)' | '(]' | '()' | Which ends of a `between` range are inclusive, written in interval notation. `'[]'` - both ends - by default; `'[)'` is the half-open form a date range usually wants. (optional) |
| caseSensitive | boolean | Compare text exactly as written. Off by default, so text matching, set membership and regular expressions all ignore case. (optional) |
| meta | Record<string, unknown> | Extra information carried alongside the condition for whoever executes it - the relative date token a range was built from, so a saved `last 7 days` filter is re-derived rather than frozen to the day it was written. (optional) |
RemoteRequest
| Property | Type | Description |
|---|---|---|
| protocol | 1 | The wire protocol version, always 1. A server implementation targets the documented version rather than reverse-engineering what this release happens to send. |
| range | { start: number; end: number } | The half-open row range being asked for at this level, `start` inclusive and `end` exclusive. |
| groupPath | string[] | The group ancestry of the level being fetched, outermost first, as display strings. It is a stable identity for expansion state - use `groupValues` to narrow a query. |
| groupValues | unknown[] | The same ancestry as `groupPath`, but as the values the server returned rather than their display strings. Always present, empty at the root, so a source can tell "no ancestors" from "a host that does not send this". `groupPath` is stringified because it is a stable *identity* for expansion state, and that is what it must stay: a numeric key `3` is `'3'` there and an absent key is `''`, indistinguishable from a group whose key really is the empty string. Useless for narrowing a query, then - which is what a grouping engine needs it for - so the typed values travel beside it. |
| groupBy | ColumnRef[] | The columns the grid is grouping by, outermost first. Empty when the rows are flat. |
| totals | ColumnRef[] | The columns that want a subtotal on each group row. The grid does not recompute what a remote source returns. |
| totalFns | Record<string, string> | The named statistic each totalled column reduces with - `{ amount: 'sum' }`. `totals` has always said *which* columns want a subtotal and never *what*, because the client reads the reduction off the column model and a server had no way to. Only string reductions appear: a column totalling with a host function has no name to send, and naming one that merely resembles it would put a plausible wrong number on every group row. `groupTotal` wins over `total`, the same precedence the client applies for the group scope. |
| pivotBy | ColumnRef[] | The columns the grid is pivoting on. |
| pivotMode | boolean | Whether the grid is in pivot mode - true exactly when `pivotBy` is not empty. |
| filters | FilterSet | The filter tree, wired for the wire: relative date tokens resolved to absolute ranges, so the server is never asked to interpret `last 7 days`. |
| quick | string | The quick-filter text, present only when there is some. It is not column-scoped, which is why it sits beside the tree rather than in it. (optional) |
| sort | SortEntry[] | The sort entries in force, outermost first. |
| context | unknown | Whatever the grid's `context` holds - a tenant id, an auth token, a locale - passed through untouched for the fetch to use. |
| signal | AbortSignal | Aborts when the grid no longer needs this block: the user scrolled past it, changed the query, or destroyed the grid. Pass it to `fetch` and the superseded request is cancelled rather than paid for. |
| where | WhereRuntime | The `where` predicates in force, as a runtime the source can evaluate but not mutate. Present **only when at least one predicate is registered**, so a grid that does not use `where` sends the request it always sent, field for field. A host `fetch` may ignore it, and every existing one does: it is a host function, so there is nothing to serialise and no engine can evaluate it - `passes` is dropped by `JSON.stringify` the way `signal` already is. It is carried for the one reader that can act on it, `createPushdownSource`, which runs it as the residual over the matching set when that set is under `whereRowLimit`. The `{ condition }` twin remains the route that narrows the fetch itself, at any size. (optional) |
RemoteResult
| Property | Type | Description |
|---|---|---|
| rows | unknown[] | The rows for the requested range, at the requested group level. Group rows carry their own subtotals; the grid does not recompute them. |
| count | number | How many rows exist at this level in total, so the scrollbar is the right size. Omit it while the total is unknown and the grid keeps discovering. (optional) |
| pendingTotal | Promise<number | null> | The exact count, still being worked out. A source whose count has to read data delivers the rows as soon as the page settles and resolves this when the count finishes; the grid keeps discovering until it does, then adopts the number and fires `source:total`. Ignored when `count` is present. Resolving with anything other than the exact count - an estimate, a page length - puts a wrong number in the place a right one goes. (optional) |
| pivotFields | string[] | The value-column names this level's pivot produced, so the grid can build the headings it has never seen before. (optional) |
Chunk
| Property | Type | Description |
|---|---|---|
| rows | unknown[] | The rows in this chunk. They are appended to whatever has already arrived, merged into the current sort rather than re-sorting the whole set. |
| progress | { loaded: number; estimated?: number } | How far the stream has come: `loaded` is how many rows have arrived, `estimated` the total when the producer knows it. It drives the progress readout instead of skeletons. (optional) |
| done | boolean | The last chunk. It closes the stream, and a final count under `promoteToMemoryBelow` promotes the source to an in-memory one so later sorts and filters are local. (optional) |
UnionSourceOptions
| Property | Type | Description |
|---|---|---|
| grid | Grid | The grid this source reads. |
| label | string | Identifies this source: it is what `__source` carries on every row this source contributes, and what namespaces that row's `__key` so two sources sharing the same identifiers do not collide. Defaults to the source's position in the `from` array (`'0'`, `'1'`, …), as a string. (optional) |
| follow | FollowScope 'filtered' | 'all' | 'selected' | 'grouped' | Which of this source's rows to read. `filtered` by default, exactly as a lone `from` follows its grid today - set independently per source, so filtering one narrows only its own contribution. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| map | (row: unknown) => unknown | row: unknown | => unknown | Reshape this source's rows into the common shape before they join the rest - typically a rename or a projection, for a field this source calls something else. Not a type coercion: if a field means something different on two sources, `map` is where you make them agree, because the union itself does not guess. (optional) |
DerivedJoin
| Property | Type | Description |
|---|---|---|
| with | Grid | The grid holding the other side. |
| on | string | { left?: string; right?: string } | The shared key: one field name when both sides use it, or one each. |
| type | JoinType 'inner' | 'left' | `inner` keeps only rows that matched; `left` keeps them all. (optional) |
| select | string[] | Which of the partner's fields to bring across. All of them by default. (optional) |
| prefix | string | Rename the brought-across fields, when both sides have one worth keeping. (optional) |
| follow | RowScope 'all' | 'filtered' | Which of the partner's rows to read. `all` by default. (optional) |
DerivedSelect
One reduced column of a derived grid.
| Property | Type | Description |
|---|---|---|
| of | string | The column to reduce, as a field name or a dotted path. Omit for `count`. (optional) |
| fn | TotalName 'sum' | 'min' | 'max' | 'avg' | 'count' | 'first' | 'last' | 'countValues' | (string & {}) | A key of `TOTAL_FNS`: `sum`, `avg`, `median`, `p95`, `distinct` and the rest. (optional) |
DerivedCorrelation
| Property | Type | Description |
|---|---|---|
| fn | 'correlation' | Selects the pairwise-correlation producer. It replaces the pipeline: one row per column pair rather than one per group. |
| columns | string[] | The columns to correlate pairwise. At least two, or the source is refused. |
| orient | CorrelationOrient 'pairs' | 'matrix' | `pairs` (default) for one row per pair; `matrix` for the square. (optional) |
DerivedSeries
A `grid.statistics.series` summary, as one row per metric: `{ metric, value, n }`. One row per *metric*, not per point: `series` returns a `SeriesStats` summary object - `n`, `first`, `last`, `change`, `changePercent`, `volatility`, `annualisedVolatility`, `growth`, `maxDrawdown`, `maxDrawdownFrom`, `maxDrawdownTo`, `autocorrelation`, `upDays`, `downDays` - and not a value per row. The shape is deliberately the one `profile`'s `orient: 'metrics'` already emits rather than a third convention for the same idea.
| Property | Type | Description |
|---|---|---|
| fn | 'series' | Selects the series-summary producer - volatility, growth, drawdown, autocorrelation - one row per metric. It replaces the pipeline. |
| of | string | The column to summarise. |
| by | string | The column that orders it. Required and never guessed. |
| periodsPerYear | number | Annualise volatility and growth against this many periods per year. (optional) |
DerivedDatasetComparison
How this grid differs from another, ranked by effect size, as rows: `{ column, measure, magnitude, distance, direction, nA, nB, reliable, unmatched }`, largest difference first. The two-grid shape: one grid is the data, a second *is* the analysis of it. Both sides are read over their filtered rows, and the peer is watched - an edit or a filter on it re-derives the comparison, because a comparison whose other side has moved is wrong rather than merely late. A column present on only one side cannot be compared. It is still reported, as a row with a null `magnitude` and `unmatched` set to `'A'` or `'B'`, so a reader sees that it was skipped and why rather than finding it absent.
| Property | Type | Description |
|---|---|---|
| fn | 'datasetVsDataset' | Selects the dataset-comparison producer: one row per compared column, against a second grid. It replaces the pipeline. |
| with | Grid | The second grid to compare this one against. |
| columns | string[] | Restrict the comparison to these columns. All shared columns by default. (optional) |
CellParams
| Property | Type | Description |
|---|---|---|
| text | string | The display text: the value after the column's format and any lookup label - exactly what the cell shows. |
| index | number | The row's display index, counting the grid's own rows: group headings, footers and totals included. |
| props | Record<string, unknown> | Whatever the column's `cell.props` holds, passed through so one renderer can be configured per column. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| t | (key: string, vars?: Record<string, unknown>) => string | key: stringvars?: Record<string, unknown> | => string | Format a message from the grid's catalogue, for a renderer that wants its own accessible names and labels localised rather than hard-coded (, WCAG 4.1.2). The built-in renderers use this; a custom renderer may too. Optional: absent when a renderer is exercised without a grid to ask. (optional) |
EditorParams
| Property | Type | Description |
|---|---|---|
| key | string | The name of the key that opened the editor, when a keystroke did. Absent when it was opened by a click or by the API. (optional) |
| charPress | string | The printable character that opened the editor, so typing straight into a cell seeds the first character instead of losing it. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| stop | (cancel?: boolean): void | cancel?: boolean | void | End the session from inside the editor: with no argument it commits, and `stop(true)` discards. This is how an editor cancels; the grid does not poll for it. |
FilterParams
| Property | Type | Description |
|---|---|---|
| column | Column | The resolved column this filter belongs to. |
| colId | string | The column's id, which the conditions the filter produces are keyed by. |
| grid | Grid | The grid instance, for a filter that needs to read the rows or another column. |
| context | unknown | Whatever `config.context` holds - the tenant, the user, whatever the filter's own logic needs. |
| props | Record<string, unknown> | Whatever the column's `filter.props` holds: the option list for a set filter, the step for a number range. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| changed | (): void | - | void | Tell the grid the filter's state moved. It reads `get()` back and applies the result; nothing happens until this is called. |
PendingWrite
| Property | Type | Description |
|---|---|---|
| id | string | The write's identity, which `cell:pending` carries and `edit.settle` takes. |
| key | string | The key of the row being written to. |
| colId | string | The column being written to. |
| value | unknown | The new value to persist. |
| before | unknown | The value the cell held before the edit - what a rejected write is rolled back to, and what a conditional write can check the server against. |
| row | Row | The row the cell belongs to, so the commit hook can send whatever else it needs to identify the record. (optional) |
Option
| Property | Type | Description |
|---|---|---|
| id | unknown | The stored value - what the cell holds, sorts by and exports when `export.lookup` asks for the value. A bare string or number as the whole option becomes both its id and its label. |
| label | string | What the reader sees. Falls back to the id stringified when the option carries none. |
| disabled | boolean | Show the option in the editor but refuse to let it be chosen - for a value that exists historically and should not be used again. (optional) |
| variant | VariantName 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'accent' | 'none' | (string & {}) | A semantic token for this option, so a pill or a dot takes its colour from the value rather than from a rule. (optional) |
| icon | IconName | A glyph name from the icon registry (see {@link IconName}), shown before the label. (optional) |
| group | string | The heading this option sits under in an editor that groups its list. Read from `groupKey` when the column names one. (optional) |
ColumnState
| Property | Type | Description |
|---|---|---|
| id | string | Which column this entry restores. An entry naming a column the grid no longer has is reported and skipped, not thrown. |
| width | number | The column's width in pixels at the time the state was taken. (optional) |
| flex | number | The column's flex weight, present only when it has one - a fixed-width column leaves it out. (optional) |
| hidden | boolean | Whether the column was hidden. (optional) |
| pin | Edge | null | Which edge the column was frozen against, or `null` when it was in the scrolling body. (optional) |
| sort | SortDirection | null | The column's sort direction, or `null` when it was not sorted. (optional) |
| sortIndex | number | null | The column's place in a multi-column sort, or `null` when it was not sorted. (optional) |
| groupIndex | number | null | The column's place in the grouping order, or `null` when it was not a grouping key. (optional) |
| pivotIndex | number | null | The column's place in the pivot order, or `null` when it was not a pivot key. (optional) |
| total | TotalName | null | The named footer aggregate the column carried, or `null`. A total given as a function has no name to save and is not recorded. (optional) |
| groupTotal | TotalName | null | The group-subtotal override, when one differs from `total`. (optional) |
| grandTotal | TotalName | null | The grand-total override, when one differs from `total`. (optional) |
| decoration | DecorationName | DecorationSpec | null | The column's runtime decoration, present only when the column carries one, so a `columns.decorate()` survives a saved view and participates in undo/redo. Absent means "not recorded"; an explicit `null` on an undo patch clears a decoration back to plain text. (optional) |
| variant | VariantSpec | null | The variant set alongside the decoration, when one is present. (optional) |
ColumnGroupState
A persisted banded-header node (,: a band with a `columns` list whose members are leaf ids or nested bands. This is what round-trips a drag-created group through a saved view.
| Property | Type | Description |
|---|---|---|
| id | string | The band's identity, matching the `id` on the live band, so a restore reattaches to the right one. |
| title | string | The band's heading at the time the state was taken. |
| collapsible | boolean | Whether the band carried an open/close control. |
| openByDefault | boolean | Whether the band opened by default. |
| columns | Array<string | ColumnGroupState> | The band's children in order - a leaf column's id, or a nested band's own state. |
AnnotationMark
| Property | Type | Description |
|---|---|---|
| type | AnnotationKind 'freehand' | 'arrow' | 'rect' | 'highlight' | 'text' | What the mark is: a freehand trail, an arrow, a rectangle, a highlighter stroke, or a text label. `pen` is accepted on input as another name for `freehand`, and `list()` reports `freehand`. |
| points | { x: number; y: number }[] | Content coordinates. A `text` mark carries a single anchor point; `arrow` and `rect` carry their two corners, and `freehand` a trail. |
| colour | string | The mark's colour, any CSS colour. Omitted, it takes the layer's current colour. (optional) |
| text | string | The label of a `text` mark. Required for `text`, ignored for other types. (optional) |
| fontSize | number | A `text` mark's font size in content pixels (before presentation scale). Defaults to 14. (optional) |
| background | string | An optional backing colour drawn behind a `text` mark's label. (optional) |
| region | AnnotationRegion 'start' | 'centre' | 'end' | Which columns the mark belongs to: a pinned region holds still while the grid scrolls sideways, the centre moves with it. Set from where a stroke began; omitted (the centre) for every mark that is not over a pinned column, so a mark saved before this existed reads unchanged. (optional) |
CommentProvider
Storage for comments. Every method returns a promise; a rejection surfaces in the panel without disturbing grid state.
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| loadIndex | (rowIds: string[], fields: string[]): Promise<CommentIndexEntry[]> | rowIds: string[]fields: string[] | Promise<CommentIndexEntry[]> | Return the counts for these rows and columns - counts and timestamps only, never bodies, because this is consulted on every repaint. Called as the viewport moves, debounced. |
| loadThread | (cellKey: string): Promise<Comment[]> | cellKey: string | Promise<Comment[]> | Return the bodies for one cell, in display order. Called when a thread is opened and dropped when it closes, so a grid never holds every thread. |
| addComment | (cellKey: string, body: string, parentId: string | null, context?: { value?: unknown }): Promise<Comment> | cellKey: stringbody: stringparentId: string | nullcontext?: { value?: unknown } | Promise<Comment> | Store a new comment on a cell, optionally as a reply, with the cell's value at the time as context. Return the stored comment; the grid shows it optimistically and rolls it back if this rejects. |
| editComment | (commentId: string, body: string): Promise<Comment> | commentId: stringbody: string | Promise<Comment> | Store a new body for an existing comment and return it. |
| deleteComment | (commentId: string): Promise<void> | commentId: string | Promise<void> | Remove a comment. The grid shows it gone at once and puts it back if this rejects. |
| resolveThread | (cellKey: string): Promise<void> | cellKey: string | Promise<void> | Mark a cell's thread resolved. |
| unresolveThread | (cellKey: string): Promise<void> | cellKey: string | Promise<void> | Reopen a cell's thread. |
PresenceProvider
Transport for presence. The grid never opens a connection: it subscribes to what the provider delivers and hands it what changed locally.
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| subscribe | (onMessage: (message: Peer | Peer[]) => void): (() => void) | void | onMessage: (message: Peer | Peer[]) => void | (() => void) | void | Returns an unsubscribe function, if it has one. |
| publish | (state: Record<string, unknown>): void | state: Record<string, unknown> | void | Send this client's cursor, ranges and open editor to the other clients. The grid throttles the calls and drops them while publishing is paused, so a transport does no rate limiting of its own. |
FacetBounds
| Property | Type | Description |
|---|---|---|
| kind | FacetBoundsKind 'numeric' | 'date' | 'category' | 'boolean' | 'none' | Which family of buckets these are: `'numeric'`, `'date'`, `'category'`, `'boolean'`, or `'none'` when the column has no histogram at all. |
| buckets | FacetBucket[] | Where the bars are - a range per bar for an ordered column, a value per bar for a categorical one. A column with absent values carries a final `Empty` bucket, and a capped categorical one carries an `Other` bucket. |
| suppressed | FacetSuppressedReason 'type' | 'cardinality' | 'rows' | 'streaming' | 'no-provider' | 'disabled' | Set when no histogram was drawn, naming why. (optional) |
| cardinality | number | Distinct values, on categorical columns. (optional) |
| granularity | FacetDateGranularity 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | The time unit chosen, on date columns. (optional) |
| strategy | FacetBucketStrategy 'equal' | 'quantile' | 'log' | The numeric strategy actually applied, which may differ from the request. (optional) |
| min | number | The smallest value the buckets span, on an ordered column. (optional) |
| max | number | The largest value the buckets span. The last bucket's top edge is inclusive so the maximum has somewhere to land; every other bucket is half-open. (optional) |
FacetBucket
One bucket of a column's distribution.
| Property | Type | Description |
|---|---|---|
| from | number | Lower edge, for ordered columns. Half-open `[from, to)` except the last. (optional) |
| to | number | Upper edge, for ordered columns. Inclusive on the last bucket only. (optional) |
| value | unknown | The value, for categorical and boolean columns. (optional) |
| null | boolean | True on the terminal bucket holding nulls, NaN and empty values. (optional) |
| remainder | boolean | True on the aggregated tail bucket under `aboveLimit: 'topN'`. (optional) |
| label | string | A ready-made label, where one is more useful than the raw value. (optional) |
FormattingCondition
| Property | Type | Description |
|---|---|---|
| op | Operator | DistributionOp | A filter operator compared against `value`, or a distribution operator whose threshold comes from the column itself: `{op: 'topPercent', value: 10}`, `{op: 'outlier'}`. Distribution thresholds are pinned when the rules compile; `grid.formatting.restat()` moves them. |
| value | unknown | What the operator compares against - the number, text, date or list the rule tests for. Left out by operators that need no operand, such as `blank`. (optional) |
| value2 | unknown | The upper operand of a two-sided operator such as `between`. (optional) |
FormattingScale
| Property | Type | Description |
|---|---|---|
| from | ScaleFrom 'minmax' | 'quantile' | 'stddev' | Where the bounds come from when `min` and `max` are not given. `'minmax'` spans the data, `'quantile'` spans `low` to `high` (5th to 95th percentile by default), `'stddev'` spans `deviations` either side of the mean. (optional) |
| min | number | The value that takes the first colour. Required unless `from` derives it. (optional) |
| max | number | The value that takes the last colour. Required unless `from` derives it. (optional) |
| mid | number | The value the middle colour is reached at. Needs an odd number of `colours` (a middle stop to pin) and a value inside `min`..`max`; each half of the scale is then spaced evenly within itself, so only the pivot moves. Without it the middle stop sits at the midpoint of the range. (optional) |
| low | number | The lower percentile for `from: 'quantile'`, as a number from 0 to 100. 5 by default. (optional) |
| high | number | The upper percentile for `from: 'quantile'`, as a number from 0 to 100. 95 by default. (optional) |
| deviations | number | How many standard deviations either side of the mean `from: 'stddev'` spans. 2 by default. (optional) |
| colours | string[] | The colour stops, low to high; at least two are needed or the scale falls back to a pale-to-blue pair. Three or more give a piecewise scale rather than an averaged blend. (optional) |
DataBarSpec
An in-cell proportional bar. Drawn as a CSS gradient on the cell background - no extra element, and it composes with the cell's text. The bar's length is the value's position between `min` and `max`. Give both to pin the scale (0 to 100 for a percentage); otherwise `from` derives them from the column - `'minmax'` (the default) spans the data, `'quantile'` the 5th-95th percentile, `'stddev'` a number of deviations either side of the mean. When the range straddles zero, bars grow from a shared axis: positive right, negative left, each in its own colour.
| Property | Type | Description |
|---|---|---|
| min | number | The value at which the bar is empty. Derived from the data when omitted. (optional) |
| max | number | The value at which the bar is full. Derived from the data when omitted. (optional) |
| from | ScaleFrom 'minmax' | 'quantile' | 'stddev' | Where the bounds come from when `min` and `max` are not given: `'minmax'` (the default) spans the data, `'quantile'` spans `low` to `high`, `'stddev'` spans `deviations` either side of the mean. Derived once when the rules compile and then held, so a bar does not move without its value changing; `formatting.restat()` re-derives it. (optional) |
| low | number | The lower percentile for `from: 'quantile'`, 0 to 100. 5 by default. (optional) |
| high | number | The upper percentile for `from: 'quantile'`, 0 to 100. 95 by default. (optional) |
| deviations | number | How many standard deviations either side of the mean `from: 'stddev'` spans. 2 by default. (optional) |
| colour | string | The fill for non-negative values. (optional) |
| color | string | American spelling of `colour`. (optional) |
| negativeColour | string | The fill for negative values. (optional) |
| negativeColor | string | American spelling of `negativeColour`. (optional) |
| direction | Extract<Direction, 'ltr' | 'rtl'> | Which way the bar grows. `'ltr'` (the default) or `'rtl'`. (optional) |
IconSetSpec
| Property | Type | Description |
|---|---|---|
| set | IconSetKind | string | A built-in glyph set: `'arrows'`, `'trafficLights'` or `'ratings'`. `'arrows'` when nothing else is given, and ignored when you supply your own `icons`. (optional) |
| icons | string[] | Your own glyphs, low value first: SVG documents, data URIs or `url(...)`. (optional) |
| count | number | How many bands, where the set's size is not fixed (e.g. `'ratings'`). (optional) |
| thresholds | number[] | Band edges, ascending; one fewer than the number of icons. (optional) |
| reverse | boolean | Reverse the glyph order, so the highest band takes the first icon. (optional) |
| size | number | Glyph height in pixels. Default 16. (optional) |
RailActionParams
What a host rail action's `run` is handed.
| Property | Type | Description |
|---|---|---|
| grid | Grid | The grid instance the rail belongs to. |
| keys | string[] | The keys of the selected rows at the moment the action ran. |
| cells | { key: string; colId: string }[] | The cells inside the selected ranges at the moment the action ran, as row key and column id pairs. |
StatValueSpec
How a statistic block finds the number it reports.
| Property | Type | Description |
|---|---|---|
| of | string | The column to reduce, as a field name or a dotted path. Omit for `count`. (optional) |
| fn | TotalName 'sum' | 'min' | 'max' | 'avg' | 'count' | 'first' | 'last' | 'countValues' | (string & {}) | A key of `TOTAL_FNS`: `sum`, `avg`, `median`, `p95`, `gini` and the rest. (optional) |
| show | string | Report this column from the row holding the extreme, rather than the extreme itself: `{ of: 'sales', fn: 'max', show: 'rep' }` is the *name* of the best rep. Needs `min` or `max`, no single row holds an average. (optional) |
ValueParams
| Property | Type | Description |
|---|---|---|
| value | unknown | The cell's resolved value, after any `compute` and before formatting. |
| data | unknown | Your own row object, exactly as you supplied it. |
| row | Row | The grid's wrapper round that object, carrying the key, the display index and whether the row is a group, a footer or a total. |
| column | Column | The resolved column, including anything you declared on it. |
| colId | string | The column's id, for the common case where that is all the callback needs. |
| grid | Grid | The grid instance, so a callback can read the rest of the grid - another cell, the selection, the filters. |
| context | unknown | Whatever `config.context` holds: the application state a callback needs and the grid knows nothing about. |
DecorationSpec
| Property | Type | Description |
|---|---|---|
| type | DecorationName 'plain' | 'fill' | 'pill' | 'dot' | 'bar' | 'heat' | 'icon' | Which shape the cell draws as: `plain`, `fill`, `pill`, `dot`, `bar`, `heat` or `icon`. Colour, padding and radius follow from the shape, the size token and the theme, so every pill in an application matches without being configured. |
| size | DecorationSize 'sm' | 'md' | 'lg' | The size token - `'sm'`, `'md'` or `'lg'`. Unset follows the grid's density. (optional) |
| shape | DecorationShape 'pill' | 'rounded' | 'square' | The container's outline shape: `'pill'`, `'rounded'` or `'square'`. Unset follows the decoration's own default. (optional) |
| outline | boolean | Pill only: draw it as a coloured border round transparent fill rather than a solid tint. Off by default. (optional) |
| edge | boolean | Fill only: draw a leading colour bar at the cell's edge instead of tinting the whole cell. Off by default. (optional) |
| position | Edge 'start' | 'end' | Dot and icon only: which side of the value the mark sits on. `'start'` by default. (optional) |
| name | IconName | Record<string, IconName> | `icon` decoration only: either a single glyph name (see {@link IconName}) used for every value, or a value -> glyph name map for exact-value icons. Omit both `name` and `bands` to use `iconSet`/its default instead. (optional) |
| iconSet | IconSetName 'trafficLights' | 'arrows' | 'trafficArrows' | 'ratings' | (string & {}) | icon only: a built-in threshold icon set, expanded to `bands`. (optional) |
| bands | IconBand[] | icon only: value bands mapped to glyphs, first match by descending `min`. (optional) |
| min | number | Bar and heat only: the value at which the bar is empty or the heat is coldest. 0 by default. A function of the cell is accepted, for a scale that follows the data. (optional) |
| max | number | Bar and heat only: the value at which the bar is full or the heat is hottest. 100 by default - or 1 on a percent-formatted column, whose values are stored as fractions, since otherwise every bar would be a sliver disagreeing with the `87%` printed beside it. (optional) |
| origin | number | Bar only: the value the bar grows out from, so a bar for a signed column can run left for negatives and right for positives. Unset grows from `min`. (optional) |
| showValue | boolean | Bar only: whether the number is printed as well as drawn. `true` by default; `'inside'` puts it within the bar and `false` leaves the bar alone. (optional) |
| track | boolean | Bar only: paint the unfilled remainder as a track, so the full scale is visible. On by default. (optional) |
| ramp | string | Heat only: the colour token used below the midpoint, which is what makes a diverging scale - one colour for the low arm, the decoration's own for the high. (optional) |
| midpoint | number | Heat only: the value the scale diverges about. Unset makes a single-ended ramp from `min` to `max`; set, the intensity is the distance from this point, normalised by the longer arm. (optional) |
ColumnTooltipSpec
A rich, keyboard-accessible tooltip for a column's cells - the object form of `cell.tooltip`, drawn by the grid rather than handed to the browser as a native `title`. Shown after a delay (`tooltip.delay`, 400ms by default) on hover *and* on keyboard focus; the cell points at it with `aria-describedby`; it can be hovered without closing and Escape dismisses it (WCAG 2.2 AA, 1.4.13). It closes on scroll, because rows are pooled and a bubble left open would be anchored to a node that is now showing a different row.
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| render | (params: TooltipParams) => HTMLElement | TooltipSpec | { html: string } | string | null | undefined | params: TooltipParams | => HTMLElement | TooltipSpec | { html: string } | string | null | undefined | Produce the content. Four shapes, and the difference between the last two is a security property rather than a style choice: - an **element** - your own DOM, attached as it is; - a **{@link TooltipSpec}** - `{ title, rows, note }`, rendered as text; - **`{ html }`** - the only wrapper that inserts markup, scrubbed of script the same way `allowUnsafeTemplates` output is; - a **string** - *always* text, never markup. The last rule is what makes `render: (p) => p.value` safe: a value comes from row data, and data must not be able to promote itself to HTML. (optional) |
| mount | (el: HTMLElement, params: TooltipParams) => void | el: HTMLElementparams: TooltipParams | => void | Put live content in the tooltip - a sparkline, a KPI tile - by calling into a module bundle your application loaded. The grid core never imports a module, so anything live is mounted here by you. (optional) |
| unmount | (el: HTMLElement) => void | el: HTMLElement | => void | Tear down whatever `mount` built. Called every time the tooltip closes, so nothing keeps running behind a hidden box. (optional) |
WhereRuntime
The `where` predicates in force, as a source sees them. A snapshot rather than the model, so a source can evaluate the predicates but cannot register or remove one through it.
| Property | Type | Description |
|---|---|---|
| active | boolean | Whether any predicate is registered at all. |
| names | string[] | The registered names, in registration order - for diagnostics. |
| version | number | Bumped on every registration or removal, so a cache key can track it. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| passes | (row: unknown, key?: string): boolean | row: unknownkey?: string | boolean | Does this row survive every registered predicate? |
CommentIndexEntry
What `loadIndex` returns per commented cell.
| Property | Type | Description |
|---|---|---|
| cellKey | string | The cell this entry describes, as the grid's own composite key. Give this, or `rowId` and `field`, and the grid builds it. (optional) |
| rowId | string | The row key, when the entry is identified by row and column rather than by `cellKey`. (optional) |
| field | string | The column id, beside `rowId`. (optional) |
Comment
One comment in a thread, as the provider returns it.
| Property | Type | Description |
|---|---|---|
| id | string | The comment's identity, as the provider assigns it. A comment awaiting the provider carries a temporary id and is replaced when the write returns. |
| body | string | The text of the comment. |
| author | { name?: string; avatarUrl?: string; initials?: string } | Rendered as supplied. The grid does not know who the user is. (optional) |
| at | number | When it was written, as epoch milliseconds. (optional) |
| edited | boolean | Whether the body has been changed since it was posted, so a reader can be told. (optional) |
| resolved | boolean | Whether the thread this comment belongs to has been resolved. (optional) |
| parentId | string | null | The comment this one replies to, or null at the top of the thread. (optional) |
| value | unknown | The cell's value when this was written, so a later reader is told it moved. (optional) |
| can | { edit?: boolean; delete?: boolean; resolve?: boolean } | What the current user may do. Absent means the grid shows every affordance and relies on the provider to refuse. Hiding a button is a convenience, never a security control. (optional) |
Peer
One peer, as the grid holds them.
| Property | Type | Description |
|---|---|---|
| id | string | The peer's stable identity, as the provider supplies it. Everything else keys off it - the colour, the cursor, the lock. |
| name | string | The display name. Falls back to the id when the provider sends none. |
| colour | string | Assigned deterministically from the id when the provider supplies none. |
| avatarUrl | string | null | A picture for the peer, or `null` when the provider sends none. (optional) |
| initials | string | null | Initials to draw when there is no avatar, or `null`. (optional) |
| cursor | { rowId: string; colId: string } | null | Row key and column, never an index. |
| ranges | Array<{ rowIds: string[]; columns: string[] }> | The cell ranges the peer has selected, as row keys and column ids - never indices, since peers sort and filter independently. |
| editing | { rowId: string; colId: string } | null | The cell the peer has an editor open on, or `null`. This is what `editorOf` and the advisory lock read. |
| at | number | Local receipt time, not the sender's clock. |
| sentAt | number | null | The sender's own timestamp, for inspection only. Nothing decides on it. (optional) |
| idle | boolean | Whether nothing has been heard from this peer for the idle window (30 seconds by default). Derived when the peers are read, so a peer goes idle without any timer having to fire. (optional) |
| silentMs | number | How long it has been, in milliseconds, since anything was heard from this peer. (optional) |
| hidden | boolean | True when the peer's cursor is on a row this view is not showing. (optional) |
IconBand
One band of a threshold icon set. A value clears a band when it is at least `min`; the highest band it clears wins. Omit `min` on the last band to make it the catch-all. `label` is what assistive technology announces for the glyph, so a screen-reader user hears the band's meaning, not only the value.
| Property | Type | Description |
|---|---|---|
| min | number | The lowest value in this band. Bands are tested from the highest `min` down, so the first one a value reaches wins; leave it out on the fallback band. (optional) |
| icon | IconName | A glyph name from the icon registry (see {@link IconName}). |
| label | string | Accessible text for the glyph, so the band means something to a screen reader and in a tooltip. (optional) |
| variant | VariantName 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'accent' | 'none' | (string & {}) | The semantic token this band's glyph is coloured with. (optional) |
TooltipParams
What a tooltip's `render` and `mount` are given: the same identification `cell:clicked` carries, plus the cell element itself and the grid. Resolved from the DOM at the moment the tooltip opens rather than when the pointer arrived, so a pooled row re-used in between names the row it is showing now.
| Property | Type | Description |
|---|---|---|
| row | Row | The row under the pointer or the keyboard cursor. |
| key | string | That row's key. |
| index | number | Its display index. |
| colId | string | The column the cell belongs to. |
| column | Column | The resolved column. |
| value | unknown | The cell's value. |
| text | string | The cell's formatted text. |
| cell | HTMLElement | The cell element the tooltip is anchored to. |
| grid | Grid | The grid. |
TooltipSpec
Structured tooltip content the grid renders for you: a heading, a list of label/value lines, and a closing note. Every field is written as **text**, never as markup, so a spec built out of row values needs no escaping and cannot become HTML by accident. Return `{ html }` from `render` when markup is genuinely wanted.
| Property | Type | Description |
|---|---|---|
| title | unknown | A heading for the tooltip. (optional) |
| rows | TooltipRow[] | Label/value lines, in order. (optional) |
| note | unknown | A closing note under the lines, drawn quieter than them. (optional) |
TooltipRow
One label/value line in a {@link TooltipSpec}. Both halves are written as text by the grid, whatever they contain.
| Property | Type | Description |
|---|---|---|
| label | unknown | The line's label, drawn on the leading edge. (optional) |
| value | unknown | The line's value, drawn on the trailing edge. (optional) |
Events
Every event the grid declares, and the payload interfaces handlers receive.
Every event
Generated from the EventName union and the EventPayloads map. A payload of unknown is one the declarations do not name yet; cancellable is read off the payload type, and - means the payload is undeclared, not that the event cannot be cancelled.
| Event | Raised by | When | Payload | Cancellable |
|---|---|---|---|---|
| ready | Lifecycle | The grid has finished building and every API on it is ready to call; fires once, on the frame after `createGrid` returns. | no payload | no |
| destroy | Lifecycle | `grid.destroy()` was called and is about to release everything, so a handler can still read the grid one last time. | no payload | no |
| render:first | Lifecycle | The renderer has written its first frame into the host element. | no payload | no |
| render:done | Lifecycle | A render pass has finished writing cells: the row window it drew, what caused the pass, and how long each phase took. | RenderDoneEvent | no |
| config:changed | Lifecycle | A configuration key was written at run time through `grid.set(key, value)` or `grid.setAll(values)`, after the grid rebuilt. | ConfigChangedEvent | no |
| licence:changed | Lifecycle | A licence key was installed through `grid.licence.set(key)`, and again when its asynchronous verification settles. | LicenceChangedEvent | no |
| model:changed | Data | The display model was rebuilt - rows reloaded, the tree re-flattened, a page fetched, a query re-run - with `reason` naming which. | ModelChangedEvent | no |
| rows:changed | Data | Rows were added, updated, removed or moved. `identified: true` means the payload names exactly which rows moved. | RowsChangedEvent | no |
| rows:queued | Data | A change arrived while the feed was being batched and was put on the queue instead of applied. | RowsQueuedEvent | no |
| rows:deferred | Data | A flush ran out of its frame budget and carried the rest of the change into the next one. | RowsDeferredEvent | no |
| rows:paused | Data | `grid.changes.pause()` held the feed: changes keep arriving and stop being applied. | RowsFlowEvent | no |
| rows:resumed | Data | `grid.changes.resume()` released the feed and applied what had been held. | RowsFlowEvent | no |
| row:received | Data | A row dragged from another grid was accepted into this one, on the receiving grid. | RowReceivedEvent | no |
| row:sent | Data | A row was dragged out of this grid into another one and removed from here (a move, not a copy). | RowTransferEvent | no |
| row:copied | Data | A row was dragged out of this grid into another one and kept here as well (a copy). | RowTransferEvent | no |
| row:moved | Data | A row was reordered within this grid, from one display index to another. | RowMovedEvent | no |
| source:error | Data | A source could not fetch what was asked of it: a page, a group's children, a tree branch, or the stream itself. | SourceErrorEvent | no |
| source:total | Data | A source that delivered its rows before counting them has finished counting; the exact total is in the payload. | SourceTotalEvent | no |
| stream:chunk | Data | A streaming source applied a chunk of arriving rows. | StreamChunkEvent | no |
| stream:end | Data | A streaming source reached the end of its feed; `promoted` says whether it handed over to an in-memory source. | StreamEndEvent | no |
| stream:evicted | Data | A rolling-window stream dropped rows off the back of its window to stay inside its limit. | StreamEvictedEvent | no |
| rowDrag:started | The row-drag gesture as it happens | A row drag passed the drag threshold and began, on the grid the row was picked up in. | RowDragEvent | no |
| rowDrag:moved | The row-drag gesture as it happens | The pointer moved during a row drag, coalesced to one event per animation frame. | RowDragEvent | no |
| rowDrag:left | The row-drag gesture as it happens | The pointer left a grid it had been dragging over; `over` names the grid just left. | RowDragEvent | no |
| rowDrag:ended | The row-drag gesture as it happens | The row drag ended - released anywhere, inside a grid or outside every one; `dropped` says whether it is being acted on. | RowDragEvent | no |
| cell:changed | Cells and editing | A cell's value was written: by an edit commit, by a revert, or by an undo/redo step. | CellChangedEvent | no |
| cell:pending | Cells and editing | An optimistic cell edit was sent to the transport and is awaiting the server's answer. | CellPendingEvent | no |
| cell:confirmed | Cells and editing | The server accepted a pending cell edit; `value` is what it confirmed, which may not be what was sent. | CellConfirmedEvent | no |
| cell:reverted | Cells and editing | A pending cell edit was refused and the previous value put back. | CellRevertedEvent | no |
| cell:conflict | Cells and editing | The server accepted a pending cell edit but returned a row that disagrees with what the grid holds. | CellConflictEvent | no |
| cell:clicked | Cells and editing | A cell was clicked (primary button, single click). | CellPointerEvent | no |
| cell:dblclicked | Cells and editing | A cell was double-clicked. | CellPointerEvent | no |
| cell:contextmenu | Cells and editing | A context menu was requested on a cell, by the pointer or by the keyboard's menu key. | CellContextMenuEvent | no |
| cell:mouseover | The pointer entering and leaving a cell | The pointer entered a cell; crossing between two children of one cell is not a re-entry. | CellPointerEvent | no |
| cell:mouseout | The pointer entering and leaving a cell | The pointer left a cell; crossing between two children of one cell is not a departure. | CellPointerEvent | no |
| cell:mousedown | Cells and editing | A pointer button was pressed on a cell, before any click is resolved. | CellPointerEvent | no |
| cell:mouseup | Cells and editing | A pointer button was released on a cell. | CellPointerEvent | no |
| cell:edit:start | Cells and editing | A cell editor opened, by double-click, by Enter, or by typing into the cell. | EditStartEvent | no |
| cell:edit:end | Cells and editing | A cell editor closed: committed, cancelled, or refused by validation - `valid` and `cancelled` say which. | EditEndEvent | no |
| row:edit:start | Cells and editing | A whole-row editor opened, the row-edit counterpart of `cell:edit:start`. | EditStartEvent | no |
| row:edit:end | Cells and editing | A whole-row editor closed, the row-edit counterpart of `cell:edit:end`. | EditEndEvent | no |
| row:clicked | Cells and editing | A row was clicked, alongside the `cell:clicked` for the cell under the pointer. | RowPointerEvent | no |
| row:dblclicked | Cells and editing | A row was double-clicked, alongside the `cell:dblclicked` for the cell under the pointer. | RowPointerEvent | no |
| row:pending | Cells and editing | An optimistic row append or delete was sent to the transport and is awaiting the server's answer. | RowPendingEvent | no |
| row:confirmed | Cells and editing | The server accepted a pending row append or delete; an append is rekeyed from its temporary key first. | RowConfirmedEvent | no |
| row:reverted | Cells and editing | A pending row append or delete was refused: the optimistic append is discarded, the tombstoned row restored. | RowRevertedEvent | no |
| row:conflict | Cells and editing | The server accepted a pending row append or delete but returned a row that disagrees with what the grid holds. | RowConflictEvent | no |
| form:opened | Cells and editing | The row form opened over a row. | FormOpenedEvent | no |
| form:closed | Cells and editing | The row form was closed without saving. | FormClosedEvent | no |
| form:saved | Cells and editing | The row form's values were saved back to the row. | FormSavedEvent | no |
| form:error | Cells and editing | The row form could not load or save a row; `timedOut` distinguishes a slow backend from a refusal. | FormErrorEvent | no |
| sort:changed | Query | The sort order changed, through `grid.sort.set()` or a header click. | SortChangedEvent | no |
| filter:changed | Query | The filters changed: a structured condition, the quick filter's text, or a named host predicate. | FilterChangedEvent | no |
| group:toggled | Query | A group row was expanded or collapsed - one group, one branch, or all of them at once. | GroupToggledEvent | no |
| facet:computed | Query | A column's facet buckets finished computing, with how long it took and whether a worker did it. | FacetComputedEvent | no |
| facet:filtered | Query | A facet histogram was used to filter its column, or that filter was cleared. | FacetFilteredEvent | no |
| facet:expanded | Query | A facet panel section was opened or closed. | FacetExpandedEvent | no |
| facet:failed | Query | A column's facet buckets could not be computed. | FacetFailedEvent | no |
| column:moved | Columns | A column was moved to a different display position. | ColumnMovedEvent | no |
| column:resized | Columns | A column's width changed, by a header drag or by `grid.columns.resize()`. | ColumnResizedEvent | no |
| column:visible | Columns | Columns were shown or hidden. | ColumnVisibleEvent | no |
| column:pinned | Columns | A column was pinned to a side, or unpinned. | ColumnPinnedEvent | no |
| column:grouped | Columns | The row grouping changed: which columns the rows are grouped by. | ColumnGroupedEvent | no |
| column:pivoted | Columns | The pivot changed: which columns the rows are pivoted by, locally or pushed down to the backend. | ColumnPivotedEvent | no |
| column:filter:open | Columns | The header's filter affordance was activated and the column's filter popup should open. | ColumnMenuEvent | no |
| column:profile:open | Columns | The column menu's profile item was activated and the column's profile should open. | ColumnMenuEvent | no |
| column:menu:open | Columns | The header's menu affordance was activated and the column menu should open. | ColumnMenuEvent | no |
| pivot:drill | Columns | A pivot measure cell was drilled into; the payload names the row and column paths behind it. | PivotDrillEvent | no |
| columns:changed | Columns | The column set changed other than by moving, resizing, hiding or pinning - a type inference pass rewrote it. | ColumnsChangedEvent | no |
| columns:tagged | Columns | `grid.columns.showTagged()` chose which columns to show from their tags. | ColumnsTaggedEvent | no |
| columngroup:changed | Columns | A banded header group was formed, renamed, moved, dissolved, removed or restored from state. | ColumnGroupChangedEvent | no |
| header:contextmenu | Columns | A context menu was requested on a column header. | HeaderContextMenuEvent | no |
| selection:changed | Selection and view | The row selection changed and was accepted (a `beforeSelect` veto raises `selection:cancelled` instead). | SelectionChangedEvent | no |
| range:changed | Selection and view | The selected cell ranges changed. | RangeChangedEvent | no |
| clipboard:copy | Selection and view | A copy to the clipboard was attempted; `ok` says whether it reached the clipboard. | ClipboardCopyEvent | no |
| page:changed | Selection and view | The page or the page size changed. | PageChangedEvent | no |
| scroll | Selection and view | The viewport scrolled to a new offset; fires only when the offset actually moved, not on a refresh. | ScrollEvent | no |
| scroll:end | Selection and view | Scrolling settled: the last of a scroll gesture's frames has been drawn. | ScrollEvent | no |
| size:changed | Selection and view | The host element's box changed size, as reported by the `ResizeObserver` the grid watches it with. | no payload | no |
| detail:toggled | Selection and view | A master-detail region was opened or closed. | DetailToggledEvent | no |
| toolpanel:focus | Selection and view | The keyboard asked for focus to move to the tool panel (Ctrl+Alt+P). | no payload | no |
| highlight:changed | Selection and view | The set of host-declared highlights changed. | HighlightChangedEvent | no |
| find:changed | Selection and view | The find bar's query, open state or match count changed. | FindChangedEvent | no |
| tree:loading | Tree data | A tree branch was expanded and `tree.loadChildren` was called for it. | TreeLoadingEvent | no |
| tree:loaded | Tree data | A tree branch's children arrived and were added. | TreeLoadedEvent | no |
| tree:loadFailed | Tree data | A tree branch's `loadChildren` rejected; the branch is left unloaded so it can be retried. | TreeLoadFailedEvent | no |
| tree:loadAborted | Tree data | A tree branch was collapsed before its children arrived, so the fetch was abandoned. | TreeLoadAbortedEvent | no |
| state:changed | State, history and views | One logical state change - a gesture, an apply, an undo or a reset - announced once, whatever routed it. | StateChangedEvent | no |
| state:reset | State, history and views | `grid.state.reset()` restored the arrangement the grid was built with. | StateResetEvent | no |
| history:changed | State, history and views | The undo/redo stacks moved: what can now be undone or redone. | HistoryChangedEvent | no |
| history:applied | State, history and views | An undo or redo step was applied. | HistoryAppliedEvent | no |
| views:changed | State, history and views | The saved-view list changed, for any reason; the named `view:*` events say which view moved. | ViewsChangedEvent | no |
| view:applied | State, history and views | A saved view was applied to the grid. | ViewAppliedEvent | no |
| view:saved | State, history and views | A saved view was created, updated or imported. | ViewChangedEvent | no |
| view:removed | State, history and views | A saved view was deleted. | ViewChangedEvent | no |
| view:renamed | State, history and views | A saved view was renamed. | ViewChangedEvent | no |
| view:default | State, history and views | A saved view was made the default one. | ViewChangedEvent | no |
| validation:failed | State, history and views | A declared column rule refused an edit; the failures name the column and the message for each. | ValidationFailedEvent | no |
| validation:cleared | State, history and views | Recorded validation errors were cleared - for one cell, one row, or the whole grid. | ValidationClearedEvent | no |
| formatting:changed | Formatting and presentation | A conditional-formatting rule was added, changed, removed or replaced. | FormattingChangedEvent | no |
| redaction:changed | Formatting and presentation | The set of redacted columns changed. | RedactionChangedEvent | no |
| permissions:changed | Formatting and presentation | The per-column permission levels changed. | PermissionsChangedEvent | no |
| presentation:changed | Formatting and presentation | Either the responsive presentation switched between the table and the card layout, or `presentation.start()` was called again while already running. | PresentationChangedEvent | no |
| presentation:started | Formatting and presentation | `grid.presentation.start()` began presenting. | PresentationStartedEvent | no |
| presentation:ended | Formatting and presentation | `grid.presentation.stop()` stopped presenting. | no payload | no |
| presentation:view | Formatting and presentation | The presentation stepped to a view in its deck, including the first one. | PresentationViewEvent | no |
| presentation:scale | Formatting and presentation | The presentation's enlargement changed. | PresentationScaleEvent | no |
| presentation:spotlight | Formatting and presentation | The presentation's spotlight was armed over some rows and columns, or cleared. | PresentationSpotlightEvent | no |
| presentation:captured | Formatting and presentation | A screenshot of the grid was captured (`grid.capture()`), with the image's size and type. | PresentationCapturedEvent | no |
| comment:added | Collaboration | A comment was added to a cell, or a reply added to a thread. | CommentAddedEvent | no |
| comment:edited | Collaboration | A comment's text was edited. | CommentEvent | no |
| comment:deleted | Collaboration | A comment was deleted. | CommentEvent | no |
| comment:failed | Collaboration | A comment operation could not reach the backend; `operation` names which one. | CommentFailedEvent | no |
| comment:resolved | Collaboration | A comment thread was marked resolved. | CommentResolvedEvent | no |
| comment:unresolved | Collaboration | A resolved comment thread was reopened. | CommentResolvedEvent | no |
| comment:threadOpened | Collaboration | A cell's comment thread was opened. | CommentThreadOpenedEvent | no |
| comment:threadClosed | Collaboration | A cell's comment thread was closed or dismissed. | CommentThreadClosedEvent | no |
| comment:indexLoaded | Collaboration | The comment index for the visible rows finished loading, with how many entries it carried. | CommentIndexLoadedEvent | no |
| presence:published | Collaboration | This grid published its own presence - the cell it is on, its selection - to the presence transport. | PresencePublishedEvent | no |
| presence:joined | Collaboration | A peer appeared in the presence channel for the first time. | PresencePeerEvent | no |
| presence:updated | Collaboration | A peer already present moved or changed what it is doing. | PresencePeerEvent | no |
| presence:left | Collaboration | A peer left the presence channel or timed out. | PresenceLeftEvent | no |
| presence:failed | Collaboration | A presence subscribe or publish could not reach the transport. | PresenceFailedEvent | no |
| presence:lockRefused | Collaboration | An edit was refused because a peer holds the cell's lock. | PresenceLockRefusedEvent | no |
| diff:changed | Comparison and time | Diff mode was turned on against a snapshot, or turned off. | DiffChangedEvent | no |
| diff:swapped | Comparison and time | The two sides of a diff were swapped. | DiffSwappedEvent | no |
| timeline:attached | Comparison and time | The timeline scrubber began recording what each change replaces. | TimelineAttachedEvent | no |
| timeline:detached | Comparison and time | The timeline scrubber stopped recording and the grid returned to the present. | no payload | no |
| timeline:seek | Comparison and time | The timeline finished moving and the grid now stands at that position. | TimelineSeekEvent | no |
| timeline:seeking | Comparison and time | The timeline is about to move, with where it is coming from and going to. | TimelineSeekingEvent | no |
| annotation:changed | Annotations | The annotation overlay's marks changed: one was drawn, moved or erased, or the tool changed. | AnnotationChangedEvent | no |
| export:progress | Export | A streaming export wrote another chunk, with rows written, rows expected and bytes so far. | ExportProgressEvent | no |
| export:request | Export | A remote export request is about to be handed to the host's `export.remote.fetch` hook. | ExportRequestEvent | no |
| export:done | Export | A remote export came back and the file was handed over (or downloaded). | ExportDoneEvent | no |
| shortcuts:opened | Keyboard help overlay (past-tense notifications) | The keyboard-shortcuts overlay was opened. | no payload | no |
| shortcuts:closed | Keyboard help overlay (past-tense notifications) | The keyboard-shortcuts overlay was closed. | no payload | no |
| print:before | Print (past-tense notifications, | Print mode has been applied and the grid laid out un-virtualised, just before the print dialog. | PrintEvent | no |
| print:after | Print (past-tense notifications, | The print dialog has returned and print mode has been undone. | PrintEvent | no |
| beforeEdit | Cancellable before-events | A user or AI edit is about to be committed; call `preventDefault(reason?)` to stop it. | BeforeEditEvent | yes |
| beforeSort | Cancellable before-events | A user sort is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeSortEvent | yes |
| beforeFilter | Cancellable before-events | A user filter - structured or quick - is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeFilterEvent | yes |
| beforeColumnMove | Cancellable before-events | A user column move is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeColumnMoveEvent | yes |
| beforeColumnResize | Cancellable before-events | A user column resize is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeColumnResizeEvent | yes |
| beforeColumnHide | Cancellable before-events | A user column hide is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeColumnHideEvent | yes |
| beforeSelect | Cancellable before-events | A user selection change is about to be announced; call `preventDefault(reason?)` to snap it back. | BeforeSelectEvent | yes |
| beforeRowAdd | Cancellable before-events | A user row append is about to be sent; call `preventDefault(reason?)` to stop it. | BeforeRowAddEvent | yes |
| beforeDelete | Cancellable before-events | A user row delete is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeDeleteEvent | yes |
| beforeRowMove | Cancellable before-events | A user row reorder is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeRowMoveEvent | yes |
| beforeGroup | Cancellable before-events | A user group expand or collapse is about to be applied; call `preventDefault(reason?)` to stop it. | BeforeGroupEvent | yes |
| beforeRowReceive | Row transfer between grids | A row dragged from another grid is about to be inserted here; call `preventDefault(reason?)` to refuse it. | BeforeRowReceiveEvent | yes |
| edit:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeEdit` handler vetoed the commit, or it went stale while an async handler was thinking. | EditCancelledEvent | no |
| sort:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeSort` handler vetoed the sort. | SortCancelledEvent | no |
| filter:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeFilter` handler vetoed the filter. | FilterCancelledEvent | no |
| columnMove:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeColumnMove` handler vetoed the move. | ColumnMoveCancelledEvent | no |
| columnResize:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeColumnResize` handler vetoed the resize. | ColumnResizeCancelledEvent | no |
| columnHide:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeColumnHide` handler vetoed the hide. | ColumnHideCancelledEvent | no |
| selection:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeSelect` handler vetoed the selection change, which has been snapped back. | SelectionCancelledEvent | no |
| rowAdd:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeRowAdd` handler vetoed the append. | RowAddCancelledEvent | no |
| delete:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeDelete` handler vetoed the delete, or the rows were gone by the time an async handler settled. | DeleteCancelledEvent | no |
| rowMove:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeRowMove` handler vetoed the reorder. | RowMoveCancelledEvent | no |
| group:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeGroup` handler vetoed the expand or collapse. | GroupCancelledEvent | no |
| rowReceive:cancelled | Their cancellation notifications (past-tense, non-cancellable). | A `beforeRowReceive` handler refused the drop, or the drop went stale while an async handler was thinking. | RowReceiveCancelledEvent | no |
| * | Every event at once, for logging and debugging. | Every event above, delivered to one handler; the payload is whichever event fired. | GridEvent | no |
AnnotationChangedEvent
`annotation:changed`: the annotation overlay's marks or tool changed.
| Property | Type | Description |
|---|---|---|
| tool | AnnotationTool | null | The tool now in use, or null when none is. |
| count | number | How many marks the layer now holds. |
BeforeColumnHideEvent
`beforeColumnHide`: one or more columns are about to be hidden.
| Property | Type | Description |
|---|---|---|
| columns | string[] | The columns about to be hidden. |
BeforeColumnMoveEvent
`beforeColumnMove`: a column is about to be moved.
| Property | Type | Description |
|---|---|---|
| column | string | The column being moved. |
| to | number | The display index it would take. |
BeforeColumnResizeEvent
`beforeColumnResize`: a column is about to be resized.
| Property | Type | Description |
|---|---|---|
| column | string | The column being resized. |
| width | number | The width it would take, in pixels. |
BeforeDeleteEvent
`beforeDelete`: one or more rows are about to be deleted.
| Property | Type | Description |
|---|---|---|
| key | string | The first key about to be deleted. |
| keys | string[] | Every key about to be deleted, on the multi-row gesture. (optional) |
| rows | string[] | Every key about to be deleted. |
BeforeEditEvent
`beforeEdit`: a cell or row edit is about to be committed. Raised only for a person's or the AI's write - origin `'user'` or `'ai'`. An `'api'` write (`grid.edit.setCells` with no origin, and the paste, fill and clear that funnel through it) is not gated and raises nothing, so nothing that worked before the gate existed changed shape.
| Property | Type | Description |
|---|---|---|
| row | Row | The row about to be committed. |
| key | string | That row's key. |
| mode | EditMode 'cell' | 'row' | Whether it is a cell edit or a row edit. |
| changes | { colId: string; oldValue: unknown; newValue: unknown }[] | The writes that are about to be made. |
BeforeEvent
A cancellable *before*-event, delivered to `on('beforeX')` handlers before a user-initiated mutation is applied. A handler cancels the pending action by calling `preventDefault(reason?)`; the mutation is then abandoned and a past-tense `<action>:cancelled` event carries the reason. A handler may be `async` (or return a Promise): the grid awaits every before-handler before deciding, so a confirm dialog or a server check can gate the write. Any one handler preventing cancels the action. The action-specific fields (the edited cells, the target index, the affected rows) are spread alongside these, so a handler decides without reaching into grid internals. `origin` distinguishes a genuine user gesture from a host/module-driven or remote write, which is how a module whose move re-enters core is deduplicated by the host.
| Property | Type | Description |
|---|---|---|
| defaultPrevented | boolean | True once any handler has called `preventDefault` or returned false. |
| reason | string | null | The reason given to `preventDefault`, or null; `'stale'` when re-validation failed. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| preventDefault | (reason?: string): void | reason?: string | void | Cancel the pending action; the optional reason is surfaced on the cancellation event. |
BeforeFilterEvent
| Property | Type | Description |
|---|---|---|
| filters | FilterSet | The structured filter about to be applied, on a `kind: 'structured'` firing. (optional) |
| quick | string | The quick-filter text about to be applied, on a `kind: 'quick'` firing. (optional) |
| kind | FilterKind 'structured' | 'quick' | Which filter this is. |
BeforeGroupEvent
`beforeGroup`: a group row is about to be expanded or collapsed.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the group row. |
| expanded | boolean | True when it is being opened, false when it is being closed. |
BeforeRowAddEvent
`beforeRowAdd`: a record is about to be appended through the pending-row path.
| Property | Type | Description |
|---|---|---|
| row | Record<string, unknown> | The record about to be appended. |
BeforeRowMoveEvent
`beforeRowMove`: a row is about to be reordered within this grid.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row being moved. |
| from | number | The display index it is at. |
| to | number | The display index it would take. |
BeforeRowReceiveEvent
The `beforeRowReceive` event: a row dragged from another grid is about to be inserted into this one. Fires on the **receiving** grid, before the insert, with the row under the pointer named - so a drop that means "assign this to that" can be recorded by the host and the insert stopped with `preventDefault(reason)`. A veto leaves the source grid untouched: the row stays where it was, and neither `row:sent` nor `row:copied` fires there. The source removes its row only after the target has admitted it, and a veto is a refusal to admit. The paired `rowReceive:cancelled` carries the same context plus the reason. Like every {@link BeforeEvent}, the handler may be `async`; the insert is held until it settles, and is cancelled as `'stale'` if the source row is gone by then, or if the row under the pointer is gone or has moved to a different index - `at` names a slot as "before `overKey`", and once that is no longer where `overKey`'s row sits, `at` is a stale index into a list that changed while the handler was thinking, not the slot the drop meant. `overKey: null` (the drop landed on no row) has no row to drift against and is never stale on that account.
| Property | Type | Description |
|---|---|---|
| data | Record<string, unknown> | The row about to be inserted: a shallow copy of the source row's data, and the very object that is inserted if no handler vetoes, so a change made to it here lands with the row. |
| at | number | The display index the row would be inserted at: the index of the row under the pointer, or `rows.count()` when the drop landed on no row. When `overKey` names a row, this is guaranteed to still be that row's index at the moment the insert actually runs - an async handler that leaves the named row at a different index causes the drop to be cancelled as `'stale'` rather than inserted at this index regardless. |
| overKey | string | null | The key of the row under the pointer when the drop happened - the row the user meant. Null when the drop landed past the last row, on empty space, on the header, or on a pinned row: there is no row to name, and a nearest guess would be wrong in a way that looks right. |
| source | Grid | The grid the row is being dragged from. |
BeforeSelectEvent
`beforeSelect`: the user changed the selection, which has not been announced yet.
| Property | Type | Description |
|---|---|---|
| keys | string[] | The keys the user has just selected. |
| previous | string[] | The keys the selection would snap back to on a veto. |
BeforeSortEvent
`beforeSort`: the user asked for a sort, which has not been applied yet.
| Property | Type | Description |
|---|---|---|
| sort | SortEntry[] | The sort that is about to be applied. |
CellChangedEvent
`cell:changed`: a cell's value was written. Fired by an edit commit, by a revert, and by each cell an undo or redo step moves - `revert` and `undo` say which, and both are absent on a plain edit.
| Property | Type | Description |
|---|---|---|
| row | Row | The row the cell belongs to. |
| key | string | That row's key. |
| colId | string | The column id that was written. |
| value | unknown | The value the cell now holds. |
| oldValue | unknown | The value it held before. |
| revert | boolean | True when the write put back a value the server refused. (optional) |
| reason | string | null | Why it was reverted, or null. (optional) |
| undo | boolean | True when the write came from an undo step rather than a redo. (optional) |
CellConfirmedEvent
`cell:confirmed`: the server accepted a pending cell edit.
| Property | Type | Description |
|---|---|---|
| row | Row | The row the cell belongs to. |
| key | string | That row's key. |
| colId | string | The column id that was written. |
| value | unknown | What the server confirmed, which need not be what was sent. |
| id | string | The id the op was tracked under. |
| superseded | boolean | True when a newer write on the same cell had already replaced this one. |
CellConflictEvent
`cell:conflict`: the server confirmed, but returned a row that disagrees.
| Property | Type | Description |
|---|---|---|
| row | Row | The row the cell belongs to. |
| key | string | That row's key. |
| colId | string | The column id that was written. |
| value | unknown | What the server confirmed for the cell. |
| serverRow | Record<string, unknown> | The row the server sent back, which the grid applied over its own. |
| id | string | The id the op was tracked under. |
CellContextMenuEvent
`cell:contextmenu`: a context menu was requested on a cell. Raised twice over, by two routes with different payloads: the keyboard's menu key goes through the grid's action table and carries `rowIndex` and `colId`; the pointer goes through the renderer and carries the full cell with the pointer position. A handler that wants the position must read it defensively (F-1688-E).
| Property | Type | Description |
|---|---|---|
| row | Row | The row the menu was requested on. |
| key | string | That row's key; absent on the keyboard route. (optional) |
| index | number | Its display index; absent on the keyboard route. (optional) |
| rowIndex | number | Its display index, on the keyboard route. (optional) |
| colId | string | The column id the menu was requested on. |
| column | Column | The resolved column; absent on the keyboard route. (optional) |
| value | unknown | The cell's value; absent on the keyboard route. (optional) |
| x | number | The pointer's viewport x, on the pointer route. (optional) |
| y | number | The pointer's viewport y, on the pointer route. (optional) |
| event | unknown | The DOM event behind this one, on the pointer route. (optional) |
CellPendingEvent
`cell:pending`: an optimistic cell edit was sent and is awaiting an answer.
| Property | Type | Description |
|---|---|---|
| row | Row | The row the cell belongs to. |
| key | string | That row's key. |
| colId | string | The column id that was written. |
| value | unknown | The value that was sent. |
| before | unknown | The value it is holding in reserve to put back if the write is refused. |
| id | string | The id the op is tracked under; `grid.edit.settle(id, …)` answers it. |
CellPointerEvent
The pointer events a cell raises: `cell:clicked`, `cell:dblclicked`, `cell:mouseover`, `cell:mouseout`, `cell:mousedown` and `cell:mouseup`. All six carry the cell, its value and the DOM event behind them. `target` - the cell element - is carried by the hover and press pairs, which exist precisely so a host does not have to find that node itself: rows and cells are pooled and re-used as the grid scrolls, so a listener a host bound to a cell node would fire for whichever row occupies it next.
| Property | Type | Description |
|---|---|---|
| row | Row | The row under the pointer. |
| key | string | That row's key. |
| index | number | Its display index. |
| colId | string | The column id under the pointer. |
| column | Column | The resolved column. |
| value | unknown | The cell's value, before formatting. |
| text | string | The cell's text, as it is drawn. |
| event | unknown | The DOM event behind this one, for modifier keys and `preventDefault`. |
| target | unknown | The cell element, on the hover and press pairs; absent on click and double-click. (optional) |
CellRevertedEvent
`cell:reverted`: a pending cell edit was refused and rolled back.
| Property | Type | Description |
|---|---|---|
| row | Row | The row the cell belongs to. |
| key | string | That row's key. |
| colId | string | The column id that was written. |
| rejected | unknown | The value the server refused. |
| restored | unknown | The value put back, or `undefined` when a newer write owns the cell. |
| reason | string | null | Why it was refused, or null when the transport gave no reason. |
| id | string | The id the op was tracked under. |
| superseded | boolean | True when a newer write on the same cell had already replaced this one. |
| applied | boolean | True when the rollback was actually applied; false when it was superseded. |
ClipboardCopyEvent
`clipboard:copy`: a copy to the clipboard was attempted.
| Property | Type | Description |
|---|---|---|
| text | string | The text that was put on the clipboard; empty when the copy was refused. |
| ok | boolean | Whether it reached the clipboard. |
| rows | string | What was copied: `'range'`, or whichever row scope the options asked for. |
| reason | string | Why a refused copy was refused - `'discontiguous'` for a non-rectangular range. (optional) |
ColumnGroupChangedEvent
`columngroup:changed`: a banded header group was formed, renamed, moved, dissolved, removed or restored.
| Property | Type | Description |
|---|---|---|
| action | ColumnGroupAction 'formed' | 'removed' | 'renamed' | 'dissolved' | 'moved' | 'applied' | What happened to it. |
| groupId | string | The band the action was on, where it has an id. (optional) |
| id | string | The leaf column removed from a band, on `'removed'`. (optional) |
| ids | string[] | The leaves a band was formed over, on `'formed'`. (optional) |
| to | number | The display position a band moved to, on `'moved'`. (optional) |
| title | string | The band's new title, on `'renamed'`. (optional) |
| dissolved | boolean | True when removing the last leaf dissolved the band with it. (optional) |
ColumnGroupedEvent
`column:grouped`: the row grouping changed.
| Property | Type | Description |
|---|---|---|
| columns | string[] | The column ids the rows are grouped by, outermost first; empty when grouping was cleared. |
ColumnHideCancelledEvent
`columnHide:cancelled`: a `beforeColumnHide` handler vetoed the hide.
| Property | Type | Description |
|---|---|---|
| columns | string[] | The columns that were not hidden. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
ColumnMenuEvent
`column:filter:open`, `column:profile:open` and `column:menu:open`: the header asked for a popup to be opened over a column. The grid raises these rather than opening anything itself, so a host can put its own control where the built-in one would go.
| Property | Type | Description |
|---|---|---|
| colId | string | The column the popup belongs to. |
| element | unknown | The header element to anchor it to, where the caller had one. (optional) |
ColumnMoveCancelledEvent
`columnMove:cancelled`: a `beforeColumnMove` handler vetoed the move.
| Property | Type | Description |
|---|---|---|
| column | string | The column that was not moved. |
| to | number | The display index it would have taken. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
ColumnMovedEvent
`column:moved`: a column was moved to a different display position. Two routes, two spellings of the same thing: the column model names it `id`, and the header drag names it `colId` (F-1688-C). Read whichever is present.
| Property | Type | Description |
|---|---|---|
| id | string | The column that moved, on the model route. (optional) |
| colId | string | The column that moved, on the header-drag route. (optional) |
| to | number | The display index it moved to. |
ColumnPinnedEvent
`column:pinned`: a column was pinned to a side, or unpinned.
| Property | Type | Description |
|---|---|---|
| id | string | The column that was pinned. |
| side | Edge | null | Which side it is pinned to now, or null when it was unpinned. The sides are the writing-direction ones {@link ColumnApi#pin} takes - `'start'` and `'end'` - not left and right, so a right-to-left grid reports the same value for the same gesture. |
ColumnPivotedEvent
`column:pivoted`: the pivot changed, locally or pushed down to the backend.
| Property | Type | Description |
|---|---|---|
| columns | string[] | The column ids the rows are pivoted by, on a local pivot. (optional) |
| pivotFields | string[] | The fields the backend was asked to pivot by, on a pushed-down pivot. (optional) |
| remote | boolean | True when the backend did the pivot. (optional) |
ColumnResizeCancelledEvent
`columnResize:cancelled`: a `beforeColumnResize` handler vetoed the resize.
| Property | Type | Description |
|---|---|---|
| column | string | The column that was not resized. |
| width | number | The width it would have taken, in pixels. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
ColumnResizedEvent
`column:resized`: a column's width changed. Named `id` by the model and `colId` by the header drag (F-1688-C).
| Property | Type | Description |
|---|---|---|
| id | string | The column that was resized, on the model route. (optional) |
| colId | string | The column that was resized, on the header-drag route. (optional) |
| width | number | Its new width, in pixels. |
ColumnVisibleEvent
`column:visible`: columns were shown or hidden.
| Property | Type | Description |
|---|---|---|
| ids | string[] | The columns whose visibility actually changed. |
| hidden | boolean | True when they were hidden, false when they were shown. |
ColumnsChangedEvent
`columns:changed`: the column set was rewritten other than by moving, resizing, hiding or pinning.
| Property | Type | Description |
|---|---|---|
| reason | string | Why it was rewritten; `'inferred'` when a type-inference pass did it. |
| types | Record<string, string> | The type inferred for each column, by column id. |
ColumnsTaggedEvent
`columns:tagged`: `grid.columns.showTagged()` chose the visible set from the columns' tags.
| Property | Type | Description |
|---|---|---|
| tags | string[] | The tags that were asked for. |
| hidden | string[] | The columns hidden because they carry none of them. |
CommentAddedEvent
`comment:added`: a comment was added to a cell, or a reply added to a thread.
| Property | Type | Description |
|---|---|---|
| cellKey | string | The cell the comment is on, as the provider keys it. |
| commentId | string | The stored comment's id, where the provider returned one. (optional) |
| parentId | string | null | The comment this one replies to, or null when it starts a thread. |
CommentEvent
`comment:edited` and `comment:deleted`: one comment changed.
| Property | Type | Description |
|---|---|---|
| cellKey | string | The cell the comment is on. |
| commentId | string | The comment that changed. |
CommentFailedEvent
`comment:failed`: a comment operation could not reach the backend.
| Property | Type | Description |
|---|---|---|
| operation | CommentOperation 'loadIndex' | 'loadThread' | 'addComment' | 'editComment' | 'deleteComment' | 'resolveThread' | 'unresolveThread' | Which provider call failed. |
| cellKey | string | The cell it was for, where the call named one. (optional) |
| commentId | string | The comment it was for, where the call named one. (optional) |
| error | unknown | What the provider threw or rejected with. |
CommentIndexLoadedEvent
`comment:indexLoaded`: the comment index for the visible rows finished loading.
| Property | Type | Description |
|---|---|---|
| rows | number | How many rows the index was asked for. |
| entries | number | How many entries came back. |
| ms | number | How long it took, in milliseconds. |
CommentResolvedEvent
`comment:resolved` and `comment:unresolved`: a thread was marked resolved or reopened.
| Property | Type | Description |
|---|---|---|
| cellKey | string | The cell whose thread changed. |
CommentThreadClosedEvent
`comment:threadClosed`: a cell's comment thread was closed.
| Property | Type | Description |
|---|---|---|
| cellKey | string | The cell whose thread was closed. |
| reason | string | Why it closed; `'dismissed'` when the caller gave no reason. |
CommentThreadOpenedEvent
`comment:threadOpened`: a cell's comment thread was opened.
| Property | Type | Description |
|---|---|---|
| cellKey | string | The cell whose thread was opened. |
| rowId | string | The row it sits on. |
| field | string | The column it sits on. |
| value | unknown | The cell's value, so a thread header can quote what is being discussed. |
ConfigChangedEvent
`config:changed`: a configuration key was written at run time. `grid.set(key, value)` fills the single form; `grid.setAll(values)` fills the batch form and fires once for the whole batch rather than once per key, so a host restoring a saved arrangement hears one event describing a settled grid instead of a dozen describing half-applied ones.
| Property | Type | Description |
|---|---|---|
| key | string | The key that was written, on a `grid.set` change. (optional) |
| value | unknown | Its new value, on a `grid.set` change. (optional) |
| oldValue | unknown | What it held before, on a `grid.set` change. (optional) |
| keys | string[] | The keys that were written, on a `grid.setAll` change. (optional) |
| values | Record<string, unknown> | Their new values, by key, on a `grid.setAll` change. (optional) |
| oldValues | Record<string, unknown> | What each of them held before, by key, on a `grid.setAll` change. (optional) |
DeleteCancelledEvent
`delete:cancelled`: a `beforeDelete` handler vetoed the delete, or the rows were gone by the time it settled.
| Property | Type | Description |
|---|---|---|
| key | string | The first key that was not deleted. |
| keys | string[] | Every key that was not deleted, on the multi-row gesture. (optional) |
| rows | string[] | Every key that was not deleted. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
DetailToggledEvent
`detail:toggled`: a master-detail region was opened or closed.
| Property | Type | Description |
|---|---|---|
| keys | string[] | The keys of every row with an open detail region. |
| active | string | null | The key of the region that is mounted, or null when none is. |
DiffChangedEvent
`diff:changed`: diff mode was turned on against a snapshot, or turned off.
| Property | Type | Description |
|---|---|---|
| enabled | boolean | Whether the grid is now diffing. |
DiffSwappedEvent
`diff:swapped`: the two sides of a diff were swapped.
| Property | Type | Description |
|---|---|---|
| swapped | boolean | True when the grid is now showing the snapshot as the "after" side. |
| rows | number | How many rows are on the side now being shown. |
| snapshot | number | How many rows are on the side it came from. |
EditCancelledEvent
`edit:cancelled`: a `beforeEdit` handler vetoed the commit, or it went stale.
| Property | Type | Description |
|---|---|---|
| row | Row | The row whose commit was abandoned. |
| key | string | That row's key. |
| mode | EditMode 'cell' | 'row' | Whether it was a cell edit or a row edit. |
| changes | { colId: string; oldValue: unknown; newValue: unknown }[] | The writes that would have been made. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
EditEndEvent
`cell:edit:end` and `row:edit:end`: an editor closed. `valid: false` means a column rule refused the commit and the editor stayed the user's problem; `cancelled: true` means nothing was written, either because the user pressed Escape or because a `beforeEdit` handler vetoed.
| Property | Type | Description |
|---|---|---|
| row | Row | The row that was being edited. |
| key | string | That row's key. |
| colId | string | null | The column the caret was in; null on a row editor with none. |
| valid | boolean | True when the commit passed validation, false when a rule refused it. |
| cancelled | boolean | True when nothing was written - Escape, or a vetoed commit. (optional) |
| errors | ValidationError[] | The validation failures, when `valid` is false. (optional) |
| writes | { key: string; colId: string; before: unknown; after: unknown }[] | The cells that were written; empty on a cancel. (optional) |
EditStartEvent
`cell:edit:start` and `row:edit:start`: an editor opened.
| Property | Type | Description |
|---|---|---|
| row | Row | The row being edited. |
| key | string | That row's key. |
| colId | string | null | The column the caret is in; null on a row editor with no focused column. |
| column | Column | The resolved column the caret is in. |
| keyName | string | The key that opened the editor, when a keypress did. (optional) |
| charPress | string | The character typed into the cell to open it, when typing did. (optional) |
EventPayloads
What a handler receives, per event. `on()` is declared as `on(event: EventName, handler: EventHandler)`, so the declarations named every event and typed none of their payloads. The published reference could list the names and nothing else, which is half an event reference: a reader still has to run the grid to find out what arrives. This map is the other half. It is populated from the payload interfaces that already exist and from the comments in {@link EventName} that name them - an event with no entry here publishes `unknown` in the reference and is counted by the undescribed-member ratchet in `tools/check.js`, so the gaps are visible and shrink rather than being papered over with a generic type. Every payload extends {@link GridEvent}; an entry says which specialisation. An entry of `void` means the event carries nothing of its own: the bus still hands the handler the {@link GridEvent} envelope - `type`, `origin` and `grid` - and the reference prints "no payload" rather than a type.
| Property | Type | Description |
|---|---|---|
| ready | void | Nothing: the grid being ready is the whole message. |
| destroy | void | Nothing: the grid is still readable from the handler, and that is the point. |
| render:first | void | Nothing: the first frame's window is reported by `render:done`, which follows it. |
| render:done | RenderDoneEvent | The window that was drawn and the milliseconds each phase took. |
| config:changed | ConfigChangedEvent | The key (or keys) that were written, with their old values. |
| licence:changed | LicenceChangedEvent | The verdict and what this deployment is now treated as. |
| model:changed | ModelChangedEvent | Why the model was rebuilt, and whatever that reason has to say. |
| rows:changed | RowsChangedEvent | Which rows moved - records when `identified`, counts on a companion firing. |
| rows:queued | RowsQueuedEvent | How much is waiting on the batch queue. |
| rows:deferred | RowsDeferredEvent | How much a flush carried into the next frame, and the budget it ran out of. |
| rows:paused | RowsFlowEvent | The whole feed counter set, as `grid.changes.stats()` returns it. |
| rows:resumed | RowsFlowEvent | The whole feed counter set, as `grid.changes.stats()` returns it. |
| row:received | RowReceivedEvent | The row that arrived, where it landed, and anything the insert refused. |
| row:sent | RowTransferEvent | The row that left and whether it was moved or copied. |
| row:copied | RowTransferEvent | The row that was copied out and left here as well. |
| row:moved | RowMovedEvent | The row that was reordered, and the indices it moved between. |
| source:error | SourceErrorEvent | What the source threw, and what it was fetching. |
| source:total | SourceTotalEvent | The exact total a deferred count settled on, and the level it counts. |
| stream:chunk | StreamChunkEvent | How much of the stream has arrived and how much is expected. |
| stream:end | StreamEndEvent | The final row count and whether the stream promoted to memory. |
| stream:evicted | StreamEvictedEvent | How many rows the window dropped, and how many are still live. |
| rowDrag:started | RowDragEvent | The row-drag gesture; all four carry the same payload. |
| rowDrag:moved | RowDragEvent | The row being dragged, the grid under the pointer, and where it would land. |
| rowDrag:left | RowDragEvent | The grid the pointer has just left, with no candidate index to report. |
| rowDrag:ended | RowDragEvent | Where the drag ended and whether the release is being acted on. |
| cell:changed | CellChangedEvent | The cell that was written, with its old and new values. |
| cell:pending | CellPendingEvent | The cell that was sent, the value held in reserve, and the op id. |
| cell:confirmed | CellConfirmedEvent | What the server confirmed, which need not be what was sent. |
| cell:reverted | CellRevertedEvent | The value that was refused, the value put back, and why. |
| cell:conflict | CellConflictEvent | The row the server sent back, which disagrees with what the grid holds. |
| cell:clicked | CellPointerEvent | The cell that was clicked, with its value and the DOM event. |
| cell:dblclicked | CellPointerEvent | The cell that was double-clicked, with its value and the DOM event. |
| cell:contextmenu | CellContextMenuEvent | The cell the menu was requested on; the two routes fill different fields (F-1688-E). |
| cell:mouseover | CellPointerEvent | The cell entered, plus its element as `target`. |
| cell:mouseout | CellPointerEvent | The cell left, plus its element as `target`. |
| cell:mousedown | CellPointerEvent | The cell pressed, plus its element as `target`. |
| cell:mouseup | CellPointerEvent | The cell released over, plus its element as `target`. |
| cell:edit:start | EditStartEvent | The cell being edited, and the keypress that opened the editor. |
| cell:edit:end | EditEndEvent | Whether the commit was valid, whether it was cancelled, and what was written. |
| row:edit:start | EditStartEvent | The row being edited, and the keypress that opened the editor. |
| row:edit:end | EditEndEvent | Whether the commit was valid, whether it was cancelled, and what was written. |
| row:clicked | RowPointerEvent | The row that was clicked and the DOM event. |
| row:dblclicked | RowPointerEvent | The row that was double-clicked and the DOM event. |
| row:pending | RowPendingEvent | Which structural write was sent, under which id and key. |
| row:confirmed | RowConfirmedEvent | The confirmed write, already rekeyed when it was an append. |
| row:reverted | RowRevertedEvent | The refused write, why, and whether the rollback was applied. |
| row:conflict | RowConflictEvent | The row the server sent back, which disagrees with what the grid holds. |
| form:opened | FormOpenedEvent | The row the form is editing. |
| form:closed | FormClosedEvent | The row the form was editing. |
| form:saved | FormSavedEvent | Every value the form held, which of them changed, and which mapped to no column. |
| form:error | FormErrorEvent | What went wrong, and whether it was a timeout rather than a refusal. |
| sort:changed | SortChangedEvent | The sort now in force, in precedence order. |
| filter:changed | FilterChangedEvent | Whichever of the four filter routes changed, and to what. |
| group:toggled | GroupToggledEvent | Which group moved, whether it is now open, and whether it was a deep or an all-groups toggle. |
| facet:computed | FacetComputedEvent | How many buckets, how long it took, and whether a worker did it. |
| facet:filtered | FacetFilteredEvent | The condition the facet installed, or null when it was cleared. |
| facet:expanded | FacetExpandedEvent | The facet section that opened or closed. |
| facet:failed | FacetFailedEvent | The column the facets were for, and what went wrong. |
| column:moved | ColumnMovedEvent | The column that moved and where to; spelled `id` or `colId` by route (F-1688-C). |
| column:resized | ColumnResizedEvent | The column that was resized and its new width. |
| column:visible | ColumnVisibleEvent | The columns whose visibility changed, and which way. |
| column:pinned | ColumnPinnedEvent | The column and the side it is pinned to now, or null. |
| column:grouped | ColumnGroupedEvent | The columns the rows are grouped by now. |
| column:pivoted | ColumnPivotedEvent | The columns the rows are pivoted by now, locally or on the backend. |
| column:filter:open | ColumnMenuEvent | The column whose filter popup should open, and the element to anchor it to. |
| column:profile:open | ColumnMenuEvent | The column whose profile should open. |
| column:menu:open | ColumnMenuEvent | The column whose menu should open, and the element to anchor it to. |
| pivot:drill | PivotDrillEvent | The source rows behind the measure, and the paths that identify the cell. |
| columns:changed | ColumnsChangedEvent | Why the column set was rewritten, and what was inferred. |
| columns:tagged | ColumnsTaggedEvent | The tags that were asked for and the columns hidden for carrying none. |
| columngroup:changed | ColumnGroupChangedEvent | What happened to the band, and to which one. |
| header:contextmenu | HeaderContextMenuEvent | The header the menu was requested on, and where the pointer was. |
| selection:changed | SelectionChangedEvent | The keys and rows now selected. |
| range:changed | RangeChangedEvent | Every cell range now selected. |
| clipboard:copy | ClipboardCopyEvent | The text, whether it reached the clipboard, and why not when it did not. |
| page:changed | PageChangedEvent | The page, the page size, and how many pages the data makes. |
| scroll | ScrollEvent | The viewport's new offset. |
| scroll:end | ScrollEvent | The viewport's offset once the gesture settled. |
| size:changed | void | Nothing: the new size is read off the element, which the handler already has. |
| detail:toggled | DetailToggledEvent | Which detail regions are open, and which one is mounted. |
| toolpanel:focus | void | Nothing: it is a request to move focus, not a report about state. |
| highlight:changed | HighlightChangedEvent | Every highlight now in force. |
| find:changed | FindChangedEvent | The query, whether the bar is open, and the match count. |
| tree:loading | TreeLoadingEvent | The branch whose children are being fetched. |
| tree:loaded | TreeLoadedEvent | The branch and how many children arrived. |
| tree:loadFailed | TreeLoadFailedEvent | The branch and what the loader rejected with. |
| tree:loadAborted | TreeLoadAbortedEvent | The branch whose fetch was abandoned. |
| state:changed | StateChangedEvent | One event per logical state change. |
| state:reset | StateResetEvent | The baseline that was restored. |
| history:changed | HistoryChangedEvent | What can now be undone and redone. |
| history:applied | HistoryAppliedEvent | Which way the stack moved, and the entry that was applied. |
| views:changed | ViewsChangedEvent | Every view after the change, and which one moved. |
| view:applied | ViewAppliedEvent | The view that was applied, and the id now active. |
| view:saved | ViewChangedEvent | The one view that was created, updated or imported. |
| view:removed | ViewChangedEvent | The one view that was deleted. |
| view:renamed | ViewChangedEvent | The one view that was renamed. |
| view:default | ViewChangedEvent | The one view that was made the default. |
| validation:failed | ValidationFailedEvent | Every cell a column rule refused, with its code and message. |
| validation:cleared | ValidationClearedEvent | The row and column that were cleared, or null for all of them. |
| formatting:changed | FormattingChangedEvent | What changed, in which scope, and every rule now in force. |
| redaction:changed | RedactionChangedEvent | Every column id now redacted. |
| permissions:changed | PermissionsChangedEvent | The permission level now in force for each column that has one. |
| presentation:changed | PresentationChangedEvent | Either the responsive layout's new presentation, or the deck's settings (F-1688-A). |
| presentation:started | PresentationStartedEvent | The scale, options and deck the presentation started with. |
| presentation:ended | void | Nothing: the presentation is over and there is no state left to report. |
| presentation:view | PresentationViewEvent | The view now showing and its position in the deck. |
| presentation:scale | PresentationScaleEvent | The enlargement now in force. |
| presentation:spotlight | PresentationSpotlightEvent | What is lit, or null when the spotlight was cleared. |
| presentation:captured | PresentationCapturedEvent | The captured image's size, type and file name. |
| comment:added | CommentAddedEvent | The cell, the stored comment, and the thread it replies to. |
| comment:edited | CommentEvent | The comment whose text changed. |
| comment:deleted | CommentEvent | The comment that was deleted. |
| comment:failed | CommentFailedEvent | Which provider call failed, on what, and with what. |
| comment:resolved | CommentResolvedEvent | The cell whose thread was marked resolved. |
| comment:unresolved | CommentResolvedEvent | The cell whose thread was reopened. |
| comment:threadOpened | CommentThreadOpenedEvent | The cell whose thread was opened, and the value being discussed. |
| comment:threadClosed | CommentThreadClosedEvent | The cell whose thread was closed, and why. |
| comment:indexLoaded | CommentIndexLoadedEvent | How many rows were indexed, how many entries came back, and how long it took. |
| presence:published | PresencePublishedEvent | This grid's own presence, as it was published. |
| presence:joined | PresencePeerEvent | The peer that appeared. |
| presence:updated | PresencePeerEvent | The peer that moved or changed what it is doing. |
| presence:left | PresenceLeftEvent | The peer that left, and why. |
| presence:failed | PresenceFailedEvent | Which presence call failed, and with what. |
| presence:lockRefused | PresenceLockRefusedEvent | The locked cell and the peer holding it. |
| diff:changed | DiffChangedEvent | Whether the grid is now diffing. |
| diff:swapped | DiffSwappedEvent | Which way round the diff now is, and how many rows are on each side. |
| timeline:attached | TimelineAttachedEvent | How far back the recorded window now reaches. |
| timeline:detached | void | Nothing: the grid is back in the present and nothing is recorded. |
| timeline:seek | TimelineSeekEvent | Where the grid now stands, and whether that is live. |
| timeline:seeking | TimelineSeekingEvent | Where the move is coming from and going to. |
| annotation:changed | AnnotationChangedEvent | The tool in use and how many marks the layer holds. |
| export:progress | ExportProgressEvent | Rows written, rows expected, bytes so far. |
| export:request | ExportRequestEvent | The request about to go to the host's export hook. |
| export:done | ExportDoneEvent | The request that produced the file that came back. |
| shortcuts:opened | void | Nothing: the overlay is open and there is nothing else to say about it. |
| shortcuts:closed | void | Nothing: the overlay is closed and focus has gone back where it was. |
| print:before | PrintEvent | How many rows the print covers. |
| print:after | PrintEvent | How many rows the print covered. |
| beforeEdit | BeforeEditEvent | The row, the mode and the writes about to be committed, with `preventDefault` to stop them. |
| beforeSort | BeforeSortEvent | The sort about to be applied, with `preventDefault` to stop it. |
| beforeFilter | BeforeFilterEvent | The filter about to be applied, with `preventDefault` to stop it. |
| beforeColumnMove | BeforeColumnMoveEvent | The column move about to be applied, with `preventDefault` to stop it. |
| beforeColumnResize | BeforeColumnResizeEvent | The column resize about to be applied, with `preventDefault` to stop it. |
| beforeColumnHide | BeforeColumnHideEvent | The column hide about to be applied, with `preventDefault` to stop it. |
| beforeSelect | BeforeSelectEvent | The selection about to be announced, with `preventDefault` to snap it back. |
| beforeRowAdd | BeforeRowAddEvent | The row append about to be sent, with `preventDefault` to stop it. |
| beforeDelete | BeforeDeleteEvent | The row delete about to be applied, with `preventDefault` to stop it. |
| beforeRowMove | BeforeRowMoveEvent | The row reorder about to be applied, with `preventDefault` to stop it. |
| beforeGroup | BeforeGroupEvent | The group toggle about to be applied, with `preventDefault` to stop it. |
| beforeRowReceive | BeforeRowReceiveEvent | A row dropped in from another grid, on the receiving grid. |
| edit:cancelled | EditCancelledEvent | The commit that was abandoned, and why. |
| sort:cancelled | SortCancelledEvent | The sort that was not applied, and why. |
| filter:cancelled | FilterCancelledEvent | The filter that was not applied, and why. |
| columnMove:cancelled | ColumnMoveCancelledEvent | The column move that was not applied, and why. |
| columnResize:cancelled | ColumnResizeCancelledEvent | The column resize that was not applied, and why. |
| columnHide:cancelled | ColumnHideCancelledEvent | The column hide that was not applied, and why. |
| selection:cancelled | SelectionCancelledEvent | The selection that was snapped back, and why. |
| rowAdd:cancelled | RowAddCancelledEvent | The append that was not sent, and why. |
| delete:cancelled | DeleteCancelledEvent | The delete that was not applied, and why. |
| rowMove:cancelled | RowMoveCancelledEvent | The reorder that was not applied, and why. |
| group:cancelled | GroupCancelledEvent | The group toggle that was not applied, and why. |
| rowReceive:cancelled | RowReceiveCancelledEvent | That veto's notification, with the reason. |
| * | GridEvent | Whichever past-tense event fired; the wildcard is never given a before-event. |
ExportDoneEvent
`export:done`: a remote export came back and the file was handed over.
| Property | Type | Description |
|---|---|---|
| request | Record<string, unknown> | The request that produced it. |
| remote | boolean | True - this firing is the remote path's; a local export does not raise it. |
ExportProgressEvent
`export:progress`: a streaming export wrote another chunk.
| Property | Type | Description |
|---|---|---|
| written | number | Rows written so far. |
| total | number | Rows expected in all. |
| bytes | number | Bytes written so far. |
ExportRequestEvent
`export:request`: a remote export request is about to go to the host's `export.remote.fetch` hook.
| Property | Type | Description |
|---|---|---|
| request | Record<string, unknown> | The request, as the hook will receive it: the query, the columns and the format. |
FacetComputedEvent
`facet:computed`: a column's facet buckets finished computing.
| Property | Type | Description |
|---|---|---|
| colId | string | The column the facets are for. |
| buckets | number | How many buckets the distribution was cut into. |
| ms | number | How long it took, in milliseconds. |
| worker | boolean | True when a worker computed it rather than the main thread. |
FacetExpandedEvent
`facet:expanded`: a facet panel section was opened or closed.
| Property | Type | Description |
|---|---|---|
| colId | string | The column whose section moved. |
| expanded | boolean | True when it is now open. |
FacetFailedEvent
`facet:failed`: a column's facet buckets could not be computed.
| Property | Type | Description |
|---|---|---|
| colId | string | The column the facets were for. |
| error | unknown | What went wrong. |
FacetFilteredEvent
`facet:filtered`: a facet histogram was used to filter its column, or cleared.
| Property | Type | Description |
|---|---|---|
| colId | string | The column that was filtered. |
| filter | FilterSet | The condition that was installed, or null when the filter was cleared. |
| gesture | string | The gesture behind it: `'click'`, `'drag'`, `'clear'`, or whatever the caller named. |
| buckets | [number, number] | The inclusive bucket range that was selected. (optional) |
FilterCancelledEvent
`filter:cancelled`: a `beforeFilter` handler vetoed the filter.
| Property | Type | Description |
|---|---|---|
| filters | FilterSet | The structured filter that was not applied, on a `kind: 'structured'` veto. (optional) |
| quick | string | The quick-filter text that was not applied, on a `kind: 'quick'` veto. (optional) |
| kind | FilterKind 'structured' | 'quick' | Which filter was refused. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
FilterChangedEvent
`filter:changed`: the filters changed. Four routes reach it - a structured condition, the quick filter, a named host predicate, and the comments filter - and each fills its own fields, so every one of them is optional.
| Property | Type | Description |
|---|---|---|
| filters | FilterSet | The structured filter now in force, or null when it was cleared. (optional) |
| quick | string | The quick-filter text now in force. (optional) |
| quickMode | string | How the quick filter matches. (optional) |
| where | string[] | The named host predicates now in force. (optional) |
| cause | string | `'where'` when a host predicate was registered, replaced, removed or re-run. (optional) |
| comments | string | `'unresolved'` or `'any'` when the change was the comments filter. (optional) |
FindChangedEvent
`find:changed`: the find bar's query, open state or match count changed. The count here is the model's narrower one - `current`, `total` and `complete`. `grid.find.count()` adds the windowed-scope fields on top; this event does not carry them (F-1688-D).
| Property | Type | Description |
|---|---|---|
| text | string | What is being searched for. |
| caseSensitive | boolean | Whether the search distinguishes case. |
| wholeCell | boolean | Whether the whole cell must match rather than contain. |
| columns | string[] | null | The columns being searched, or null for every visible column. |
| open | boolean | Whether the find bar is showing. |
| count | { current: number; total: number; complete: boolean } | Which match is current, how many there are, and whether the scan finished. |
FormClosedEvent
`form:closed`: the row form was closed without saving.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row the form was editing. |
FormErrorEvent
`form:error`: the row form could not load or save a row.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row the form was working on. |
| error | unknown | What went wrong. |
| timedOut | boolean | True when the load timed out rather than being refused. |
FormOpenedEvent
`form:opened`: the row form opened over a row.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row the form is editing. |
| row | Row | That row. |
FormSavedEvent
`form:saved`: the row form's values were written back to the row.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row that was saved. |
| values | Record<string, unknown> | Every value the form held, by field name. |
| changed | Record<string, unknown> | Only the values that differ from what the row held. |
| unmapped | string[] | Fields the form held that no column maps, so nothing was written for them. |
FormattingChangedEvent
`formatting:changed`: a conditional-formatting rule was added, changed, removed or replaced.
| Property | Type | Description |
|---|---|---|
| reason | string | What happened to it. |
| scope | FormattingScope | The scope that changed: a column id, or the grid scope. |
| rules | Record<FormattingScope, FormattingRule[]> | Every rule now in force, by scope. |
GridEvent
| Property | Type | Description |
|---|---|---|
| type | string | Which event this is - `rows:changed`, `sort:changed`, `cell:edit:end` and the rest. |
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Who caused the action. `'ai'` tags a write an AI proposed and a human approved, applied through `grid.edit.setCells(writes, type, { origin: 'ai' })`; it fires the same cancellable `beforeEdit` gate a `'user'` edit does, so a host can policy-gate AI writes distinctly. |
| grid | Grid | The grid that emitted it, so one handler can serve several grids. |
| [key: string] | unknown | The event's own fields, spread alongside the three above: which cell, which column, which rows. What arrives depends on the event - {@link EventPayloads} names the specialisation each one carries. |
GroupCancelledEvent
`group:cancelled`: a `beforeGroup` handler vetoed the expand or collapse.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the group row that did not move. |
| expanded | boolean | Whether it was being opened (true) or closed (false). |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
GroupToggledEvent
`group:toggled`: a group row was expanded or collapsed. One group carries `key` or `row`; "expand all" / "collapse all" carries `all: true` and no target; a deep expand carries `deep: true`.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the group row that was toggled. (optional) |
| row | Row | That group row, where the caller had it. (optional) |
| expanded | boolean | True when it is now open. |
| deep | boolean | True when the whole branch beneath it was opened. (optional) |
| all | boolean | True when every group was toggled at once. (optional) |
HeaderContextMenuEvent
`header:contextmenu`: a context menu was requested on a column header.
| Property | Type | Description |
|---|---|---|
| colId | string | The column the menu was requested on. |
| column | Column | The resolved column. |
| element | unknown | The header element, to anchor a menu to. |
| x | number | The pointer's viewport x. |
| y | number | The pointer's viewport y. |
| event | unknown | The DOM event behind this one. |
HighlightChangedEvent
`highlight:changed`: the set of host-declared highlights changed.
| Property | Type | Description |
|---|---|---|
| highlights | { scope: string; key: string | null; colId: string | null; colour: string; duration: number }[] | Every highlight in force, with its scope, target, colour and duration. |
HistoryAppliedEvent
`history:applied`: an undo or redo step was applied.
| Property | Type | Description |
|---|---|---|
| direction | HistoryDirection 'undo' | 'redo' | Which way the stack moved. |
| step | HistoryEntry | null | The entry that was applied, or null when there was nothing to apply. |
HistoryChangedEvent
`history:changed`: the undo and redo stacks moved.
| Property | Type | Description |
|---|---|---|
| canUndo | boolean | Whether there is anything to undo. |
| canRedo | boolean | Whether there is anything to redo. |
| undo | HistoryEntry | null | The entry an undo would apply, or null. |
| redo | HistoryEntry | null | The entry a redo would apply, or null. |
LicenceChangedEvent
`licence:changed`: a key was installed, and again when its check settles.
| Property | Type | Description |
|---|---|---|
| info | LicenceInfo | The verdict as it stands - provisional on the first firing, settled on the second. |
| state | LicenceState 'licensed' | 'localhost' | 'trial' | What this deployment is now treated as. |
ModelChangedEvent
`model:changed`: the display model was rebuilt. The one event every viewer of the grid follows. `reason` is what actually happened, and the rest of the payload is whatever that reason has to say - a page's block and range, a tree branch's row, a stream's anchor.
| Property | Type | Description |
|---|---|---|
| reason | string | Why it was rebuilt: `'rows'`, `'tree'`, `'expanded'`, `'children'`, `'children:loading'`, `'reload'`, `'page'`, `'expand'`, `'collapse'`, `'query'`, `'stream'`, or one of the pipeline's settle reasons. |
| count | number | How many display rows there now are, or how many children arrived. (optional) |
| row | Row | The branch row a `'children'` or `'children:loading'` rebuild is about. (optional) |
| key | string | The row key an `'expand'` or `'collapse'` is about. (optional) |
| block | string | number | The block id a `'page'` rebuild filled. (optional) |
| from | number | The first display index a `'page'` rebuild filled. (optional) |
| to | number | One past the last display index a `'page'` rebuild filled. (optional) |
| groupPath | unknown[] | The group path a remote source's `'page'` rebuild filled under. (optional) |
| shiftAboveViewport | number | How far a stream's arrivals pushed the rows above the viewport down. (optional) |
| anchor | unknown | The row the stream is holding the viewport against. (optional) |
PageChangedEvent
`page:changed`: the page or the page size changed.
| Property | Type | Description |
|---|---|---|
| page | number | The page now showing, zero-based. |
| pageSize | number | Rows per page; 0 means paging is off. |
| total | number | null | How many rows the current query produces, or null while the source is still counting. |
| pageCount | number | How many pages that makes. |
| counting | boolean | Whether a deferred exact total is still being counted. (optional) |
PermissionsChangedEvent
`permissions:changed`: the per-column permission levels changed.
| Property | Type | Description |
|---|---|---|
| levels | Record<string, PermissionLevel> | The level now in force for each column that has one. |
PivotDrillEvent
`pivot:drill`: a pivot measure cell was drilled into.
| Property | Type | Description |
|---|---|---|
| keys | string[] | The keys of the source rows behind the measure. |
| rowPath | string | null | The row path of the cell, as the header wrote it. |
| colPath | string | null | The column path of the cell. |
| measure | string | null | Which measure the cell shows. |
| event | unknown | The DOM event behind the drill. |
PresenceFailedEvent
`presence:failed`: a presence subscribe or publish could not reach the transport.
| Property | Type | Description |
|---|---|---|
| operation | PresenceOperation 'subscribe' | 'publish' | Which call failed. |
| error | unknown | What the transport threw or rejected with. |
PresenceLeftEvent
`presence:left`: a peer left the presence channel or timed out.
| Property | Type | Description |
|---|---|---|
| peer | Peer | The peer that left. |
| reason | string | Why it left - the transport's reason, or the grid's own timeout. |
PresenceLockRefusedEvent
`presence:lockRefused`: an edit was refused because a peer holds the cell's lock.
| Property | Type | Description |
|---|---|---|
| key | string | The row key of the locked cell. |
| colId | string | Its column. |
| peer | Peer | The peer holding the lock. |
PresencePeerEvent
`presence:joined` and `presence:updated`: a peer appeared, or one already present moved.
| Property | Type | Description |
|---|---|---|
| peer | Peer | The peer, as the grid now holds it. |
PresencePublishedEvent
`presence:published`: this grid published its own presence to the transport.
| Property | Type | Description |
|---|---|---|
| state | Record<string, unknown> | What was published: this peer's cursor, selection and identity. |
PresentationCapturedEvent
`presentation:captured`: a screenshot of the grid was taken.
| Property | Type | Description |
|---|---|---|
| width | number | The image's width in pixels. |
| height | number | Its height in pixels. |
| bytes | number | Its size in bytes. |
| mimeType | string | Its MIME type. |
| fileName | string | null | The file name it was downloaded under, or null when it was not downloaded. |
PresentationChangedEvent
| Property | Type | Description |
|---|---|---|
| presentation | ViewPresentation 'cards' | 'table' | `'cards'` or `'table'`, on the responsive-layout firing. (optional) |
| scale | number | The enlargement now in force, on the presentation-model firing. (optional) |
| options | Record<string, unknown> | The options the presentation is running with. (optional) |
| views | string[] | The view ids in the deck. (optional) |
| index | number | Which of them is showing, or -1 when the deck is empty. (optional) |
PresentationScaleEvent
`presentation:scale`: the presentation's enlargement changed.
| Property | Type | Description |
|---|---|---|
| scale | number | The enlargement now in force, already clamped to the allowed range. |
PresentationSpotlightEvent
`presentation:spotlight`: the spotlight was armed over some rows and columns, or cleared.
| Property | Type | Description |
|---|---|---|
| spotlight | { keys: string[]; colIds: string[] } | null | What is lit, or null when the spotlight was cleared. |
PresentationStartedEvent
`presentation:started`: `grid.presentation.start()` began presenting.
| Property | Type | Description |
|---|---|---|
| scale | number | The enlargement it started at. |
| options | Record<string, unknown> | The options it was started with. |
| views | string[] | The view ids in the deck; empty when it is presenting the grid as it stands. |
| index | number | Which view is showing, or -1 when there is no deck. |
PresentationViewEvent
`presentation:view`: the presentation stepped to a view, including the first.
| Property | Type | Description |
|---|---|---|
| viewId | string | null | The view now showing, or null when the deck is empty. |
| index | number | Its position in the deck. |
| count | number | How many views the deck holds. |
PrintEvent
`print:before` and `print:after`: print mode was applied, and undone.
| Property | Type | Description |
|---|---|---|
| rows | number | How many rows the print covers. |
RangeChangedEvent
`range:changed`: the selected cell ranges changed.
| Property | Type | Description |
|---|---|---|
| ranges | CellRange[] | Every range now selected. |
RedactionChangedEvent
`redaction:changed`: the set of redacted columns changed.
| Property | Type | Description |
|---|---|---|
| columns | string[] | Every column id now redacted. |
RenderDoneEvent
`render:done`: one render pass has finished writing cells. The pass is over and the cells are stable, which is why anything that decorates them from outside - a highlight painter, a diff painter - hangs off this rather than guessing at a frame delay.
| Property | Type | Description |
|---|---|---|
| first | number | The first display index the pass drew, including the overscan either side. |
| last | number | The last display index the pass drew, inclusive; `-1` when there were no rows. |
| cause | string | What asked for the pass - `'scroll'` unless something else invalidated first. |
| phases | { layoutMs: number; hintMs: number; writeMs: number; totalMs: number } | Milliseconds per phase. `layoutMs` is deciding what to draw, `hintMs` is telling the source about it, `writeMs` is the DOM itself. The wait for paint is the browser's and is not measurable here. |
RowAddCancelledEvent
`rowAdd:cancelled`: a `beforeRowAdd` handler vetoed the append.
| Property | Type | Description |
|---|---|---|
| row | Record<string, unknown> | The record that was not appended. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
RowConfirmedEvent
`row:confirmed`: the server accepted a pending row append or delete.
| Property | Type | Description |
|---|---|---|
| id | string | The id the op was tracked under. |
| kind | RowChangeKind 'append' | 'delete' | Which structural write it was. |
| key | string | The row key, already rekeyed from the temporary one on an append. |
| tempKey | string | The temporary key an append was rekeyed from. (optional) |
| row | Row | The row as it now stands; undefined for a confirmed delete. (optional) |
| superseded | boolean | True when a newer op on the same key had already replaced this one. |
RowConflictEvent
`row:conflict`: the server confirmed a structural write but sent back a row that disagrees.
| Property | Type | Description |
|---|---|---|
| id | string | The id the op was tracked under. |
| kind | RowChangeKind 'append' | 'delete' | Which structural write it was. |
| key | string | The row key. |
| serverRow | Record<string, unknown> | The row the server sent back. |
| row | Row | The row as the grid holds it; undefined for a delete. (optional) |
RowDragEvent
The row-drag lifecycle events: `rowDrag:started`, `rowDrag:moved`, `rowDrag:left` and `rowDrag:ended`, which report a row drag *as it happens* rather than once it has settled. Before them a host got the handle the grid draws and then one settled event, with nothing in between to highlight a candidate target, drive a custom drop indicator, or react when the pointer left the grid. **All four fire on the grid the drag started in**, whether the row is being reordered within that grid or dragged into another one. A drag is one gesture with one owner, and the source grid is the only grid present for the whole of it - the pointer may cross several others, or none. `over` names whichever grid the event is about, so a single subscription can drive decoration on any of them. **Notifications, not gates.** None of these is cancellable and none carries `preventDefault`. The drop is already vetoable twice over - `beforeRowMove` for a reorder, `beforeRowReceive` for a drop into another grid - and a third veto on the same gesture would be a third place to look when a drop does not happen. **What is safe to do in a handler.** Read, measure and draw: highlight a candidate row, move an indicator, update a side panel. Do not mutate rows, columns, sort, filters or grouping from one of these. The drag resolves where it would land against the display order, so changing that order mid-gesture moves the ground under the drop; and `data` is the source row's own object rather than a copy, so writing to it edits the row that is still in the grid without announcing it. Work that changes the grid belongs in `beforeRowReceive`, which is asked before the insert, or in the settled events afterwards. **`rowDrag:moved` is coalesced to one event per animation frame**, carrying the latest pointer position of that frame, so a handler runs at the display's rate rather than the pointer's several hundred events a second. The other three fire on the transition itself. The sequence for any gesture is `rowDrag:started`, then `rowDrag:moved` and `rowDrag:left` as the pointer travels, then exactly one `rowDrag:ended` - including when the pointer is released outside every grid. No `rowDrag:moved` is delivered after `rowDrag:ended`. A press that never passes the drag threshold is a click and raises none of them; a grid destroyed mid-drag raises no `rowDrag:ended`.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row being dragged. |
| data | Record<string, unknown> | null | The dragged row's data as it stands in the source grid - that row's own object, not a copy. Null if the row has left the source during the drag. |
| over | Grid | null | The grid the event is about: the grid under the pointer for `rowDrag:started`, `rowDrag:moved` and `rowDrag:ended`, and the grid just left for `rowDrag:left`. Null when the pointer is over no grid at all. |
| at | number | null | Where the row would land in `over`: the display index it would take. Null when there is no candidate to report - the pointer is over no grid, over a grid that will refuse the row, or over a header; and on `rowDrag:left`, which is about a grid the pointer has already gone from. |
| overKey | string | null | The key of the row under the pointer in `over`, or null where there is no row to name: past the last row, on empty space, on a header, on a pinned row, on a grid that will refuse the drop, or on `rowDrag:left`. |
| dropped | boolean | `rowDrag:ended` only: whether the release is being acted on - a transfer the target accepts, or a same-grid reorder that is a real move and is not refused by a sort, filter or grouping. False when the row was released over no grid, over a grid that refuses it, or back where it started. What became of an acted-on drop is reported by `row:moved`, `row:sent`, `row:received` and `rowReceive:cancelled`. (optional) |
RowMoveCancelledEvent
`rowMove:cancelled`: a `beforeRowMove` handler vetoed the reorder.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row that did not move. |
| from | number | The display index it is still at. |
| to | number | The display index it would have taken. |
| reason | string | The reason given to `preventDefault`, `'prevented'`, or `'unchanged'` when the move was a no-op. |
RowMovedEvent
`row:moved`: a row was reordered within this grid.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row that moved. |
| from | number | The display index it came from. |
| to | number | The display index it went to. |
| data | Record<string, unknown> | That row's data. |
RowPendingEvent
`row:pending`: an optimistic row append or delete was sent to the transport.
| Property | Type | Description |
|---|---|---|
| id | string | The id the op is tracked under; `grid.edit.settleRow(id, …)` answers it. |
| kind | RowChangeKind 'append' | 'delete' | Which structural write it is. |
| key | string | The row key - a temporary one for an append until the server rekeys it. |
| temp | boolean | True while the key is the grid's own temporary one. |
| row | Row | The row as it stands in the grid, or undefined when there is none. (optional) |
RowPointerEvent
`row:clicked` and `row:dblclicked`: a row was clicked or double-clicked.
| Property | Type | Description |
|---|---|---|
| row | Row | The row under the pointer. |
| key | string | That row's key. |
| index | number | Its display index. |
| event | unknown | The DOM event behind this one. |
RowReceiveCancelledEvent
The `rowReceive:cancelled` event: a `beforeRowReceive` was vetoed, or went stale during an async handler. Nothing was inserted and the source grid is untouched.
| Property | Type | Description |
|---|---|---|
| data | Record<string, unknown> | The row that was not inserted, as the handler saw it. |
| at | number | The display index it would have taken. |
| overKey | string | null | The key of the row under the pointer, or null. |
| source | Grid | The grid the row would have come from; it still holds the row. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was given, or `'stale'` when the row under the pointer or the source row was gone by the time an async handler settled. |
RowReceivedEvent
`row:received`: a row dragged from another grid was inserted here.
| Property | Type | Description |
|---|---|---|
| data | Record<string, unknown> | The row's data, as it was inserted. |
| at | number | The display index it took. |
| overKey | string | null | The key of the row it was dropped on, or null when it landed on no row. |
| rejected | RejectedRow[] | Rows the insert could not apply; empty on a clean insert. |
RowRevertedEvent
`row:reverted`: a pending row append or delete was refused and rolled back.
| Property | Type | Description |
|---|---|---|
| id | string | The id the op was tracked under. |
| kind | RowChangeKind 'append' | 'delete' | Which structural write it was. |
| key | string | The row key. |
| tempKey | string | The temporary key the append had been given. (optional) |
| reason | string | null | Why it was refused, or null when the transport gave no reason. |
| superseded | boolean | True when a newer op on the same key had already replaced this one. |
| applied | boolean | True when the rollback was applied; false when it was superseded. |
| row | Row | The row as it stands after the rollback, where there is one. (optional) |
RowTransferEvent
`row:sent` and `row:copied`: a row left this grid for another one. `row:sent` means it was removed from here, `row:copied` means it was kept.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row that was transferred. |
| data | Record<string, unknown> | That row's data, as the target received it. |
| mode | RowTransferMode 'move' | 'copy' | Which gesture it was. |
RowsChangedEvent
`rows:changed`: rows were added, updated, removed or moved. **Read `identified` first.** When it is `true` the three arrays name exactly the rows that moved and a derived viewer can patch rather than rescan. Every other firing omits it, and a consumer that does not see it re-reads in full () - the default is deliberately the safe one. The `companion: true` firings carry **counts** in `added`/`updated`/ `removed`, not rows: they come from the source's own companion channel, which has the numbers and not the records. Anything reading `.length` has to check `identified` rather than assume an array (F-1688-B).
| Property | Type | Description |
|---|---|---|
| identified | boolean | True when `added`, `updated` and `removed` name exactly the rows that moved. (optional) |
| added | Row[] | number | The rows added - records when `identified`, a count on a companion firing. (optional) |
| updated | Row[] | number | The rows updated - records when `identified`, a count on a companion firing. (optional) |
| removed | string[] | number | The keys removed - keys when `identified`, a count on a companion firing. (optional) |
| rejected | RejectedRow[] | Rows the host could not apply; the rest of the batch still applied. (optional) |
| plan | unknown | How the change was planned and applied, for diagnostics. (optional) |
| companion | boolean | True on the firings that echo a change the source has already applied. (optional) |
| change | RowChange | The change as it was handed in, on a companion firing that carries one. (optional) |
| reason | string | `'import'` on a CSV/Excel import; `'edit'`-side reasons name the write. (optional) |
| edit | boolean | True when the change came from an edit commit rather than a data feed. (optional) |
| columns | string[] | The column ids an edit wrote to. (optional) |
| moved | number | `1` when the change was a single row reorder. (optional) |
| key | string | The key of the row that moved. (optional) |
| from | number | The display index it moved from. (optional) |
| to | number | The display index it moved to. (optional) |
RowsDeferredEvent
`rows:deferred`: a flush ran out of frame budget and carried work over.
| Property | Type | Description |
|---|---|---|
| deferred | number | How many rows were carried into the next frame. |
| applied | number | How many rows this flush did apply. |
| budgetMs | number | The per-flush budget, in milliseconds, that ran out. |
RowsFlowEvent
`rows:paused` and `rows:resumed`: the whole counter set the feed keeps, which is what a host watching a live feed wants at the moment it stops or starts. Identical to what `grid.changes.stats()` returns.
| Property | Type | Description |
|---|---|---|
| paused | boolean | Whether the feed is held. |
| pending | number | Changes waiting to be applied. |
| queued | number | Rows those changes carry. |
| coalesced | number | Rows coalescing saved on the current queue. |
| coalescedTotal | number | Rows coalescing has saved over the grid's life. |
| rows | number | Rows that have arrived over the grid's life. |
| dropped | number | Rows dropped because the buffer was full. |
| held | number | Rows the change log is holding right now. |
| heldLimit | number | The most it will hold before trimming. |
| flushes | number | How many flushes have run. |
| strategy | string | The batching strategy in force. |
| deferrals | number | Flushes that ran out of budget and carried work over; a rising number means the feed outpaces the grid. |
| maxQueued | number | The largest queue seen. |
| budgetMs | number | The per-flush budget, in milliseconds. |
| span | { from: number; to: number } | null | The time span the held log covers, or null when it holds nothing. |
RowsQueuedEvent
`rows:queued`: a change arrived while the feed was batching and was queued.
| Property | Type | Description |
|---|---|---|
| pending | number | How many changes are waiting to be applied. |
| queued | number | How many rows those changes carry. |
| coalesced | number | How many rows coalescing has saved on this queue. |
| paused | boolean | Whether the feed is currently paused. |
ScrollEvent
`scroll` and `scroll:end`: the viewport's offset. `scroll` fires only when the offset actually moved, so a refresh is never mistaken for a scroll; `scroll:end` fires once the gesture has settled.
| Property | Type | Description |
|---|---|---|
| top | number | The vertical offset, in content space rather than spacer space, so it survives a row-count change. |
| left | number | The logical horizontal offset: zero at the content's start in either writing direction. |
SelectionCancelledEvent
`selection:cancelled`: a `beforeSelect` handler vetoed the change, which has been snapped back.
| Property | Type | Description |
|---|---|---|
| keys | string[] | The keys the user had selected, which are no longer selected. |
| previous | string[] | The keys the selection was snapped back to. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
SelectionChangedEvent
`selection:changed`: the row selection changed and was accepted.
| Property | Type | Description |
|---|---|---|
| keys | string[] | The keys of every selected row. |
| rows | Row[] | Those rows. |
SortCancelledEvent
`sort:cancelled`: a `beforeSort` handler vetoed the sort.
| Property | Type | Description |
|---|---|---|
| sort | SortEntry[] | The sort that was not applied. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
SortChangedEvent
`sort:changed`: the sort order changed.
| Property | Type | Description |
|---|---|---|
| sort | SortEntry[] | The sort now in force, in precedence order; empty when nothing is sorted. |
SourceErrorEvent
`source:error`: a source could not fetch what was asked of it. Which of the optional fields is present says what was being fetched.
| Property | Type | Description |
|---|---|---|
| error | unknown | What the source threw or rejected with. |
| row | Row | The branch row whose children could not be loaded. (optional) |
| reason | string | `'loadChildren'` on a tree fetch; absent on a page or stream failure. (optional) |
| block | string | number | The block id that failed, on a paged or remote source. (optional) |
| range | { start: number; end: number } | The display range that block covers. (optional) |
| groupPath | unknown[] | The group path the failed block sits under, on a remote grouped source. (optional) |
SourceTotalEvent
`source:total`: a deferred exact total landed. A pushdown source whose count has to read data - a filtered query against a remote Parquet file, where counting costs a second and a page costs a tenth of one - delivers the rows as soon as the page settles and counts afterwards. This is the count arriving: the number is exact, it fires once per query, and until it does `grid.rows.totalCount()` is `null` and `grid.rows.totalPending()` is `true`. A query whose total arrives with its rows never fires it.
| Property | Type | Description |
|---|---|---|
| total | number | The exact number of rows the query matches. Never an estimate. |
| groupPath | unknown[] | The group path the total counts, empty at the root of an ungrouped grid. (optional) |
StateChangedEvent
The `state:changed` event. Fires once per logical state change, whether it began as a user gesture or as a programmatic call, so view persistence is built on this one event rather than on the ten individual ones - `reset()` raises those too, which made a debounced save write the reset arrangement back. **Exactly one event per change.** A change that internally routes through `state.apply()` - applying a saved view, an undo, a reset - announces itself once, carrying the outermost cause rather than the inner mechanism's. A host predicate registered, replaced or removed through `filters.where(name, fn)`, and a `filters.reapply()` that re-runs one, go through the same tracked door as `sort` and `filters`: each fires this event once, `cause: 'user'`, with `'where'` in `sections`.
| Property | Type | Description |
|---|---|---|
| cause | StateChangeCause 'user' | 'apply' | 'reset' | Why the state changed. `'reset'` is the one a save should ignore. |
| sections | StateSection[] | Which sections moved, sorted and de-duplicated. For `'apply'` and `'reset'` these are the sections the report applied; for `'user'`, the sections the change touches. |
| state | GridState | null | The state that was applied - present for `'apply'` and `'reset'`, null for `'user'`. A full capture on every gesture would put an unsanitised copy of the state, hidden column ids and widths included, on the bus for every listener; a host calls `grid.state.get()` when it decides to write, which is permission-sanitised. |
| report | StateApplyReport | null | What an apply could not restore; null for `'user'`. |
StateResetEvent
`state:reset`: `grid.state.reset()` restored the arrangement the grid was built with.
| Property | Type | Description |
|---|---|---|
| state | GridState | The baseline that was restored. |
StreamChunkEvent
`stream:chunk`: a streaming source applied a chunk of arriving rows.
| Property | Type | Description |
|---|---|---|
| loaded | number | Bytes or rows read so far, as the transport reports them. |
| estimated | number | What the transport expects in total, or 0 when it does not say. |
| count | number | How many rows the source now holds. |
| renders | number | How many times the grid has been asked to repaint for this stream. |
StreamEndEvent
`stream:end`: a streaming source reached the end of its feed.
| Property | Type | Description |
|---|---|---|
| loaded | number | How many rows arrived in all. |
| promoted | boolean | True when the stream handed over to an in-memory source at the end. |
| threshold | number | The row count above which it would have promoted. |
StreamEvictedEvent
`stream:evicted`: a rolling-window stream dropped rows off the back.
| Property | Type | Description |
|---|---|---|
| evicted | number | How many rows this eviction dropped. |
| total | number | How many rows have been evicted over the stream's life. |
| live | number | How many rows are still live in the window. |
TimelineAttachedEvent
`timeline:attached`: the scrubber began recording what each change replaces.
| Property | Type | Description |
|---|---|---|
| depth | number | How many steps back it is currently possible to go. |
TimelineSeekEvent
`timeline:seek`: the timeline finished moving.
| Property | Type | Description |
|---|---|---|
| position | number | How many steps back from the present the grid now stands; 0 is live. |
| depth | number | How many steps back it is possible to go. |
| live | boolean | Whether it is standing in the present. |
| at | number | null | The timestamp of the recorded state it is standing at, or null when live. |
TimelineSeekingEvent
`timeline:seeking`: the timeline is about to move.
| Property | Type | Description |
|---|---|---|
| from | number | How many steps back it is coming from. |
| to | number | How many steps back it is going to. |
TreeLoadAbortedEvent
`tree:loadAborted`: a branch was collapsed before its children arrived, so the fetch was abandoned.
| Property | Type | Description |
|---|---|---|
| key | string | The branch's row key. |
TreeLoadFailedEvent
`tree:loadFailed`: a branch's `loadChildren` rejected; the branch stays unloaded so it can be retried.
| Property | Type | Description |
|---|---|---|
| key | string | The branch's row key. |
| error | unknown | What the loader rejected with. |
TreeLoadedEvent
`tree:loaded`: a branch's children arrived and were added.
| Property | Type | Description |
|---|---|---|
| key | string | The branch's row key. |
| count | number | How many children arrived. |
TreeLoadingEvent
`tree:loading`: a branch was expanded and `tree.loadChildren` was called for it.
| Property | Type | Description |
|---|---|---|
| key | string | The branch's row key. |
| row | Row | That branch row. |
ValidationClearedEvent
`validation:cleared`: recorded validation errors were cleared.
| Property | Type | Description |
|---|---|---|
| key | string | null | The row that was cleared, or null when every row was. |
| colId | string | null | The column that was cleared, or null when every column was. |
ValidationFailedEvent
`validation:failed`: a declared column rule refused an edit.
| Property | Type | Description |
|---|---|---|
| key | string | The key of the row whose commit was refused. |
| failures | ValidationError[] | One entry per failing cell, with its column, code and message. |
| count | number | How many cells failed. |
ViewAppliedEvent
`view:applied`: a saved view was applied to the grid.
| Property | Type | Description |
|---|---|---|
| view | SavedView | The view that was applied. |
| views | SavedView[] | Every view, unchanged by the apply. |
| activeId | string | The id of the view now active. |
ViewChangedEvent
`view:saved`, `view:removed`, `view:renamed` and `view:default`: the one view that moved, so a host can POST that record instead of diffing two full lists to work out what the user just did.
| Property | Type | Description |
|---|---|---|
| view | SavedView | null | The view that moved. |
| views | SavedView[] | Every view after the change. |
| reason | string | The underlying reason: `'save'`, `'update'`, `'import'`, `'remove'`, `'rename'` or `'default'`. |
ViewsChangedEvent
`views:changed`: the saved-view list changed, for any reason. Paired with a named `view:*` event that carries the one view that moved: this one is what a picker or a `localStorage` mirror wants, the named one is what a host persisting to a server wants.
| Property | Type | Description |
|---|---|---|
| views | SavedView[] | Every view after the change. |
| reason | string | What happened: `'save'`, `'update'`, `'import'`, `'remove'`, `'rename'`, `'default'`, `'seed'`, `'replace'` or `'apply'`. |
| view | SavedView | null | The view that moved, or null when the change was not about one view. |
| activeId | string | The view now applied, on the `'apply'` firing. (optional) |