@toclocoinc/lattice-grid 1.54.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/docs/API.html +360 -62
- package/docs/api-detail.html +348 -15
- package/lattice-grid.d.ts +287 -27
- package/lattice-grid.esm.min.js +1610 -622
- package/lattice-grid.min.cjs +1610 -622
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +1610 -622
- package/modules/ai.esm.min.js +19 -4
- package/modules/ai.min.cjs +19 -4
- package/modules/ai.min.js +19 -4
- package/modules/angular.esm.min.js +2 -2
- package/modules/angular.min.cjs +2 -2
- package/modules/angular.min.js +2 -2
- package/modules/chart-alluvial.esm.min.js +1 -1
- package/modules/chart-arc.esm.min.js +1 -1
- package/modules/chart-bubblemap.esm.min.js +1 -1
- package/modules/chart-bump.esm.min.js +1 -1
- package/modules/chart-calendar.esm.min.js +1 -1
- package/modules/chart-decomposition.esm.min.js +1 -1
- package/modules/chart-diverging.esm.min.js +1 -1
- package/modules/chart-dumbbell.esm.min.js +1 -1
- package/modules/chart-fan.esm.min.js +1 -1
- package/modules/chart-hexbin.esm.min.js +1 -1
- package/modules/chart-hexmap.esm.min.js +1 -1
- package/modules/chart-icicle.esm.min.js +1 -1
- package/modules/chart-parallel.esm.min.js +1 -1
- package/modules/chart-ridgeline.esm.min.js +1 -1
- package/modules/chart-roc.esm.min.js +1 -1
- package/modules/chart-slope.esm.min.js +1 -1
- package/modules/chart-splom.esm.min.js +1 -1
- package/modules/chart-waffle.esm.min.js +1 -1
- package/modules/charts.esm.min.js +115 -29
- package/modules/charts.min.cjs +115 -29
- package/modules/charts.min.js +115 -29
- package/modules/data-router.esm.min.js +37 -4
- package/modules/data-router.min.cjs +37 -4
- package/modules/data-router.min.js +37 -4
- package/modules/devtools.esm.min.js +2 -2
- package/modules/devtools.min.cjs +2 -2
- package/modules/devtools.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +4 -4
- package/modules/dhtmlx-compat.min.cjs +4 -4
- package/modules/dhtmlx-compat.min.js +4 -4
- package/modules/gantt.esm.min.js +32 -6
- package/modules/gantt.min.cjs +32 -6
- package/modules/gantt.min.js +32 -6
- package/modules/htmx.esm.min.js +1610 -622
- package/modules/htmx.min.cjs +1610 -622
- package/modules/htmx.min.js +1610 -622
- package/modules/kanban.esm.min.js +49 -9
- package/modules/kanban.min.cjs +49 -9
- package/modules/kanban.min.js +49 -9
- package/modules/kpi.esm.min.js +4141 -13
- package/modules/kpi.min.cjs +4141 -13
- package/modules/kpi.min.js +4141 -13
- package/modules/layout.esm.min.js +12 -8
- package/modules/layout.min.cjs +12 -8
- package/modules/layout.min.js +12 -8
- package/modules/mock-socket.esm.min.js +2 -2
- package/modules/mock-socket.min.cjs +2 -2
- package/modules/mock-socket.min.js +2 -2
- package/modules/react.esm.min.js +2 -2
- package/modules/react.min.cjs +2 -2
- package/modules/react.min.js +2 -2
- package/modules/svelte.esm.min.js +2 -2
- package/modules/svelte.min.cjs +2 -2
- package/modules/svelte.min.js +2 -2
- package/modules/tabs.esm.min.js +4 -4
- package/modules/tabs.min.cjs +4 -4
- package/modules/tabs.min.js +4 -4
- package/modules/vue.esm.min.js +2 -2
- package/modules/vue.min.cjs +2 -2
- package/modules/vue.min.js +2 -2
- package/modules/webcomponent.esm.min.js +1610 -622
- package/modules/webcomponent.min.cjs +1610 -622
- package/modules/webcomponent.min.js +1610 -622
- package/package.json +3 -2
package/docs/API.html
CHANGED
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
<div class="shell">
|
|
361
361
|
<aside class="rail">
|
|
362
362
|
<p class="rail__brand">Lattice Grid</p>
|
|
363
|
-
<p class="rail__sub">API reference · v1.
|
|
363
|
+
<p class="rail__sub">API reference · v1.56.0</p>
|
|
364
364
|
<nav>
|
|
365
365
|
<div class="rail__group">
|
|
366
366
|
<span class="rail__label">Start</span>
|
|
@@ -443,7 +443,7 @@
|
|
|
443
443
|
</header>
|
|
444
444
|
|
|
445
445
|
<p class="chips">
|
|
446
|
-
<span class="chip">Version 1.
|
|
446
|
+
<span class="chip">Version 1.56.0</span>
|
|
447
447
|
<span class="chip">Zero dependencies</span>
|
|
448
448
|
<span class="chip"><a href="api-detail.html">Developer guide →</a></span>
|
|
449
449
|
</p>
|
|
@@ -461,8 +461,8 @@
|
|
|
461
461
|
|
|
462
462
|
<p>Or straight from jsDelivr, no npm install, no bundler, no local copy at all:</p>
|
|
463
463
|
|
|
464
|
-
<pre><code><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1
|
|
465
|
-
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1
|
|
464
|
+
<pre><code><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/lattice-grid.min.css">
|
|
465
|
+
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/lattice-grid.min.js"></script>
|
|
466
466
|
|
|
467
467
|
<script>
|
|
468
468
|
<span class="kw">const</span> grid = LatticeGrid.createGrid(document.getElementById('grid'), config);
|
|
@@ -473,7 +473,7 @@
|
|
|
473
473
|
<pre><code><span class="cmt">// With a renderer, in a browser. Any of:</span>
|
|
474
474
|
<span class="kw">import</span> { createGrid } <span class="kw">from</span> './dist/lattice-grid.esm.js';
|
|
475
475
|
<span class="cmt">// import { createGrid } from '@toclocoinc/lattice-grid';</span>
|
|
476
|
-
<span class="cmt">// import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1
|
|
476
|
+
<span class="cmt">// import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/lattice-grid.esm.min.js';</span>
|
|
477
477
|
<span class="kw">const</span> grid = createGrid(document.getElementById('grid'), config);
|
|
478
478
|
|
|
479
479
|
<span class="cmt">// Headless: the same API without a renderer. Data, filters, sort,</span>
|
|
@@ -484,9 +484,10 @@
|
|
|
484
484
|
<span class="kw">const</span> grid = createHeadlessGrid(config);</code></pre>
|
|
485
485
|
|
|
486
486
|
<div class="note"><p>jsDelivr mirrors every version published to npm at
|
|
487
|
-
<code>cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid
|
|
488
|
-
|
|
489
|
-
|
|
487
|
+
<code>cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/<file></code>. <code>@1</code> pins the
|
|
488
|
+
major: a page in production picks up fixes within 1.x and never a breaking release, where
|
|
489
|
+
<code>@latest</code> would. To freeze a page on one exact build, replace <code>@1</code> with the
|
|
490
|
+
full version <code>getVersion()</code> reports. The same convention reaches any module:
|
|
490
491
|
<code>.../modules/htmx.esm.min.js</code>, <code>.../modules/react.esm.min.js</code>, and so on.</p></div>
|
|
491
492
|
|
|
492
493
|
<h2 id="adapters">Framework adapters</h2>
|
|
@@ -570,17 +571,61 @@
|
|
|
570
571
|
<tr>
|
|
571
572
|
<td class="sig">createGrid(element, config?)</td>
|
|
572
573
|
<td class="type">Grid</td>
|
|
573
|
-
<td class="desc">Resolves the document from <code>element.ownerDocument</code>, so a grid inside an iframe uses that frame's document. Throws with a clear message if there is no DOM.</td>
|
|
574
|
+
<td class="desc">Resolves the document from <code>element.ownerDocument</code>, so a grid inside an iframe uses that frame's document. Throws with a clear message if there is no DOM. The exported name itself cannot be reassigned to wrap it — see the note below.</td>
|
|
574
575
|
</tr>
|
|
575
576
|
<tr>
|
|
576
577
|
<td class="sig">createHeadlessGrid(config?)</td>
|
|
577
578
|
<td class="type">Grid</td>
|
|
578
|
-
<td class="desc">Core only. Everything below except <code>grid.element</code> and the DOM-only config keys works unchanged.</td>
|
|
579
|
+
<td class="desc">Core only. Everything below except <code>grid.element</code> and the DOM-only config keys works unchanged. What that does and doesn't reach is spelled out below.</td>
|
|
579
580
|
</tr>
|
|
580
581
|
</tbody>
|
|
581
582
|
</table>
|
|
582
583
|
</div>
|
|
583
584
|
|
|
585
|
+
<div class="note" id="wrapping-creategrid">
|
|
586
|
+
<p><strong><code>createGrid</code> cannot be monkey-patched.</strong> Wherever it is exported —
|
|
587
|
+
<code>window.LatticeGrid.createGrid</code> from the UMD build, or the named import from the ESM
|
|
588
|
+
build — it is defined with <code>Object.defineProperty(..., { get, enumerable: true })</code>
|
|
589
|
+
and no setter, and <code>configurable</code> defaults to <code>false</code> because the
|
|
590
|
+
descriptor never sets it. Assigning to it in an ordinary (non-strict) script is not an error:
|
|
591
|
+
the assignment is simply discarded and <code>LatticeGrid.createGrid</code> still returns the
|
|
592
|
+
original function. In a module or any script under <code>'use strict'</code> — which every ES
|
|
593
|
+
module is — the same assignment throws <code>TypeError: Cannot set property createGrid of
|
|
594
|
+
[object Object] which has only a getter</code>. Either way, a house-wide patch applied this way
|
|
595
|
+
has no effect, and in the sloppy-mode case nothing tells you it didn't. There is no supported
|
|
596
|
+
way to replace the function in place. The supported pattern is a factory your own code owns:</p>
|
|
597
|
+
<pre><code><span class="cmt">// your-lattice.js — the one place that knows your house defaults</span>
|
|
598
|
+
<span class="kw">import</span> { createGrid <span class="kw">as</span> baseCreateGrid } <span class="kw">from</span> '@toclocoinc/lattice-grid';
|
|
599
|
+
|
|
600
|
+
<span class="kw">export function</span> createGrid(element, config) {
|
|
601
|
+
<span class="kw">return</span> baseCreateGrid(element, { theme: 'house', density: 'compact', ...config });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
<span class="cmt">// everywhere else</span>
|
|
605
|
+
<span class="kw">import</span> { createGrid } <span class="kw">from</span> './your-lattice.js';</code></pre>
|
|
606
|
+
<p>There is no built-in <code>defaults()</code> call that applies house-wide options for you
|
|
607
|
+
today; a separate card (BACKLOG-0001187) considers adding one. Until then, the wrapping
|
|
608
|
+
function above — called everywhere <code>createGrid</code> would otherwise be called directly —
|
|
609
|
+
is the supported seam.</p>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<div class="note" id="headless-coverage">
|
|
613
|
+
<p><strong>What <code>createHeadlessGrid</code> covers, and what it cannot.</strong> It builds
|
|
614
|
+
the same core the DOM build attaches a renderer to, so everything that is not the renderer
|
|
615
|
+
itself is exercised exactly as it runs in a browser:</p>
|
|
616
|
+
<ul>
|
|
617
|
+
<li>Covered: data (<code>rows</code>, <code>columns</code>), state (<code>grid.state</code>,
|
|
618
|
+
saved views), sort, filter, group, total and pivot, formulas and computed columns, editing
|
|
619
|
+
and optimistic write-back, export, and every event the grid emits.</li>
|
|
620
|
+
<li>Not covered: the DOM renderer, layout and measurement (column widths, row heights,
|
|
621
|
+
scrolling), focus, and anything whose behaviour depends on a real box being painted on
|
|
622
|
+
screen — <code>grid.element</code> is <code>null</code> and there is nothing to measure.</li>
|
|
623
|
+
</ul>
|
|
624
|
+
<p>See <a href="api-detail.html#concepts">How it works</a> in the guide for the two specifics
|
|
625
|
+
that have cost real debugging time: a grid mounted where it has no rendered box, and what the
|
|
626
|
+
in-repo test DOM stub does and does not stand in for.</p>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
584
629
|
<h2 id="webcomponent"><lattice-grid> web component</h2>
|
|
585
630
|
<p class="section-note">A self-contained module bundle that registers a custom element on import. One script, one tag, no build step, for Rails, Django, Laravel or any page without a bundler. Load this <em>or</em> <code>lattice-grid.esm.js</code>, not both: the module carries the grid with it.</p>
|
|
586
631
|
|
|
@@ -755,7 +800,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
755
800
|
<tr><td class="name">columns</td><td class="type">(Column | ColumnGroup)[]</td><td class="dflt">, </td><td class="desc">Column definitions. Groups may nest.</td></tr>
|
|
756
801
|
<tr><td class="name">columnGroups</td><td class="type">ColumnGroup[]</td><td class="dflt">, </td><td class="desc">Header grouping declared separately from the columns.</td></tr>
|
|
757
802
|
<tr><td class="name">rows</td><td class="type">unknown[]</td><td class="dflt">, </td><td class="desc">Row objects. Held by reference; not copied.</td></tr>
|
|
758
|
-
<tr><td class="name">rowKey</td><td class="type">string | (row) => string</td><td class="dflt">, </td><td class="desc">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.</td></tr>
|
|
803
|
+
<tr><td class="name">rowKey</td><td class="type">string | (row) => string</td><td class="dflt">, </td><td class="desc">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 <code>rows.load()</code> as well as at construction.</td></tr>
|
|
759
804
|
<tr><td class="name">source</td><td class="type">SourceConfig</td><td class="dflt">memory</td><td class="desc">Where rows come from: <code>memory</code>, <code>paged</code>, <code>remote</code> or <code>stream</code>. See <a href="#sources">Sources</a>.</td></tr>
|
|
760
805
|
<tr><td class="name">ingest</td><td class="type">IngestConfig</td><td class="dflt">, </td><td class="desc"><code>{ retainSource, dropSourceRows }</code>. How rows enter the column store. <code>retainSource</code> defaults to <code>true</code>: the caller's row objects are held by reference so <code>rows.data()</code> returns them unchanged and <code>row === sourceObject</code> holds. Set it <code>false</code> 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. <code>dropSourceRows: true</code> 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 <code>rows.data()</code> then returns freshly reconstructed objects, so identity checks and <code>row.sourceObject</code> no longer hold and equality becomes value-based. Cell values are unchanged.</td></tr>
|
|
761
806
|
<tr><td class="name">tree</td><td class="type">TreeConfig</td><td class="dflt">, </td><td class="desc"><code>{ path }</code> or <code>{ parentKey }</code>, plus <code>label</code>, <code>orphans</code>. Rows form a hierarchy. See <a href="api-detail.html#tree-data">Tree data</a>.</td></tr>
|
|
@@ -788,7 +833,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
788
833
|
<table>
|
|
789
834
|
<thead><tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
790
835
|
<tbody>
|
|
791
|
-
<tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="dflt">, </td><td class="desc">Object form adds <code>checkbox</code> (a pinned column of row checkboxes), <code>headerCheckbox</code> (tri-state select-all in its heading), <code>groupSelectsChildren</code>, <code>ranges</code>, <code>fillHandle</code>, <code>fill</code>. See <a href="api-detail.html#selection-checkbox">Selection and ranges</a>.</td></tr>
|
|
836
|
+
<tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="dflt">, </td><td class="desc">Object form adds <code>checkbox</code> (a pinned column of row checkboxes), <code>headerCheckbox</code> (tri-state select-all in its heading), <code>checkboxOnly</code> (only that column may change selection — for a row with its own click action), <code>groupSelectsChildren</code>, <code>ranges</code>, <code>fillHandle</code>, <code>fill</code>. See <a href="api-detail.html#selection-checkbox">Selection and ranges</a>.</td></tr>
|
|
792
837
|
<tr><td class="name">edit</td><td class="type">EditConfig | boolean</td><td class="dflt">, </td><td class="desc"><code>{ enabled, mode: 'cell' | 'row', start: 'single' | 'double' | 'key', enterMovesDown, undoDepth, commit, confirm, pendingTimeout, pastePreview }</code>. <code>commit</code>/<code>confirm</code>/<code>pendingTimeout</code> turn on optimistic writes; <code>pastePreview</code> (default off) shows a confirm/cancel diff before a bulk paste commits.</td></tr>
|
|
793
838
|
<tr><td class="name">pagination</td><td class="type">PaginationConfig | boolean</td><td class="dflt">, </td><td class="desc">Local or remote paging.</td></tr>
|
|
794
839
|
<tr><td class="name">quickFilterText</td><td class="type">string</td><td class="dflt">, </td><td class="desc">Initial quick-filter term. Equivalent to <code>grid.filters.quick(text)</code>.</td></tr>
|
|
@@ -862,7 +907,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
862
907
|
<tr><td class="name">comments</td><td class="type">object</td><td class="desc">Threaded cell comments: storage, the current author, and whether the indicator shows on an unread thread.</td></tr>
|
|
863
908
|
<tr><td class="name">presence</td><td class="type">object</td><td class="desc">Live cursors, selections and edit locks. Carries intent and never values; see <code>grid.presence</code>.</td></tr>
|
|
864
909
|
<tr><td class="name">environment</td><td class="type">function</td><td class="desc">Extra fields for the diagnostics bundle: build number, tenant, region. Called when a bundle is taken, never on the render path.</td></tr>
|
|
865
|
-
<tr><td class="name">contextMenu</td><td class="type">boolean | (p) => MenuItem[]</td><td class="desc">Right-click menu. The function form is <code>(params, defaults) => items</code>: see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it: what a read-only grid wants, since the default menu offers Paste, Clear and Fill down. A <strong>column</strong> takes its own <code>contextMenu</code> (also accepting a bare <code>MenuItem[]</code
|
|
910
|
+
<tr><td class="name">contextMenu</td><td class="type">boolean | (p) => MenuItem[]</td><td class="desc">Right-click menu. The function form is <code>(params, defaults) => items</code>: see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it: what a read-only grid wants, since the default menu offers Paste, Clear and Fill down. A <strong>column</strong> takes its own <code>contextMenu</code> (also accepting a bare <code>MenuItem[]</code>, which is appended after the grid-level items), which composes onto this one as a chain and outranks it on suppression.</td></tr>
|
|
866
911
|
<tr><td class="name">columnMenu</td><td class="type">boolean | (p) => MenuItem[]</td><td class="desc">The header's 3-dot menu, and a right-click on a column heading. The function form is <code>(params, defaults) => items</code>, with <code>params</code> carrying <code>colId</code>, <code>column</code> and <code>grid</code>: see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it.</td></tr>
|
|
867
912
|
<tr><td class="name">rangeChart</td><td class="type">fn | { onChart } | boolean</td><td class="desc">Off by default. Offers <strong>Chart selection</strong> in the cell menu and binds <kbd>Alt</kbd>+<kbd>F1</kbd> when a selected range has a number to plot. The DOM layer draws no charts, so the handler you give — a function, or <code>{ onChart }</code>, called <code>(grid, range)</code> — is where the page wires in <code>chartRange</code> from <a href="#chart-a-range">the charts module</a>.</td></tr>
|
|
868
913
|
<tr><td class="name">shortcuts</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">The <kbd>?</kbd> keyboard shortcut overlay. <code>false</code> suppresses it, for a host that wants <kbd>?</kbd> for itself. See <a href="api-detail.html#keyboard">Keyboard</a>.</td></tr>
|
|
@@ -904,7 +949,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
904
949
|
|
|
905
950
|
<h2 id="column">Column definition</h2>
|
|
906
951
|
<p class="section-note">Everything is optional. A column with only <code>field</code> infers its type from sampled data and takes every default from there.</p>
|
|
907
|
-
<p><strong>Inferring a <code>Date</code
|
|
952
|
+
<p><strong>Inferring a <code>Date</code>, or an ISO string.</strong> Inference walks
|
|
908
953
|
<code>boolean</code>, <code>number</code>, <code>date</code>, <code>dateString</code>,
|
|
909
954
|
<code>datetime</code>, <code>text</code>, <code>object</code> and takes the first type that
|
|
910
955
|
matches every sampled value. A <code>Date</code> whose <em>local</em> wall clock reads exactly
|
|
@@ -912,17 +957,27 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
912
957
|
a <code>Date</code> carrying any time of day infers as <code>datetime</code> and stores
|
|
913
958
|
<code>YYYY-MM-DDTHH:mm</code> (with seconds when they are non-zero), because a
|
|
914
959
|
<code>date</code> column would discard the clock on ingest and nothing downstream could
|
|
915
|
-
recover it. <strong>
|
|
960
|
+
recover it. <strong>An ISO string follows the same rule:</strong> a date-only string
|
|
961
|
+
(<code>YYYY-MM-DD</code>) infers as <code>date</code>; a string with a time part
|
|
962
|
+
(<code>T</code> plus a time, with or without a zone offset — <code>'2026-09-12T14:30:00Z'</code>)
|
|
963
|
+
infers as <code>datetime</code> and keeps the time, and a column mixing both forms infers
|
|
964
|
+
<code>datetime</code> rather than falling back to <code>text</code>. A timestamp such as
|
|
965
|
+
<code>'2026-09-12T14:30:00Z'</code> therefore keeps its 14:30 on ingest, rather than being
|
|
966
|
+
read as the calendar day <code>'2026-09-12'</code> with nothing to say that a time had been
|
|
967
|
+
dropped. <strong>This is a heuristic with one stated blind spot:</strong> a genuine
|
|
916
968
|
timestamp that lands on exactly local midnight — a nightly batch stamped
|
|
917
|
-
<code>00:00:00.000</code
|
|
969
|
+
<code>00:00:00.000</code>, or a bare <code>'2026-09-12'</code> string that really meant an
|
|
970
|
+
instant — is indistinguishable from a date-only value and is still inferred
|
|
918
971
|
as <code>date</code>, so its time of day is still discarded. Sub-second resolution is never
|
|
919
972
|
retained by <code>datetime</code>. <strong>If you group by such a column, declare it:</strong> an
|
|
920
973
|
undeclared <code>Date</code> column groups by the instant, which is one group per row, where a
|
|
921
974
|
<code>type: 'date'</code> column groups into day buckets. Date filters are unaffected either way
|
|
922
975
|
— they compare on the day and return the same rows. Declare <code>type</code> and none of this applies:
|
|
923
976
|
<code>'date'</code> truncates on purpose, <code>'datetime'</code> keeps a wall clock, and
|
|
924
|
-
<code>'timestamp'</code> keeps the instant to the millisecond.
|
|
925
|
-
|
|
977
|
+
<code>'timestamp'</code> keeps the instant to the millisecond. A column of ISO timestamps that
|
|
978
|
+
must stay a calendar day opts out with an explicit <code>type: 'date'</code> — no warning is
|
|
979
|
+
logged for that column, because the value is preserved (declared, not narrowed) and there is
|
|
980
|
+
nothing to disclose.</p>
|
|
926
981
|
|
|
927
982
|
<div class="table-wrap">
|
|
928
983
|
<table>
|
|
@@ -962,9 +1017,9 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
962
1017
|
<table>
|
|
963
1018
|
<thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
|
|
964
1019
|
<tbody>
|
|
965
|
-
<tr><td class="name">compute</td><td class="type">(deps, ctx) => unknown</td><td class="desc">Derived value. Receives only its declared dependencies
|
|
966
|
-
<tr><td class="name">deps</td><td class="type">string[] | '*'</td><td class="desc">Declared dependencies. Cycles are caught at compile time, not at render.</td></tr>
|
|
967
|
-
<tr><td class="name">pure</td><td class="type">boolean</td><td class="desc">
|
|
1020
|
+
<tr><td class="name">compute</td><td class="type">(deps, ctx) => unknown</td><td class="desc">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 <code>rows.apply({ update })</code> or <code>rows.load()</code>, when the grid a derived grid follows changes, and when the host asks with <code>rows.refresh({ rows, columns, force: true })</code>. An in-place cell edit to one of its <code>deps</code> 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 <code>rows.refresh({ rows, columns: [id], force: true })</code> once it resolves — or declare <code>pure: false</code>.</td></tr>
|
|
1021
|
+
<tr><td class="name">deps</td><td class="type">string[] | '*'</td><td class="desc">Declared dependencies. Cycles are caught at compile time, not at render. An edit to a column outside <code>deps</code> does not re-run a pure compute.</td></tr>
|
|
1022
|
+
<tr><td class="name">pure</td><td class="type">boolean</td><td class="desc">Default <code>true</code>: the result is cached and served until a dependency changes or a refresh forces it. <code>false</code> 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.</td></tr>
|
|
968
1023
|
<tr><td class="name">format</td><td class="type">(p) => string</td><td class="desc">Overrides the type's formatter.</td></tr>
|
|
969
1024
|
<tr><td class="name">parse</td><td class="type">(p) => unknown</td><td class="desc">Editor output to value. Always called, whatever the editor emitted.</td></tr>
|
|
970
1025
|
<tr><td class="name">apply</td><td class="type">(p) => boolean</td><td class="desc">Writes the value back into the row object.</td></tr>
|
|
@@ -989,7 +1044,8 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
989
1044
|
<tr><td class="name">classWhen</td><td class="type">{ [class]: (p) => boolean }</td><td class="desc">A class per predicate, re-evaluated as values change.</td></tr>
|
|
990
1045
|
<tr><td class="name">style / css</td><td class="type">CellStyle | (p) => CellStyle</td><td class="desc">Inline styles, static or computed.</td></tr>
|
|
991
1046
|
<tr><td class="name">tooltip</td><td class="type">string | (p) => string</td><td class="desc"></td></tr>
|
|
992
|
-
<tr><td class="name">align
|
|
1047
|
+
<tr><td class="name">align</td><td class="type">'start' | 'center' | 'end' | 'left' | 'right'</td><td class="desc">Horizontal alignment; also accepted at the top level of the column. <code>start</code>, <code>center</code> and <code>end</code> are <strong>logical</strong>: they follow the writing direction, so an <code>end</code>-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 (<code>direction</code>). <code>left</code> and <code>right</code> are <strong>physical</strong>: they name an edge and keep it in both directions. <code>centre</code> is accepted for <code>center</code>. Omitted, the column takes its data type's default (numbers <code>end</code>, booleans <code>center</code>, text <code>start</code>). The heading follows the cell unless <code>header.align</code> says otherwise.</td></tr>
|
|
1048
|
+
<tr><td class="name">wrap / autoHeight</td><td class="type">, </td><td class="desc">Presentation flags.</td></tr>
|
|
993
1049
|
<tr><td class="name">spanColumns / spanRows</td><td class="type">(p) => number</td><td class="desc">Spanned cells render in their own layer so row recycling cannot clip them.</td></tr>
|
|
994
1050
|
</tbody>
|
|
995
1051
|
</table>
|
|
@@ -1015,7 +1071,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
1015
1071
|
<tr><td class="name">edit</td><td class="desc"><code>enabled</code> (boolean or predicate), <code>editor</code>, <code>props</code>, <code>popup</code>, <code>validate</code></td></tr>
|
|
1016
1072
|
<tr><td class="name">sort</td><td class="desc"><code>enabled</code>, <code>direction</code>, <code>order</code>, <code>nullsFirst</code></td></tr>
|
|
1017
1073
|
<tr><td class="name">filter</td><td class="desc"><code>enabled</code>, <code>type</code>, <code>props</code></td></tr>
|
|
1018
|
-
<tr><td class="name">layout</td><td class="desc"><code>width</code>, <code>min</code>, <code>max</code>, <code>flex</code>, <code>pin</code>, <code>hidden</code>, <code>resizable</code>, <code>movable</code>, <code>lockVisible</code>, <code>lockPosition</code>. A <code>pin</code> of <code>'start'</code> or <code>'end'</code> 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.</td></tr>
|
|
1074
|
+
<tr><td class="name">layout</td><td class="desc"><code>width</code>, <code>min</code>, <code>max</code>, <code>flex</code>, <code>pin</code>, <code>hidden</code>, <code>resizable</code>, <code>movable</code>, <code>lockVisible</code>, <code>lockPosition</code>. <code>width</code> is a pixel number or a percentage string (<code>'25%'</code>): a share of the grid's inner width that <strong>follows the viewport</strong> — after the container changes size the column is re-resolved against the new width, clamped to its <code>min</code>/<code>max</code>, so a <code>'50%'</code> 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 <code>pin</code> of <code>'start'</code> or <code>'end'</code> 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.</td></tr>
|
|
1019
1075
|
<tr><td class="name">header</td><td class="desc"><code>template</code>, <code>render</code>, <code>props</code>, <code>class</code>, <code>tooltip</code>, <code>align</code>. <code>render</code> draws a custom heading and may be a <strong>function</strong> or a <strong>component</strong> (a class with a <code>render</code> method); the two forms are interchangeable and each may either append to the passed heading element itself (returning nothing) or <em>return</em> an <code>Element</code> (attached for you) or a <code>string</code> (used as the heading text). <code>class</code> adds a class to the heading cell; <code>template</code> is not read.</td></tr>
|
|
1020
1076
|
</tbody>
|
|
1021
1077
|
</table>
|
|
@@ -1028,7 +1084,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
1028
1084
|
<table>
|
|
1029
1085
|
<thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1030
1086
|
<tbody>
|
|
1031
|
-
<tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.
|
|
1087
|
+
<tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.56.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
|
|
1032
1088
|
<tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
|
|
1033
1089
|
<tr><td class="sig">set(key, value)</td><td class="type">void</td><td class="desc">Write one key. Every key is live; nothing needs a rebuild.</td></tr>
|
|
1034
1090
|
<tr><td class="sig">setAll(values)</td><td class="type">void</td><td class="desc">Write several in one pass. Emits one <code>config:changed</code> for the batch, not one per key.</td></tr>
|
|
@@ -1078,7 +1134,7 @@ grid.overlay.hide();</code></pre>
|
|
|
1078
1134
|
<tr><td class="sig">forEachExcept(colId, fn)</td><td class="type">void</td><td class="desc">Walks the rows surviving every filter <em>except</em> 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 <a href="#cross-filter">cross-filtering</a> panel avoid narrowing itself out of existence. Needs a memory source; anything else falls back to the filtered rows and warns.</td></tr>
|
|
1079
1135
|
<tr><td class="sig">apply(change)</td><td class="type">object</td><td class="desc">Transactional add / update / remove. Needs <code>rowKey</code>.</td></tr>
|
|
1080
1136
|
<tr><td class="sig">queue(change)</td><td class="type">void</td><td class="desc">Batches a change into the next frame, the high-frequency path.</td></tr>
|
|
1081
|
-
<tr><td class="sig">refresh(opts)</td><td class="type">void</td><td class="desc">
|
|
1137
|
+
<tr><td class="sig">refresh(opts)</td><td class="type">void</td><td class="desc">Re-run computed values and repaint, without re-running sort, filter or grouping. <code>{ rows, columns }</code> 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 <code>rows.text()</code> 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. <code>force: true</code> also recomputes a <em>pure</em> (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 <code>pure: false</code> is never cached, so a plain <code>refresh()</code> is enough to show its new value. Without <code>force</code>, whether a <em>stored</em> (pure) computation is re-run for the named cells depends on the store the grid chose for the row count, and it flips at <code>columnarBelow</code>: below that many rows the named cell is re-run on its next read, at or above it the stored value stands until a <code>force: true</code>. Pass <code>force: true</code> when you want the same answer whatever the row count.</td></tr>
|
|
1082
1138
|
<tr><td class="sig">move(key, to)</td><td class="type">{ moved, from, to, reason? }</td><td class="desc">Move a row to another position in the data. Refuses, naming the reason, while a sort, filter or grouping is active. Emits <code>row:moved</code>; persisting the new order is yours.</td></tr>
|
|
1083
1139
|
<tr><td class="sig">groupHeadings(index)</td><td class="type">Row[]</td><td class="desc">The group rows enclosing a display index, outermost first. Empty when the grid is not grouped. Useful for a breadcrumb of your own.</td></tr>
|
|
1084
1140
|
<tr><td class="sig">expand(key, deep?)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
@@ -1101,7 +1157,7 @@ grid.overlay.hide();</code></pre>
|
|
|
1101
1157
|
<tr><td class="sig">pin(id, side)</td><td class="type">void</td><td class="desc"><code>'start'</code>, <code>'end'</code> or <code>null</code>.</td></tr>
|
|
1102
1158
|
<tr><td class="sig">resize(id, px)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1103
1159
|
<tr><td class="sig">autoSize(ids)</td><td class="type">void</td><td class="desc">Fit each column to its rendered content.</td></tr>
|
|
1104
|
-
<tr><td class="sig">fit()</td><td class="type">void</td><td class="desc">
|
|
1160
|
+
<tr><td class="sig">fit()</td><td class="type">void</td><td class="desc">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 (<code>resizable: false</code>, 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 <code>min</code>/<code>max</code>. 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 <code>flex</code> column included) and does not follow later size changes; call it again after a resize or after late rows bring a scrollbar in.</td></tr>
|
|
1105
1161
|
<tr><td class="sig">group(ids)</td><td class="type">void</td><td class="desc">Set the row-group columns, in order.</td></tr>
|
|
1106
1162
|
<tr><td class="sig">pivot(ids)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1107
1163
|
<tr><td class="sig">totals(ids)</td><td class="type">void</td><td class="desc">Which columns carry an aggregation.</td></tr>
|
|
@@ -2841,10 +2897,12 @@ grid.destroy();
|
|
|
2841
2897
|
<tr><td class="name">profile</td><td class="type">string | string[]</td><td class="desc">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.</td></tr>
|
|
2842
2898
|
<tr><td class="name">orient</td><td class="type">'columns' | 'metrics'</td><td class="desc">With <code>profile</code>, emit one row per statistic instead of one per column, the shape a dashboard tile wants.</td></tr>
|
|
2843
2899
|
<tr><td class="name">crossFilter</td><td class="type">boolean | string</td><td class="desc">Let this grid filter the grid it derives from. <code>true</code> cross-filters through whatever it groups by; a string names a different source column.</td></tr>
|
|
2844
|
-
<tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">When to re-derive. <code>idle</code> by default, coalescing to a frame, because a hundred cell updates in one frame are one derivation. A number debounces by that many milliseconds.</td></tr>
|
|
2900
|
+
<tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">When to re-derive. <code>idle</code> by default, coalescing to a frame, because a hundred cell updates in one frame are one derivation. A number debounces by that many milliseconds. <code>live</code> re-derives on every change. <code>manual</code> never re-derives on its own: the host triggers it by calling <code>rows.load()</code> on the derived grid, with no argument, which re-reads <code>from</code> there and then — <a href="api-detail.html#derived-manual-refresh">a frozen panel refreshed on a button press</a>, executed.</td></tr>
|
|
2845
2901
|
</tbody>
|
|
2846
2902
|
</table>
|
|
2847
2903
|
</div>
|
|
2904
|
+
<pre><code><span class="cmt">// refresh: 'manual' — the summary re-derives only when the host asks.</span>
|
|
2905
|
+
refreshButton.addEventListener('click', () => summary.rows.load());</code></pre>
|
|
2848
2906
|
<h3 id="derived-statistics">The relational statistics, as rows</h3>
|
|
2849
2907
|
<p class="section-note">
|
|
2850
2908
|
A single-column statistic already has a route: <code>select</code> reduces a group with any
|
|
@@ -4162,7 +4220,7 @@ off(); <span class="cmt">// on() returns i
|
|
|
4162
4220
|
<tr><td class="name">row:confirmed</td><td class="type">{ id, kind, key, tempKey?, row, superseded }</td><td class="desc">The append or delete reached the server. For an append the row has already been rekeyed from <code>tempKey</code> to the server <code>key</code> — selection, expansion, focus and in-flight cell edits followed.</td></tr>
|
|
4163
4221
|
<tr><td class="name">row:reverted</td><td class="type">{ id, kind, key, tempKey?, reason, row, superseded, applied }</td><td class="desc">The append or delete failed: an appended row is removed, a deleted row restored. <code>applied: false</code> means a newer op owned the key, so nothing was undone.</td></tr>
|
|
4164
4222
|
<tr><td class="name">row:conflict</td><td class="type">{ id, kind, key, serverRow, row }</td><td class="desc">The op succeeded but the server row had moved underneath it. Last-write-wins: <code>serverRow</code> carries the server's truth so the divergence is surfaced, never swallowed.</td></tr>
|
|
4165
|
-
<tr><td class="name">cell:contextmenu</td><td class="type">{ ...cellParams, row }</td><td class="desc">Right-click on a cell
|
|
4223
|
+
<tr><td class="name">cell:contextmenu</td><td class="type">{ ...cellParams, row }</td><td class="desc">Right-click on a cell, or anywhere else on a row: in the empty tail beyond the last column <code>colId</code> is <code>null</code>, <code>column</code> and <code>value</code> are <code>undefined</code>. See <a href="#custom-menu">the menu chain</a>.</td></tr>
|
|
4166
4224
|
<tr><td class="name">sort:changed</td><td class="type">{ sort }</td><td class="desc">The full sort entry list.</td></tr>
|
|
4167
4225
|
<tr><td class="name">filter:changed</td><td class="type">{ filters } | { quick }</td><td class="desc">The condition tree or the quick filter changed.</td></tr>
|
|
4168
4226
|
<tr><td class="name">group:toggled</td><td class="type">{ expanded, all? }</td><td class="desc">A group row opened or closed.</td></tr>
|
|
@@ -4182,7 +4240,7 @@ off(); <span class="cmt">// on() returns i
|
|
|
4182
4240
|
<tr><td class="name">scroll</td><td class="type">{ top, left }</td><td class="desc">Throttled to the frame.</td></tr>
|
|
4183
4241
|
<tr><td class="name">scroll:end</td><td class="type">{}</td><td class="desc">Scrolling settled, the moment to trigger deferred work.</td></tr>
|
|
4184
4242
|
<tr><td class="name">size:changed</td><td class="type">{}</td><td class="desc">The viewport resized.</td></tr>
|
|
4185
|
-
<tr><td class="name">state:changed</td><td class="type">{ state, report }</td><td class="desc"
|
|
4243
|
+
<tr><td class="name">state:changed</td><td class="type">{ cause, sections, state, report }</td><td class="desc">Every state change, gesture or API call, announced exactly once — with one known gap: a predicate registered through <code>filters.where()</code> changes the <code>where</code> section and the rows on screen without raising it (BACKLOG-0001235). <code>cause</code> is <code>'user'</code> (a sort, filter, column move, resize, pin or hide, a grouping, a pivot, a page), <code>'apply'</code> (<code>state.apply()</code>, including an undo and a <code>config.state</code> seed) or <code>'reset'</code> (<code>state.reset()</code>) — build view persistence on this one event and skip <code>'reset'</code>, or the default is written back over the view the user just left. <code>sections</code> names the <code>GridState</code> keys that moved. <code>state</code> and <code>report</code> are carried by an apply or a reset and are <code>null</code> for <code>'user'</code>: call <code>grid.state.get()</code>, which is permission-sanitised, when you write. Selection, scroll, expansion, facets and annotations do not raise it.</td></tr>
|
|
4186
4244
|
<tr><td class="name">stream:chunk</td><td class="type">{ loaded, estimated, count, renders }</td><td class="desc">A streamed chunk landed.</td></tr>
|
|
4187
4245
|
<tr><td class="name">stream:end</td><td class="type">{ loaded, promoted, threshold }</td><td class="desc">Streaming finished; <code>promoted</code> means it switched to in-memory.</td></tr>
|
|
4188
4246
|
<tr><td class="name">source:error</td><td class="type">{ error, block?, range? }</td><td class="desc">A source or block load failed.</td></tr>
|
|
@@ -4605,7 +4663,7 @@ const chart = createChart({
|
|
|
4605
4663
|
<tr><td class="sig">Two axes</td><td><code>combo</code>, <code>pareto</code></td><td><code>x</code>, <code>measures</code></td></tr>
|
|
4606
4664
|
<tr><td class="sig">Distribution</td><td><code>histogram</code>, <code>boxplot</code></td><td><code>y</code> alone</td></tr>
|
|
4607
4665
|
<tr><td class="sig">Matrix</td><td><code>heatmap</code></td><td><code>x</code>, <code>y</code>, <code>series</code></td></tr>
|
|
4608
|
-
<tr><td class="sig">Part to whole</td><td><code>pie</code>, <code>donut</code>, <code>sunburst</code>, <code>treemap</code></td><td><code>x</code>, <code>y</code
|
|
4666
|
+
<tr><td class="sig">Part to whole</td><td><code>pie</code>, <code>donut</code>, <code>sunburst</code>, <code>treemap</code></td><td><code>x</code>, <code>y</code>; or the grid's grouping, see below</td></tr>
|
|
4609
4667
|
<tr><td class="sig">Specialist</td><td><code>radar</code>, <code>gauge</code>, <code>funnel</code>, <code>candlestick</code></td><td>varies; candlestick takes four <code>measures</code> in open, high, low, close order</td></tr>
|
|
4610
4668
|
<tr><td class="sig">Geographic</td><td><code>geomap</code></td><td><code>x</code> as an ISO code, <code>y</code> as the value</td></tr>
|
|
4611
4669
|
<tr><td class="sig">Flow</td><td><code>sankey</code>, <code>chord</code>, <code>network</code></td><td><code>source</code>, <code>target</code>, <code>y</code></td></tr>
|
|
@@ -4614,6 +4672,7 @@ const chart = createChart({
|
|
|
4614
4672
|
</table>
|
|
4615
4673
|
</div>
|
|
4616
4674
|
<p>A chart given data it cannot draw (a candlestick with three measures rather than four) says so on the chart rather than drawing nothing, because a chart that silently draws nothing is indistinguishable from one that is broken.</p>
|
|
4675
|
+
<p><strong>Hierarchical data.</strong> The part-to-whole types read nested input from the grid's own grouping, not from the spec: with <code>grid.columns.group(['region', 'product'])</code> in place, the tree is that grouping, one level per grouped column in that order, and <code>x</code> is not consulted; <code>depth</code> caps how many levels are read. On a flat grid, <code>x</code> is the single level. What each type draws of that tree: a <code>pie</code> or <code>donut</code> draws the top level; a <code>sunburst</code> draws every level as a ring, each segment its share of the segment inside it, and names a segment on any ring where the name fits, leaving it unnamed where it does not; a <code>treemap</code> nests: children inside their parent's tile, each branch with a header band naming it and padding round its children, to the depth the tree has. A child whose tile would be smaller than a line of text is not drawn and its parent's tile stands for it, so the levels drawn are the levels that can be read; a small group at the top level is always drawn, as a labelled tile with no children inside it. <code>drill</code> descends the tree on click, on either: clicking any tile or arc, at any depth, makes that node the root — a tile nested two levels down, or a segment on the outer ring, not only the top level — and the <code>drill</code> event carries the full <code>path</code> of labels from the root to it. <code>ascend()</code> comes back out a level at a time along the same path.</p>
|
|
4617
4676
|
|
|
4618
4677
|
<h3>The spec</h3>
|
|
4619
4678
|
<div class="table-wrap">
|
|
@@ -4623,7 +4682,7 @@ const chart = createChart({
|
|
|
4623
4682
|
<tr><td class="name">grid</td><td class="type">Grid</td><td class="desc">Required. The grid to read.</td></tr>
|
|
4624
4683
|
<tr><td class="name">container</td><td class="type">Element | string</td><td class="desc">Required. Where to draw.</td></tr>
|
|
4625
4684
|
<tr><td class="name">type</td><td class="type">string</td><td class="desc">One of the thirty above.</td></tr>
|
|
4626
|
-
<tr><td class="name">x / y</td><td class="type">string</td><td class="desc">Category and measure columns.</td></tr>
|
|
4685
|
+
<tr><td class="name">x / y</td><td class="type">string</td><td class="desc">Category and measure columns. <code>x</code> is one column id: an array there is not a nesting instruction and warns once, naming the option and what it accepts; nest by grouping the grid instead (see <em>Hierarchical data</em> above).</td></tr>
|
|
4627
4686
|
<tr><td class="name">series</td><td class="type">string</td><td class="desc">Splits the measure into one series per distinct value.</td></tr>
|
|
4628
4687
|
<tr><td class="name">measures</td><td class="type">object[]</td><td class="desc"><code>{col, fn, type, axis}</code>: several measures at once, each reduced by an aggregation. <code>fn</code> is one of <code>sum</code>, <code>avg</code> (alias <code>mean</code>), <code>min</code>, <code>max</code>, <code>count</code>, <code>countValues</code>, <code>first</code>, <code>last</code>; it defaults to <code>sum</code>. An <code>fn</code> that is none of these is a mistake, not a silent <code>sum</code>: it warns once, naming the value and the supported set, and falls back to <code>sum</code> so the chart still draws.</td></tr>
|
|
4629
4688
|
<tr><td class="name">title</td><td class="type">string</td><td class="desc">Drawn above the plot.</td></tr>
|
|
@@ -4639,7 +4698,7 @@ const chart = createChart({
|
|
|
4639
4698
|
<tr><td class="name">footnote</td><td class="type">string</td><td class="desc">A note under the plot, a source, a caveat, a unit.</td></tr>
|
|
4640
4699
|
<tr><td class="name">emptyText</td><td class="type">string</td><td class="desc">What to show when the binding produces nothing. Said rather than left blank, because an empty plot and a broken one look identical.</td></tr>
|
|
4641
4700
|
<tr><td class="name">fit</td><td class="type">boolean | 'line'</td><td class="desc">A least-squares line through a scatter or bubble chart, one per series. <code>true</code> draws it with its R²; <code>'line'</code> draws the line alone. Only where the x axis is numeric: on a band scale a slope would be a slope through the order the categories happened to be listed in.</td></tr>
|
|
4642
|
-
<tr><td class="name">trend</td><td class="type">boolean | string | ChartTrend | array</td><td class="desc">Trend and forecast overlays (BACKLOG-0000952): a <code>linear</code> least-squares line, a <code>movingAverage</code>, or <code>exponential</code> smoothing, one per series. <code>true</code> draws a single linear trend; a method name or a <code>{ method, forecast, window, kind, alpha, beta }</code> object configures one; an array draws several. The maths matches the core stats engine to the last digit (a parity test asserts it) but is computed locally to keep the charts bundle lean. For the linear method, <code>forecast: n</code> projects the line <code>n</code> steps past the data as a dashed forecast; a moving average and a smoothed level have no slope to project, so <code>forecast</code> is ignored for them and the fact is said in the accessible description.</td></tr>
|
|
4701
|
+
<tr><td class="name">trend</td><td class="type">boolean | string | ChartTrend | array</td><td class="desc">Trend and forecast overlays (BACKLOG-0000952): a <code>linear</code> least-squares line, a <code>movingAverage</code>, or <code>exponential</code> smoothing, one per series. <code>true</code> draws a single linear trend; a method name or a <code>{ method, forecast, window, kind, alpha, beta }</code> object configures one; an array draws several. The maths matches the core stats engine to the last digit (a parity test asserts it) but is computed locally to keep the charts bundle lean. For the linear method, <code>forecast: n</code> projects the line <code>n</code> steps past the data as a dashed forecast; a moving average and a smoothed level have no slope to project, so <code>forecast</code> is ignored for them and the fact is said in the accessible description. The trend layer (the fitted line, the forecast line, its band and the R² label) may extend past the plot into the chart's margin, but it is bounded by the chart box, the chart's own <code><svg></code>: a forecast that reaches past the chart's edge is cut off there rather than painted over the content beside it (BACKLOG-0001120).</td></tr>
|
|
4643
4702
|
<tr><td class="name">error</td><td class="type">boolean | object</td><td class="desc">Whiskers showing the uncertainty in each mark, computed from the readings the chart can see behind it. <code>{ of }</code> takes a symmetric margin from a column instead; <code>{ confidence }</code> sets the level. A mark the chart sees only one value for gets none, and the chart says so.</td></tr>
|
|
4644
4703
|
<tr><td class="name">stack</td><td class="type">boolean</td><td class="desc">Stack the series rather than drawing them side by side.</td></tr>
|
|
4645
4704
|
<tr><td class="name">curve</td><td class="type">boolean</td><td class="desc">Overlay a kernel density curve on a histogram, which shows which features are in the data and which are in the binning.</td></tr>
|
|
@@ -4658,6 +4717,48 @@ const chart = createChart({
|
|
|
4658
4717
|
</tbody>
|
|
4659
4718
|
</table>
|
|
4660
4719
|
</div>
|
|
4720
|
+
<p class="section-note">
|
|
4721
|
+
<strong>A reduction over no readings is a gap, not a zero (BACKLOG-0001088).</strong>
|
|
4722
|
+
<code>sum</code>, <code>avg</code>/<code>mean</code>, <code>min</code>, <code>max</code>,
|
|
4723
|
+
<code>first</code> and <code>last</code> all answer <code>null</code> for a category whose
|
|
4724
|
+
rows carry no value to reduce, so the line breaks and the bar is absent rather than dropping
|
|
4725
|
+
to zero — a zero is a real reading, and drawing one where the data reported nothing
|
|
4726
|
+
would show a plunge that never happened. <code>count</code> and <code>countValues</code> are
|
|
4727
|
+
the deliberate exception: <code>count</code> tallies rows and <code>countValues</code> tallies
|
|
4728
|
+
the values actually present, so both are honestly zero when that is the true answer.
|
|
4729
|
+
<code>countValues</code> is the one to ask for when “none arrived” is the reading
|
|
4730
|
+
you want drawn as zero rather than as a break in the line.
|
|
4731
|
+
</p>
|
|
4732
|
+
<pre data-run="js" data-expect="10,null | 1,0" data-covers="export:bindSeries config:fn config:countValues"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
4733
|
+
<span class="kw">const</span> { bindSeries } = <span class="kw">await</span> import('../packages/modules/charts/bind.js');
|
|
4734
|
+
|
|
4735
|
+
<span class="cmt">// 'a' has a reading; 'b' has a row, but the reading itself is absent.</span>
|
|
4736
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
4737
|
+
rowKey: 'id',
|
|
4738
|
+
columns: [{ field: 'day', type: 'text' }, { field: 'sales', type: 'number' }],
|
|
4739
|
+
rows: [
|
|
4740
|
+
{ id: 1, day: 'a', sales: 10 },
|
|
4741
|
+
{ id: 2, day: 'b', sales: null },
|
|
4742
|
+
],
|
|
4743
|
+
});
|
|
4744
|
+
|
|
4745
|
+
<span class="cmt">// avg over no readings is null: 'b' is a gap in the line, not a zero.</span>
|
|
4746
|
+
<span class="kw">const</span> gap = bindSeries(grid, { x: 'day', y: { col: 'sales', fn: 'avg' } });
|
|
4747
|
+
<span class="cmt">// countValues asks "how many arrived": honestly 0 for 'b', not null.</span>
|
|
4748
|
+
<span class="kw">const</span> none = bindSeries(grid, { x: 'day', y: { col: 'sales', fn: 'countValues' } });
|
|
4749
|
+
grid.destroy();
|
|
4750
|
+
|
|
4751
|
+
<span class="kw">return</span> [gap, none].map((bound) => bound.series[0].points.map((p) => String(p.y)).join(',')).join(' | ');</code></pre>
|
|
4752
|
+
<p class="section-note">
|
|
4753
|
+
<strong>A rolling <code>axis.x.window</code> that has aged past its data shows the empty
|
|
4754
|
+
state, not a picture drawn off-plot.</strong> The window's domain ends at the wall clock
|
|
4755
|
+
(see <code>window</code> under the <a href="#type-ChartAxis">axis</a> options below), so a
|
|
4756
|
+
feed that has gone quiet for longer than the window's span would otherwise have every mark
|
|
4757
|
+
fall outside the plot, with the axes and legend still drawn as if the chart were healthy.
|
|
4758
|
+
The chart shows its empty state instead and warns once <strong>per chart instance</strong>,
|
|
4759
|
+
naming the span and how old the newest reading is, so a dead feed reads as no data rather
|
|
4760
|
+
than as a chart that quietly stopped moving.
|
|
4761
|
+
</p>
|
|
4661
4762
|
|
|
4662
4763
|
<h3>The chart</h3>
|
|
4663
4764
|
<div class="table-wrap">
|
|
@@ -5419,7 +5520,7 @@ gantt.mount(document.querySelector('#plan'), {
|
|
|
5419
5520
|
width: 'container', // the default: fill the container, and keep following it
|
|
5420
5521
|
});</code></pre>
|
|
5421
5522
|
<p><strong>Sizing (BACKLOG-0001079).</strong> <code>width</code> defaults to <code>'container'</code>: the view measures the box it was mounted into and redraws itself whenever that box changes, so a plan in a tab, a drawer, an accordion, a responsive panel or a split pane fits without the host writing a <code>ResizeObserver</code> of its own. A container with no box — a hidden tab, or an element that has not been laid out yet — is not treated as a container of zero width: the view holds a 720px fallback and adopts the real width the moment there is one. Pass a <strong>number</strong> to take the decision yourself; a numeric <code>width</code> is honoured exactly, installs no observer, and keeps the eight-tick axis it always had — only a container-sized plot thins its tick labels to the width it was given, because only a container-sized plot can be somewhere it had not been before. <code>zoom</code> and a numeric <code>width</code> are mutually exclusive: <strong>zoom wins</strong> — it fixes the pixels-per-day and lets the plot scroll past the container — and passing both now warns rather than discarding the <code>width</code> in silence.</p>
|
|
5422
|
-
<p><strong>The project anchor (BACKLOG-0001079).</strong> A <code>mount</code> option, not a <code>mountSplit</code> one: the joined split view below takes neither <code>projectEpoch</code> nor a date-valued <code>today</code>, and its weekend shading is unanchored. The engine's time line is whole days since the Unix epoch, so a plan written as day offsets (<code>0, 4, 9…</code>) legitimately renders as January 1970 — day 0 <em>is</em> 1970-01-01, and the module cannot tell an offset from a real epoch day, so it cannot warn about it. <code>projectEpoch</code> says which calendar date plan day 0 stands for. It is <strong>display-only</strong>: axis ticks, bar labels, tooltips, screen-reader text and the built-in weekend shading move with it, and nothing the scheduler, <code>getState</code>, the CSV or the MSPDI export produces does — every <code>es</code>/<code>ef</code> you read back is still the number you supplied. A host-supplied <code>nonWorking</code> function keeps receiving raw plan days, since it was written against your day numbers. Use <code>projectStart</code> instead when you want the model itself to be on calendar dates.</p>
|
|
5523
|
+
<p><strong>The project anchor (BACKLOG-0001079).</strong> A <code>mount</code> option, not a <code>mountSplit</code> one: the joined split view below takes neither <code>projectEpoch</code> nor a date-valued <code>today</code>, and its weekend shading is unanchored. The engine's time line is whole days since the Unix epoch, so a plan written as day offsets (<code>0, 4, 9…</code>) legitimately renders as January 1970 — day 0 <em>is</em> 1970-01-01, and the module cannot tell an offset from a real epoch day, so it cannot warn about it. <code>projectEpoch</code> says which calendar date plan day 0 stands for. It and <code>today</code> take an ISO date string, a <code>Date</code> or a day number. <strong>A <code>Date</code> is read as the calendar date its local wall clock shows</strong> (BACKLOG-0001104), the way a <code>date</code> column reads one: <code>new Date(2026, 2, 2)</code> is 2 March in every time zone, and a <code>Date</code> that carries a time of day is the local day it falls on. Before 1104 the UTC instant was floored, which put local midnight a day early everywhere east of Greenwich. A <code>Date</code> is therefore decided by the reader's zone; a string is the same day on every machine — <code>'2026-03-02'</code> is 2 March in Sydney and in New York alike — which is the form to prefer for an anchor stored with the plan. Recognise your own case: if you worked around the old behaviour by passing <code>new Date(Date.UTC(y, m, d))</code>, a reader west of Greenwich now sees the previous day, because UTC midnight is still the evening before in New York — pass <code>new Date(y, m, d)</code> or the ISO string instead. It is <strong>display-only</strong>: axis ticks, bar labels, tooltips, screen-reader text and the built-in weekend shading move with it, and nothing the scheduler, <code>getState</code>, the CSV or the MSPDI export produces does — every <code>es</code>/<code>ef</code> you read back is still the number you supplied. A host-supplied <code>nonWorking</code> function keeps receiving raw plan days, since it was written against your day numbers. Use <code>projectStart</code> instead when you want the model itself to be on calendar dates.</p>
|
|
5423
5524
|
<pre><code>// A relative plan: offsets in the data, real dates on the screen.
|
|
5424
5525
|
gantt.mount(el, {
|
|
5425
5526
|
projectEpoch: '2026-03-02', // plan day 0 is this Monday
|
|
@@ -5518,7 +5619,7 @@ const board = createKanban(document.querySelector('#board'), {
|
|
|
5518
5619
|
<tr><td class="sig">sla</td><td class="desc">The card-aging / SLA monitor, present only when a <code>sla</code> config is supplied. Read <code>sla.states()</code>, <code>sla.breaches()</code>/<code>sla.warnings()</code> and <code>sla.stateFor(cardOrKey)</code> for each card's age and level; <code>sla.evaluate()</code> re-checks and fires crossings. See the card-aging note below.</td></tr>
|
|
5519
5620
|
<tr><td class="sig">setLoading(bool) / setError(message)</td><td class="desc">A loading state and a host-supplied error banner; empty columns already render their placeholder.</td></tr>
|
|
5520
5621
|
<tr><td class="sig">fields</td><td class="desc">Extra columns of the bound <code>grid</code> to project onto the rows a tile <code>filter</code> sees, beyond the fields the tiles declare. A read of a bound column outside the projection still resolves, and warns once naming the tile and the column; a <code>field</code> naming no column at all is refused by name at first read, and that tile reports <code>unknown</code> rather than an aggregation identity.</td></tr>
|
|
5521
|
-
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or recompute and re-render.</td></tr>
|
|
5622
|
+
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or recompute and re-render. What <code>refresh()</code> re-reads depends on where the rows come from: a <strong>grid-bound</strong> board re-reads the bound grid; a board still on its <strong><code>config.rows</code></strong> re-reads that array (a host that mutated it in place sees the change); a <strong>routed</strong> board — any row has arrived through <code>rows.apply</code>, typically from a Data Router <code>attach</code> — re-reads <em>nothing</em>: it regroups from the rows it holds and shows exactly what it showed before, re-rendered. <code>setRows</code> puts a routed board back on its configured rows.</td></tr>
|
|
5522
5623
|
<tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>, <code>card:move</code>, <code>card:reverted</code>, <code>card:confirmed</code>, <code>card:sla</code>, <code>selection:changed</code>, <code>drag:start</code>, <code>drag:end</code> (plus the vocabulary the later cycles emit). On a grid-bound board a move fires <code>card:move</code> optimistically; the grid's write-back then settles it with <code>card:confirmed</code> or, if the server rejects, <code>card:reverted</code> (the card re-reads and the flow transition log rolls the optimistic move back).</td></tr>
|
|
5523
5624
|
<tr><td class="sig">readonly(scope)</td><td class="desc">Whether a scope is readonly — the whole board, a <code>{ column }</code> or a <code>{ card }</code>. A readonly card is not draggable; a move into a readonly column is refused.</td></tr>
|
|
5524
5625
|
<tr><td class="sig">destroy()</td><td class="desc">Empty the element and drop the model. The host still owns any bound grid.</td></tr>
|
|
@@ -5693,7 +5794,7 @@ router.destroy();
|
|
|
5693
5794
|
<pre><code>import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
|
|
5694
5795
|
|
|
5695
5796
|
const kpi = createKPI(document.querySelector('#kpis'), {
|
|
5696
|
-
rows, <span class="cmt">// or { grid } to
|
|
5797
|
+
rows, <span class="cmt">// or { grid } to follow a live grid's rows</span>
|
|
5697
5798
|
rowKey: 'id',
|
|
5698
5799
|
columns: 4, <span class="cmt">// responsive tile columns</span>
|
|
5699
5800
|
tiles: [
|
|
@@ -5709,7 +5810,8 @@ const kpi = createKPI(document.querySelector('#kpis'), {
|
|
|
5709
5810
|
<p>Each tile names an <strong>aggregation</strong> (<code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code>, or a <code>custom</code> reducer <code>(rows, tile) => value</code>), the <strong>field</strong> it reads, and an optional <strong>filter</strong> predicate. Formatting (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>, with decimals, currency and locale), a <strong>baseline</strong> for a delta, and semantic <strong>threshold bands</strong> (two cut points with a direction, or an explicit <code>bands</code> list) are all per tile; the band is a semantic name (<code>good</code>/<code>warn</code>/<code>critical</code>), separate from any accent colour. An optional <strong>sparkline</strong> plots a <code>{ x, y }</code> series in sequence order. Each tile is a labelled <code><figure></code>, focusable and keyboard-activatable, its value announced; the sparkline transition respects <code>prefers-reduced-motion</code>.</p>
|
|
5710
5811
|
<p><strong>A panel with no data says so.</strong> A tile's status is <code>good</code>, <code>warn</code>, <code>critical</code>, <code>null</code> (no thresholds) — or <code>unknown</code>, which means the tile <em>measured nothing</em>. Two things cause that: the panel holds <em>no rows at all</em>, or the tile's <code>field</code> names no column on the bound grid, so it never read a cell to reduce over. That fourth status is decided from data presence <em>before</em> any threshold is consulted, because an aggregation over nothing returns the identity of its operation (<code>sum</code> and <code>count</code> return 0) and 0 is a number a threshold grades: under <code>lowerIsBetter</code> cut points it grades <code>good</code>, so an empty panel would otherwise read as a healthy one. An <code>unknown</code> tile reports a <code>null</code> value, renders the <code>nullText</code> placeholder rather than a zero, and is marked with a dashed edge and a visible “No data” caption that also forms part of its accessible name — the state is never carried by colour alone. Because <code>unknown</code> is an explicit status rather than a severity, anything rolling tiles up can surface “not measured” instead of inheriting a false green.</p>
|
|
5711
5812
|
<p><strong>A measured zero is still a measurement.</strong> A tile whose <code>filter</code> matches none of the rows the panel <em>does</em> hold is a different thing: no open incidents is genuinely good, so it reads <code>0</code> and is graded on its thresholds exactly as before. Only an empty panel is <code>unknown</code>.</p>
|
|
5712
|
-
<p><strong>Staleness is a different question, and a time-bounded source answers it.</strong> <code>unknown</code> is about the absence of rows, not their staleness: a panel over an unbounded store keeps showing the last values it was given when its feed goes quiet, because those rows are still there. Give the underlying source a <a href="#sources">rolling time window</a> (<code>maxAge</code> with <code>ageBy</code>) and its rows age out while the feed is silent, so the panel drains and reports <code>unknown</code> the next time it reads them — a grid-bound panel when the host calls <code>refresh()</code
|
|
5813
|
+
<p><strong>Staleness is a different question, and a time-bounded source answers it.</strong> <code>unknown</code> is about the absence of rows, not their staleness: a panel over an unbounded store keeps showing the last values it was given when its feed goes quiet, because those rows are still there. Give the underlying source a <a href="#sources">rolling time window</a> (<code>maxAge</code> with <code>ageBy</code>) and its rows age out while the feed is silent, so the panel drains and reports <code>unknown</code> the next time it reads them — a grid-bound panel as the grid announces the change (or when the host calls <code>refresh()</code>), a routed one as the removals reach <code>rows.apply</code>.</p>
|
|
5814
|
+
<p id="kpi-follows-grid"><strong>A grid-bound panel follows the grid.</strong> A panel given <code>grid</code> reads the grid's rows when it is built <em>and every time what the grid shows changes</em>: after a filter, a sort, a cell edit (<code>edit.setCells</code> or the keyboard), <code>rows.apply</code> or <code>rows.load</code> on the grid, and the grid's pipeline settle after an off-thread sort, the panel agrees with the grid beneath it without the host calling <code>refresh()</code>. It used to read the grid once, at bind, so a KPI rail beside a filtered grid kept showing the unfiltered numbers until the host wired <code>refresh()</code> to the grid's events themselves. The panel now subscribes to the same grid events a <code>createStat</code> tile follows, gated on the same pipeline-settle test, so a <code>model:changed</code> for an expand, a collapse or a page turn re-reads nothing. <strong>One re-read per change, not one per event:</strong> the grid announces one change several ways (<code>rows.apply</code> fires six events; an edit fires one <code>cell:changed</code> per cell), so the re-read is queued on a microtask and every event of one synchronous turn collapses into one read of the grid — read the panel after the turn ends (after an <code>await</code>, or in its <code>change</code> event), or call <code>refresh()</code> for the numbers now, which re-reads in the same turn and replaces the queued follow rather than doubling it. On a sorted grid past the worker threshold a filter change is two re-reads (one at the end of the turn, one when the settle lands); on a synchronous grid it is one. The grid is the truth, so <code>rows.apply</code> and <code>setRows</code> on a bound panel are still refused, and <code>destroy()</code> stops following. <strong>Want a snapshot instead?</strong> Do not pass <code>grid</code> as the source: pass the rows (<code>rows: snapshot</code>), with <code>grid</code> alongside if the panel should still adopt the grid's type and density — a panel given both reads <code>rows</code> and follows nothing.</p>
|
|
5713
5815
|
<p><strong>A grid-bound filter reads the columns you project, and says so when it cannot.</strong> A panel bound to a <code>grid</code> does not see whole grid rows: it materialises a <em>projection</em> of each row through the grid’s own value pipeline, carrying the row key plus the fields the tiles declare. That is what keeps a refresh over a large grid cheap — and it used to mean a <code>filter</code> reading any <em>other</em> column saw <code>undefined</code>, matched nothing, and reported a confident <code>0</code> beside a grid full of rows that matched. Declare the extra columns with <code>fields</code>:</p>
|
|
5714
5816
|
<pre><code>createKPI(el, {
|
|
5715
5817
|
grid,
|
|
@@ -5727,7 +5829,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
|
|
|
5727
5829
|
<tr><td class="sig">createKPI(el, config)</td><td class="desc">Create a KPI panel. Pass a DOM element to render into, or <code>null</code> for a headless panel that computes the same tile model without a DOM.</td></tr>
|
|
5728
5830
|
<tr><td class="sig">rows.apply({ add, update, remove })</td><td class="desc">The keyed-diff consumer contract a grid shares, so the panel is a drop-in Data Router target and updates each tile incrementally. Also <code>rows.forEach</code> and <code>rows.count</code>.</td></tr>
|
|
5729
5831
|
<tr><td class="sig">tiles() / tile(id) / value(id)</td><td class="desc">Every computed tile model, one tile by id (<code>value</code>, <code>formatted</code>, <code>status</code>, <code>delta</code>, <code>deltaPercent</code>, <code>count</code>, <code>sparkline</code>, <code>bar</code>), or a tile's raw value. <code>status</code> is <code>good</code>, <code>warn</code>, <code>critical</code>, <code>unknown</code> (the panel holds no rows) or <code>null</code> (no thresholds configured); <code>value</code> is <code>null</code> whenever the tile measured nothing.</td></tr>
|
|
5730
|
-
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or re-
|
|
5832
|
+
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or recompute every tile and re-render. What <code>refresh()</code> re-reads depends on where the rows come from: a <strong>grid-bound</strong> panel re-reads the bound grid (it <a href="#kpi-follows-grid">follows the grid</a> on its own, so this is for a host that wants the numbers in the same turn); a panel still on its <strong><code>config.rows</code></strong> re-reads that array (a host that mutated it in place sees the change); a <strong>routed</strong> panel — any row has arrived through <code>rows.apply</code>, typically from a Data Router <code>attach</code> — re-reads <em>nothing</em>: every tile is re-derived from the rows the panel holds and it shows exactly what it showed before, re-rendered. <code>setRows</code> puts a routed panel back on its configured rows.</td></tr>
|
|
5731
5833
|
<tr><td class="sig">nodes() / node(key) / visibleNodes()</td><td class="desc">The hierarchy, when <code>tree</code> resolves one: the top-level nodes with their children, one node by key at any depth, or just the nodes on screen. Each node carries <code>label</code>, <code>level</code>, <code>tile</code> (null on a synthesised level), <code>status</code>, <code>rollup</code> (the worst severity at or below it, never <code>unknown</code>), <code>unknown</code> (how many below it measured nothing) and <code>items</code>. Empty on a flat panel, where <code>kpi.tree</code> is <code>false</code>.</td></tr>
|
|
5732
5834
|
<tr><td class="sig">expand(key) / collapse(key) / toggle(key)</td><td class="desc">Open or close a branch. A key for a branch the panel does not (yet) hold is retained rather than dropped, so a delta that later introduces it finds it already open.</td></tr>
|
|
5733
5835
|
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">Serialise and restore the panel's row set, so a headless panel round-trips, plus <code>expanded</code> (the open branch keys) on a hierarchical panel. A snapshot with no <code>expanded</code> key leaves expansion alone rather than resetting it.</td></tr>
|
|
@@ -6016,7 +6118,7 @@ createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });</code><
|
|
|
6016
6118
|
<p><strong>Rearrangement.</strong> <code>compact: 'vertical'</code> (the default) pushes displaced windows down and then pulls everything up into whatever space that left, so a window dropped into empty space falls to the top of its column — <code>window:moved</code> carries both <code>to</code> (where it was asked to go) and <code>landed</code> (where it actually ended up). <code>compact: 'none'</code> keeps every window exactly where it is put. There is no horizontal compactor: pushing sideways has no single obviously-correct direction, and getting it wrong silently rearranges a dashboard a user carefully built.</p>
|
|
6017
6119
|
<p><strong>Keyboard, to the same standard as the drag.</strong> Every movable and resizable window carries a focusable handle running the full grab / move / drop / cancel model the kanban board established: <kbd>Space</kbd> or <kbd>Enter</kbd> grabs, the arrow keys move a tentative placement, <kbd>Enter</kbd> drops it through the same <code>beforeWindowMove</code> gate the pointer drag uses, and <kbd>Escape</kbd> cancels. A polite live region announces every step — grabbed, each tentative position with its column and row, dropped, cancelled, and <em>reverted</em> when a handler vetoes the drop — and focus returns to the handle afterwards. A window with <code>chrome: false</code> still gets a handle, because a movable window a keyboard user cannot move is not movable.</p>
|
|
6018
6120
|
<p><strong>An “Edit layout” button, without rebuilding the dashboard.</strong> <code>closable</code>, <code>movable</code> and <code>resizable</code> also take a <em>layout-level</em> default, so unlocking a twelve-window dashboard is one setting rather than twenty-four, and <code>setInteractive(true|false|{movable, resizable, closable})</code> changes that default at runtime — unlock, let the user rearrange, lock again and save <code>getLayout()</code>. Nothing is destroyed and nothing is rebuilt, so every grid, chart and board mounted in a window survives the toggle untouched. <strong>The asymmetry is deliberate: you can always take a capability away; you can never grant one where the developer said no.</strong> <code>setInteractive(false)</code> locks every window, including one whose own spec says <code>movable: true</code>, so a dashboard hard-locks in a single call without auditing twelve window specs; <code>setInteractive(true)</code> unlocks only the windows that never opted out, so a masthead declared <code>movable: false</code> stays pinned. Both halves of the enforcement move together — the handles a window renders <em>and</em> the checks the pointer and keyboard paths make, because removing a handle stops a mouse while only the gesture check stops a keyboard user already standing on one. <strong><code>config.movable: false</code> and <code>setInteractive(false)</code> are deliberately not the same thing:</strong> the config states the <em>default</em> for windows that declare nothing — and <code>false</code> is already that default, so it takes nothing away from a window that declared <code>movable: true</code> — while <code>setInteractive(false)</code> is an <em>active lock</em> that pins every window whatever its own spec says. <code>getInteractive()</code> reports all three states rather than two: <code>undefined</code> where no layout-level default is in force, <code>true</code>, or <code>false</code> for a lock. Reporting “unset” as <code>false</code> would read correctly and round-trip wrongly, so <code>setInteractive(getInteractive())</code> is a no-op in every state, and a key carrying <code>undefined</code> means “leave this capability alone”. Interactivity is a <em>mode</em>, not part of the arrangement: <code>getLayout()</code> does not carry it, <code>setLayout()</code> does not read it, and no event fires. <strong>A locked layout is not a read-only dashboard:</strong> the module creates the payload container and never reads or writes its contents, so a grid inside a window is made read-only with the grid's own settings — a dashboard that must not be edited is two decisions, not one.</p>
|
|
6019
|
-
<p><strong>Which payloads re-lay-out on <code>window:resized</code>, honestly.</strong> The grid and the chart each own a <code>ResizeObserver</code> and respond correctly; the Gantt does too since 1.52.0; kanban and KPI do no JS work at all on a resize and need none, because they reflow by CSS construction (a kanban column keeps its 280px and the board starts scrolling). Every one of those is measured in <code>test/layout-browser.test.js</code> rather than asserted
|
|
6121
|
+
<p><strong>Which payloads re-lay-out on <code>window:resized</code>, honestly.</strong> The grid and the chart each own a <code>ResizeObserver</code> and respond correctly; the Gantt does too since 1.52.0; kanban and KPI do no JS work at all on a resize and need none, because they reflow by CSS construction (a kanban column keeps its 280px and the board starts scrolling). Every one of those is measured in <code>test/layout-browser.test.js</code> rather than asserted, including a grid column declared as a <em>percentage</em> (<code>layout: { width: '50%' }</code>), which follows the window (BACKLOG-0001117): half of the viewport at 800px, half of it again at 400px.</p>
|
|
6020
6122
|
<p><strong>Idle cost, measured.</strong> A twelve-window dashboard is <strong>indistinguishable from a page with no layout module on it at all</strong>. Over 8 seconds of real Chrome (<code>bench/layout-idle.mjs</code>), twelve windows with empty payloads, twelve <em>independent</em> live grids in them, and a single lone grid with no layout module all sit in the same few-millisecond band — under a tenth of one percent of a core. They are not separated here because they cannot be: seven runs across two machines land between 3.1ms and 9.0ms and the ordering between them inverts run to run, so a stated delta would be reporting the noise floor. The module adds no timer, no frame loop and no polling, and owns exactly one <code>ResizeObserver</code> for the whole layout rather than one per window. <strong>The one figure that is a result rather than noise is not this module's:</strong> twelve grids <em>derived</em> from one shared parent filtered at 20Hz cost <strong>3,155–3,409ms</strong> over the same 8 seconds — several hundred times the quiet band, and stable across every run — because the engine's repaint listener for a derived source's <code>rows:changed</code> calls the renderer directly and bypasses <code>grid.updates.pause()</code>. The bench reports the size of that gap rather than leaving it inferred.</p>
|
|
6021
6123
|
<p><strong>Closing a window does not destroy its payload.</strong> <code>window:closed</code> hands the payload container back; whatever you mounted inside it is yours to destroy. Stated plainly because a leaked grid per closed window is the obvious failure, and this module has no way to know that a <code>div</code> contains something with a <code>destroy()</code>.</p>
|
|
6022
6124
|
<p><strong>Not in v1:</strong> horizontal compaction; per-frame drag events; nested layouts; window maximise/minimise; tabbed windows (that is <code>modules/tabs</code>); and <strong>responsive breakpoints — a twelve-window dashboard on a phone is unsolved, and this does not pretend otherwise</strong>. Server-side persistence is the host's, with <code>getLayout()</code>.</p>
|
|
@@ -6031,7 +6133,7 @@ createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });</code><
|
|
|
6031
6133
|
<tr><td class="sig">getLayout() / setLayout(snapshot)</td><td class="desc">The full current arrangement as plain JSON (<code>{columns, rows, windows: [{id, xPos, yPos, xSize, ySize}]}</code>), and its restore. <code>setLayout</code> never throws on garbage, and an entry naming a window that does not exist yet is <em>retained</em> and applied when that window is added.</td></tr>
|
|
6032
6134
|
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">The versioned persistence pair, following core's and the Gantt's shape: no arguments in, one plain JSON-safe object out, and <code>setState</code> survives whatever is handed to it.</td></tr>
|
|
6033
6135
|
<tr><td class="sig">setInteractive(value) / getInteractive()</td><td class="desc">Lock or unlock the whole dashboard at runtime, without destroying it. A boolean sets <code>movable</code>, <code>resizable</code> and <code>closable</code> together; an object sets only the keys it carries, and a key carrying <code>undefined</code> is treated as absent; <code>getInteractive()</code> returns the layout-level values as a copy, three-valued (<code>undefined</code> for unset, <code>true</code>, or <code>false</code> for a lock) so that <code>setInteractive(getInteractive())</code> is a no-op in every state. The config keys of the same name state the <em>default</em>; only this method takes a capability away. Locking always wins and unlocking never overrides an opt-out: <code>setInteractive(false)</code> pins a window that declared <code>movable: true</code>, and <code>setInteractive(true)</code> leaves a window that declared <code>movable: false</code> pinned. No event fires and <code>getLayout()</code> is unchanged — a mode is not an arrangement. It does not touch <code>maximisable</code> or <code>minimisable</code> either, for the same reason.</td></tr>
|
|
6034
|
-
<tr><td class="sig">maximise(id) / minimise(id) / restore(id)</td><td class="desc"><strong>Maximise fills the layout host</strong> — the element you mounted on — not the browser window, and hides every other window for the duration. That is deliberate: filling the viewport means <code>position: fixed</code>, whose containing block is the nearest ancestor carrying a <code>transform</code>, <code>filter</code>, <code>contain</code> or <code>will-change</code>, so the same rule fills the screen on one page and lands in a 300px box on the next; filling the host is a geometry change inside the layout and cannot disturb the page around it. <strong>Nothing moves</strong>: no compaction runs, no placement changes, and the payload container is the same DOM node throughout, so whatever you mounted in it is untouched. <strong>Escape restores it</strong> from anywhere inside the layout
|
|
6136
|
+
<tr><td class="sig">maximise(id) / minimise(id) / restore(id)</td><td class="desc"><strong>Maximise fills the layout host</strong> — the element you mounted on — not the browser window, and hides every other window for the duration. That is deliberate: filling the viewport means <code>position: fixed</code>, whose containing block is the nearest ancestor carrying a <code>transform</code>, <code>filter</code>, <code>contain</code> or <code>will-change</code>, so the same rule fills the screen on one page and lands in a 300px box on the next; filling the host is a geometry change inside the layout and cannot disturb the page around it. <strong>Nothing moves</strong>: no compaction runs, no placement changes, and the payload container is the same DOM node throughout, so whatever you mounted in it is untouched. <strong>Escape restores it</strong> from anywhere inside the layout — a focused grid body cell or column heading included — unless something inside has already claimed the key: an open cell editor, a filter menu or a column menu closes first, and the next Escape restores the window. A grid claims only an Escape it actually used, so a maximised grid never keeps the key (BACKLOG-0001143). Afterwards focus lands on the window's maximise control, so a keyboard user is somewhere they can act rather than wherever the payload left them. <code>minimise(id)</code> draws a window as a single row and hides its payload, keeping the chrome that carries the way back — so under <code>compact: 'vertical'</code> the windows below <em>pull up</em> on screen, which is the point of minimising one. <strong>In the arrangement, nothing moves at all:</strong> the collapse is a projection of the dashboard, not a change to it, so <code>restore(id)</code> gives back exactly the arrangement that was there — in <strong>any</strong> order, with any number of other windows still collapsed. All 14,400 minimise/restore orderings of a five-window dashboard are asserted. A window with <code>chrome: false</code> is refused by name: there would be nothing left on screen to restore it with. The controls are opt-in per window (<code>maximisable</code>, <code>minimisable</code>) with a layout-level default of the same name, and <code>setInteractive()</code> does <strong>not</strong> touch them — a display mode neither moves nor resizes a window in the arrangement, so a locked dashboard can still be blown up to read.</td></tr>
|
|
6035
6137
|
<tr><td class="sig">maximised() / minimised()</td><td class="desc">The id of the window filling the host (at most one — maximising a second restores the first), or <code>null</code>; and the ids of every minimised window in mount order. Neither state is part of <code>getLayout()</code>: a mode is not an arrangement, so <code>getLayout()</code> reports the <em>underlying</em> placement in both states — where the window will be when restored — and <code>setLayout()</code> never restores anyone into a mode, moving a minimised window <em>under</em> it instead.</td></tr>
|
|
6036
6138
|
<tr><td class="sig">refresh()</td><td class="desc">Re-measure every window and emit <code>window:resized</code> for those that changed. Called automatically; exposed for a host that changed something the module cannot observe, such as revealing an ancestor.</td></tr>
|
|
6037
6139
|
<tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>window:moved</code>, <code>window:resized</code>, <code>window:closed</code>, <code>layout:changed</code>; the cancellable <code>beforeWindowMove</code>, <code>beforeWindowResize</code> and <code>beforeWindowClose</code> (call <code>preventDefault(reason?)</code> or return <code>false</code>), each paired with <code>windowMove:cancelled</code>, <code>windowResize:cancelled</code> and <code>windowClose:cancelled</code>. <code>'*'</code> subscribes to every past-tense event and is deliberately never delivered a before-event. Config sugar for all ten. Drag progress is <strong>not</strong> emitted per frame.</td></tr>
|
|
@@ -6228,18 +6330,25 @@ createGrid(el, {
|
|
|
6228
6330
|
|
|
6229
6331
|
<p>The same option is accepted <strong>on a column definition</strong>, so a column's menu is
|
|
6230
6332
|
declared where the column is rather than as one more branch inside a single grid-level callback.
|
|
6231
|
-
It takes the same shapes plus a bare array for the common “
|
|
6333
|
+
It takes the same shapes plus a bare array for the common “these items here too”
|
|
6232
6334
|
case: <code>boolean | MenuItem[] | (params, defaults) => items</code>.</p>
|
|
6233
6335
|
<pre><code>createGrid(el, {
|
|
6234
6336
|
columns: [
|
|
6235
|
-
{ field: 'owner', contextMenu: [{ name: 'Reassign', action: reassign }] },
|
|
6337
|
+
{ field: 'owner', contextMenu: [{ name: 'Reassign', action: reassign }] }, <span class="cmt">// appended after the grid's items</span>
|
|
6236
6338
|
{ field: 'amount', contextMenu: (p, defaults) => [...defaults, { name: 'Reprice', action: reprice }] },
|
|
6339
|
+
{ field: 'ref', contextMenu: () => [{ name: 'Copy reference', action: copyRef }] }, <span class="cmt">// only this: ignore defaults</span>
|
|
6237
6340
|
{ field: 'nationalId', contextMenu: <span class="kw">false</span> }, <span class="cmt">// no menu on this column, others unaffected</span>
|
|
6238
6341
|
],
|
|
6239
6342
|
});</code></pre>
|
|
6240
6343
|
<p>The three levels <strong>compose as a chain</strong>: built-in defaults, then the grid-level
|
|
6241
6344
|
<code>contextMenu</code>, then the column's — each handed the previous result as its
|
|
6242
|
-
<code>defaults</code>, so a column adding one item never restates the built-ins.
|
|
6345
|
+
<code>defaults</code>, so a column adding one item never restates the built-ins. <strong>The
|
|
6346
|
+
array form chains too:</strong> <code>contextMenu: [items]</code> on a column is exactly
|
|
6347
|
+
<code>contextMenu: (p, defaults) => [...defaults, ...items]</code>, so the built-ins and every
|
|
6348
|
+
grid-level item stay and the column's items follow them, in the order written. To
|
|
6349
|
+
<strong>replace</strong> a column's menu instead, use the function form and ignore
|
|
6350
|
+
<code>defaults</code>: <code>contextMenu: () => items</code>. (Earlier releases let an array replace the grid-level
|
|
6351
|
+
items; the two forms now agree.) Suppression
|
|
6243
6352
|
follows the same order and the more specific level wins: <code>false</code> on a column is a
|
|
6244
6353
|
statement about that column alone. <strong>The reverse holds too, and is worth knowing before you
|
|
6245
6354
|
rely on grid-level <code>contextMenu: false</code> as a safety property: a column that declares
|
|
@@ -6253,6 +6362,21 @@ createGrid(el, {
|
|
|
6253
6362
|
the <kbd>Context Menu</kbd> key) honour the column exactly as the pointer does. See
|
|
6254
6363
|
<a href="api-detail.html#per-column-menu">the guide</a> for the full table of combinations.</p>
|
|
6255
6364
|
|
|
6365
|
+
<p><strong>The empty tail of a row is the row's.</strong> When the columns do not fill the
|
|
6366
|
+
grid's width, each row has an empty area to the right of the last column. A right-click there
|
|
6367
|
+
opens the grid's menu for that row, never the browser's, exactly as a right-click on a group row
|
|
6368
|
+
does: there is no column under the pointer, so the column link is missing from the chain and the
|
|
6369
|
+
grid-level menu stands, and <code>cell:contextmenu</code> (and so a builder's <code>params</code>)
|
|
6370
|
+
carries <code>colId: null</code>, <code>column: undefined</code> and <code>value: undefined</code>
|
|
6371
|
+
with the row, <code>key</code> and <code>index</code> filled in. The built-in items that act on
|
|
6372
|
+
a cell — Paste, Clear, Fill down, Edit cell — are not offered there (there is no cell
|
|
6373
|
+
for them to act on); the row and grid items are. A builder that reads
|
|
6374
|
+
<code>params.column</code> should expect it to be absent there. The area below the last row
|
|
6375
|
+
belongs to no row and keeps the browser's menu. <em>On 1.54 and earlier</em> a right-click in the
|
|
6376
|
+
tail fell through to the browser's menu, which looked as though the grid had none; on those
|
|
6377
|
+
versions give one column <code>layout: { flex: 1 }</code> so the cells reach the edge and there
|
|
6378
|
+
is no tail to click.</p>
|
|
6379
|
+
|
|
6256
6380
|
<p><code>columnMenu</code> takes the same form for the header's menu: both the 3-dot button
|
|
6257
6381
|
and a right-click on a heading. Its <code>params</code> is
|
|
6258
6382
|
<code>{ colId, column, grid }</code>. Anything of your own that you put on a column definition
|
|
@@ -6282,6 +6406,9 @@ createGrid(el, {
|
|
|
6282
6406
|
}],
|
|
6283
6407
|
},
|
|
6284
6408
|
});</code></pre>
|
|
6409
|
+
<p>An <code>icon</code> naming a sprite the registry does not have draws the blank glyph
|
|
6410
|
+
and logs a <code>[lattice]</code> warning once, naming the icon and how to register it
|
|
6411
|
+
(BACKLOG-0001211) — it does not fail silently as an empty, still-clickable button.</p>
|
|
6285
6412
|
|
|
6286
6413
|
<h2 id="styling">Styling and your page's CSS</h2>
|
|
6287
6414
|
<p><strong>Forced colours.</strong> In Windows High Contrast Mode the grid translates state that
|
|
@@ -6308,6 +6435,8 @@ createGrid(el, {
|
|
|
6308
6435
|
window, so a reader on row 500,000 is told so; and rows in a hierarchy carry their position among
|
|
6309
6436
|
their siblings, which a reader cannot count for itself when most of a branch was never rendered.</p>
|
|
6310
6437
|
<p>Focus is real focus rather than <code>aria-activedescendant</code>, and survives row recycling.
|
|
6438
|
+
Tabbing into a grid shows a focus ring around the grid at once; the first arrow key moves focus,
|
|
6439
|
+
and the ring, to a cell, and from then on Tab returns to that cell.
|
|
6311
6440
|
Sorting, filtering, selection, grouping, expanding, paging, undo, paste and a refused edit are all
|
|
6312
6441
|
announced. In Windows High Contrast Mode state is translated into borders and system colours
|
|
6313
6442
|
instead of tints. No information is carried by hue alone.</p>
|
|
@@ -6385,7 +6514,7 @@ createGrid(el, {
|
|
|
6385
6514
|
<span class="chip">clock</span><span class="chip">lock</span><span class="chip">link</span><span class="chip">external</span><span class="chip">filter</span>
|
|
6386
6515
|
<span class="chip">sortAsc</span><span class="chip">sortDesc</span><span class="chip">menu</span><span class="chip">drag</span>
|
|
6387
6516
|
<span class="chip">star</span><span class="chip">heart</span><span class="chip">circleFilled</span><span class="chip">square</span><span class="chip">bolt</span><span class="chip">flag</span><span class="chip">thumbUp</span>
|
|
6388
|
-
<span class="chip">eye</span><span class="chip">eyeOff</span><span class="chip">copy</span><span class="chip">blank</span>
|
|
6517
|
+
<span class="chip">eye</span><span class="chip">eyeOff</span><span class="chip">copy</span><span class="chip">present</span><span class="chip">blank</span>
|
|
6389
6518
|
</div>
|
|
6390
6519
|
|
|
6391
6520
|
<h2 id="operators">Filter grammar</h2>
|
|
@@ -6421,10 +6550,93 @@ createGrid(el, {
|
|
|
6421
6550
|
</table>
|
|
6422
6551
|
</div>
|
|
6423
6552
|
|
|
6553
|
+
<div class="note">
|
|
6554
|
+
<p><strong>An operator outside this list is refused, not applied (BACKLOG-0001180).</strong> <code>filters.set()</code> and <code>state.apply()</code> drop a condition whose <code>op</code> is not one of the operators above rather than installing it — it never reaches <code>filters.get()</code> and the grid is left exactly as filtered as it was before. A <code>[lattice]</code> 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.</p>
|
|
6555
|
+
</div>
|
|
6556
|
+
|
|
6424
6557
|
<div class="note">
|
|
6425
6558
|
<p><strong>One filter, not two.</strong> A condition set from a header popup, from the tool panel, or through <code>grid.filters.set()</code> all merge into the same tree. Reading <code>grid.filters.get()</code> always gives the whole truth.</p>
|
|
6426
6559
|
</div>
|
|
6427
6560
|
|
|
6561
|
+
<h3 id="where-predicates">Host predicates: <code>where</code></h3>
|
|
6562
|
+
<p class="section-note">
|
|
6563
|
+
Some filters cannot be written as a condition, because what they test is not in any column:
|
|
6564
|
+
whether this user may see the row, whether you hold a rate for its currency, whether it is in
|
|
6565
|
+
the set your last API call returned. Register those as named predicates.
|
|
6566
|
+
</p>
|
|
6567
|
+
|
|
6568
|
+
<pre><code>grid.filters.where('visibleToMe', row => row.owner === me);
|
|
6569
|
+
grid.filters.where('rateKnown', row => rates.has(row.ccy), { deps: ['ccy'], pinned: <span class="kw">true</span> });
|
|
6570
|
+
grid.filters.where('visibleToMe', <span class="kw">null</span>); <span class="cmt">// remove</span>
|
|
6571
|
+
grid.filters.where(); <span class="cmt">// the registered names</span>
|
|
6572
|
+
grid.filters.reapply('rateKnown'); <span class="cmt">// re-run one</span>
|
|
6573
|
+
grid.filters.reapply(); <span class="cmt">// re-run all</span></code></pre>
|
|
6574
|
+
|
|
6575
|
+
<div class="note">
|
|
6576
|
+
<p><strong>Registering is activating.</strong> 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.</p>
|
|
6577
|
+
</div>
|
|
6578
|
+
|
|
6579
|
+
<p>Several are in force at once under their own names, ANDed with each other and with the
|
|
6580
|
+
condition tree; removing one leaves the rest alone. The predicate is handed the <strong>data
|
|
6581
|
+
row</strong>, the same shape <code>DerivedSourceConfig.where</code> receives.</p>
|
|
6582
|
+
|
|
6583
|
+
<div class="table-wrap">
|
|
6584
|
+
<table>
|
|
6585
|
+
<thead><tr><th>Option</th><th>Type</th><th>What it does</th></tr></thead>
|
|
6586
|
+
<tbody>
|
|
6587
|
+
<tr><td class="name">deps</td><td class="type">string[]</td><td class="desc">The columns the predicate reads, in the same spirit as <code>value.deps</code> 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.</td></tr>
|
|
6588
|
+
<tr><td class="name">pinned</td><td class="type">boolean</td><td class="desc">Survive <code>filters.clear()</code>. For row-level permissions and tenant scoping, where a "clear filters" button must never widen what the user can see.</td></tr>
|
|
6589
|
+
<tr><td class="name">condition</td><td class="type">FilterSet</td><td class="desc">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.</td></tr>
|
|
6590
|
+
</tbody>
|
|
6591
|
+
</table>
|
|
6592
|
+
</div>
|
|
6593
|
+
|
|
6594
|
+
<div class="note">
|
|
6595
|
+
<p><strong>Coming from AG Grid's external filter?</strong> The three pieces map onto two. <code>isExternalFilterPresent()</code> disappears — registration <em>is</em> presence. <code>doesExternalFilterPass(node)</code> becomes the named predicate you pass to <code>where</code>. <code>onFilterChanged()</code> becomes either <code>deps</code>, when what changed is a column the grid can watch, or <code>reapply(name?)</code>, when it is something the grid cannot see at all — a rate table arriving late, a permission refresh.</p>
|
|
6596
|
+
</div>
|
|
6597
|
+
|
|
6598
|
+
<div class="note">
|
|
6599
|
+
<p><strong>On a pushdown source the counts are page-relative, and the grid says so.</strong> A predicate is a function: no engine can evaluate it, so it always runs client-side, over the rows that came back. The grid warns once that match counts and totals are therefore relative to the fetched set, and names the fix — give the predicate a <code>condition</code> twin so the engine narrows the fetch itself. The paged and remote sources receive the twin but do <strong>not</strong> apply the predicate to their window: their rows are held in a block cache indexed by the server's own ranges and totals, so filtering a block client-side would leave <code>count()</code> disagreeing with what is painted.</p>
|
|
6600
|
+
</div>
|
|
6601
|
+
|
|
6602
|
+
<div class="note">
|
|
6603
|
+
<p><strong>Only names are state.</strong> <code>filters.get()</code> still returns exactly what the user set. <code>state.get()</code> carries <code>where: string[]</code> — the names in force — because a predicate is your code and cannot be serialised into a saved view or restored from one. <code>state.apply()</code> naming a predicate you have not registered <em>reports the skip</em> rather than installing anything, and never removes a predicate a saved view did not name.</p>
|
|
6604
|
+
</div>
|
|
6605
|
+
|
|
6606
|
+
<p class="section-note">A pinned permission filter and a "my items" toggle on one grid, executed on every build:</p>
|
|
6607
|
+
<pre data-run="js" data-expect="1|2|2|teamVisible|teamVisible" data-covers="config:deps config:pinned config:condition"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6608
|
+
|
|
6609
|
+
<span class="kw">const</span> me = 'ana';
|
|
6610
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
6611
|
+
rowKey: 'id',
|
|
6612
|
+
columns: [{ field: 'team' }, { field: 'owner' }, { field: 'ccy' }],
|
|
6613
|
+
rows: [
|
|
6614
|
+
{ id: '1', team: 'eu', owner: 'ana', ccy: 'USD' },
|
|
6615
|
+
{ id: '2', team: 'us', owner: 'ana', ccy: 'USD' },
|
|
6616
|
+
{ id: '3', team: 'eu', owner: 'bo', ccy: 'ZWL' },
|
|
6617
|
+
],
|
|
6618
|
+
});
|
|
6619
|
+
|
|
6620
|
+
<span class="cmt">// Row-level permission. Pinned, so "clear filters" cannot widen it, and it</span>
|
|
6621
|
+
<span class="cmt">// carries a declarative twin a server can push.</span>
|
|
6622
|
+
grid.filters.where('teamVisible', (row) => row.team === 'eu', {
|
|
6623
|
+
pinned: <span class="kw">true</span>,
|
|
6624
|
+
condition: { col: 'team', op: 'eq', value: 'eu' },
|
|
6625
|
+
});
|
|
6626
|
+
<span class="cmt">// An ordinary "my items" toggle, re-run only when `owner` changes on a row.</span>
|
|
6627
|
+
grid.filters.where('myItems', (row) => row.owner === me, { deps: ['owner'] });
|
|
6628
|
+
|
|
6629
|
+
<span class="kw">const</span> both = grid.rows.count(); <span class="cmt">// permission AND my items</span>
|
|
6630
|
+
grid.filters.where('myItems', <span class="kw">null</span>); <span class="cmt">// toggle off</span>
|
|
6631
|
+
<span class="kw">const</span> afterToggleOff = grid.rows.count();
|
|
6632
|
+
grid.filters.clear(); <span class="cmt">// the pinned one survives</span>
|
|
6633
|
+
<span class="kw">const</span> afterClear = grid.rows.count();
|
|
6634
|
+
<span class="kw">const</span> stillOn = grid.filters.where().join(',');
|
|
6635
|
+
<span class="kw">const</span> inState = grid.state.get().where.join(',');
|
|
6636
|
+
grid.destroy();
|
|
6637
|
+
|
|
6638
|
+
<span class="kw">return</span> `${both}|${afterToggleOff}|${afterClear}|${stillOn}|${inState}`;</code></pre>
|
|
6639
|
+
|
|
6428
6640
|
<h3 id="config-example">A configuration, executed</h3>
|
|
6429
6641
|
<p class="section-note">This block runs on every build. If a key here stopped being honoured, or was
|
|
6430
6642
|
renamed, the build would fail rather than the documentation quietly going stale.</p>
|
|
@@ -6454,6 +6666,30 @@ createGrid(el, {
|
|
|
6454
6666
|
grid.destroy();
|
|
6455
6667
|
<span class="kw">return</span> n;</code></pre>
|
|
6456
6668
|
|
|
6669
|
+
<h3 id="direction-example">Writing direction and the two alignment vocabularies, executed</h3>
|
|
6670
|
+
<p class="section-note"><code>direction</code> is a recognised configuration key, and a column's resolved
|
|
6671
|
+
<code>align</code> keeps the spelling it was given: <code>left</code>/<code>right</code> are physical edges,
|
|
6672
|
+
<code>start</code>/<code>end</code> are logical and mirror in a right-to-left grid. A number column with no
|
|
6673
|
+
<code>align</code> of its own still defaults to the logical <code>end</code>.</p>
|
|
6674
|
+
<pre data-run="js" data-expect="rtl|left|right|start|end|end" data-covers="config:direction"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6675
|
+
|
|
6676
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
6677
|
+
direction: 'rtl',
|
|
6678
|
+
rowKey: 'id',
|
|
6679
|
+
columns: [
|
|
6680
|
+
{ id: 'l', field: 'l', align: 'left' }, <span class="cmt">// physical: the left edge in either direction</span>
|
|
6681
|
+
{ id: 'r', field: 'r', align: 'right' }, <span class="cmt">// physical: the right edge in either direction</span>
|
|
6682
|
+
{ id: 's', field: 's', align: 'start' }, <span class="cmt">// logical: the right edge in this RTL grid</span>
|
|
6683
|
+
{ id: 'e', field: 'e', align: 'end' }, <span class="cmt">// logical: the left edge in this RTL grid</span>
|
|
6684
|
+
{ id: 'n', field: 'n', type: 'number' }, <span class="cmt">// a number column defaults to the logical end</span>
|
|
6685
|
+
],
|
|
6686
|
+
rows: [{ id: '1', l: 'a', r: 'b', s: 'c', e: 'd', n: 1 }],
|
|
6687
|
+
});
|
|
6688
|
+
<span class="kw">const</span> resolved = ['l', 'r', 's', 'e', 'n'].map((id) => grid.columns.get(id).align);
|
|
6689
|
+
<span class="kw">const</span> out = [grid.config().direction, ...resolved].join('|');
|
|
6690
|
+
grid.destroy();
|
|
6691
|
+
<span class="kw">return</span> out;</code></pre>
|
|
6692
|
+
|
|
6457
6693
|
<h3 id="ingest-worker-example">Non-blocking stream ingest, executed</h3>
|
|
6458
6694
|
<p class="section-note">A <code>stream</code> source loaded with <code>ingest.useWorker</code> on. In a browser a
|
|
6459
6695
|
chunk that clears <code>ingest.workerThreshold</code> is columnized on a Worker so the main thread
|
|
@@ -6541,6 +6777,38 @@ grid.state.reset(); <span class="cmt">// state:re
|
|
|
6541
6777
|
grid.destroy();
|
|
6542
6778
|
<span class="kw">return</span> raised;</code></pre>
|
|
6543
6779
|
|
|
6780
|
+
<h3 id="persistence-example">View persistence, executed</h3>
|
|
6781
|
+
<p class="section-note">One event carries every state change and says what caused it, so a save
|
|
6782
|
+
layer subscribes once and skips the restore-to-default — which would otherwise write the default
|
|
6783
|
+
straight back over the view the user had just abandoned.</p>
|
|
6784
|
+
<pre data-run="js" data-expect="user,apply,reset:2" data-covers="event:state:changed method:state method:on method:destroy"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6785
|
+
|
|
6786
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
6787
|
+
rowKey: 'id',
|
|
6788
|
+
columns: [{ id: 'n', field: 'n' }, { id: 's', field: 's', type: 'number' }],
|
|
6789
|
+
rows: [{ id: '1', n: 'a', s: 3 }, { id: '2', n: 'b', s: 1 }],
|
|
6790
|
+
});
|
|
6791
|
+
|
|
6792
|
+
<span class="kw">const</span> causes = [];
|
|
6793
|
+
<span class="kw">let</span> writes = 0;
|
|
6794
|
+
grid.on('state:changed', (event) => {
|
|
6795
|
+
causes.push(event.cause);
|
|
6796
|
+
<span class="cmt">// The one cause a save must ignore: persisting a reset writes the default</span>
|
|
6797
|
+
<span class="cmt">// back over the view the user has just abandoned.</span>
|
|
6798
|
+
<span class="kw">if</span> (event.cause === 'reset') <span class="kw">return</span>;
|
|
6799
|
+
<span class="cmt">// A real host debounces, then writes grid.state.get() — which is</span>
|
|
6800
|
+
<span class="cmt">// permission-sanitised, unlike the raw capture.</span>
|
|
6801
|
+
writes++;
|
|
6802
|
+
});
|
|
6803
|
+
|
|
6804
|
+
grid.sort.set([{ col: 's', dir: 'asc' }]); <span class="cmt">// cause: 'user', sections: ['sort']</span>
|
|
6805
|
+
grid.state.apply({ version: 2, sort: [] }); <span class="cmt">// cause: 'apply', with a report</span>
|
|
6806
|
+
grid.state.reset(); <span class="cmt">// cause: 'reset' — deliberately not saved</span>
|
|
6807
|
+
|
|
6808
|
+
<span class="kw">const</span> result = `${causes.join(',')}:${writes}`;
|
|
6809
|
+
grid.destroy();
|
|
6810
|
+
<span class="kw">return</span> result;</code></pre>
|
|
6811
|
+
|
|
6544
6812
|
<h3 id="methods-example">Every grid namespace and method, executed</h3>
|
|
6545
6813
|
<p class="section-note">Reading a namespace builds it, so this proves each is reachable rather than
|
|
6546
6814
|
declared and absent. The plain methods are called, not merely named.</p>
|
|
@@ -6736,7 +7004,7 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
|
|
|
6736
7004
|
<h3 id="nested-config-example">Nested configuration, executed</h3>
|
|
6737
7005
|
<p class="section-note">Thirteen option blocks, each key written where it belongs. Parsed and evaluated on
|
|
6738
7006
|
every build, so a key that was renamed or moved shows up here.</p>
|
|
6739
|
-
<pre data-run="js" data-expect="14" data-covers="config:aboveLimit config:adapter config:ageBy config:binary config:bucket config:bucketFn config:buckets config:cacheLimit config:cardinalityLimit config:checkbox config:coalesceMs config:commit config:confirm config:cumulative config:debounce config:decimals config:delimiter config:display config:download config:enabled config:enterMovesDown config:fetch config:fileName config:fill config:fillHandle config:follow config:format config:from config:granularity config:groupBy config:hasChildren config:headerCheckbox config:headers config:hint config:idleMs config:indexLimit config:isMaster config:join config:label config:limit config:limitPer config:lineEnding config:loadChildren config:lock config:lockMs config:markdown config:maxCachedPages config:maxAge config:maxDecimals config:maxRows config:me config:minDecimals config:mode config:onCreate config:open config:orient config:orphans config:pageSize config:palette config:parentKey config:path config:pendingTimeout config:placement config:processCell config:profile config:promoteToMemoryBelow config:provider config:quote config:ranges config:removeMs config:retainSource config:roster config:scale config:select config:space config:start config:strategy config:system config:target config:throttleMs config:undoDepth config:unit config:unnest config:where"><code><span class="cmt">// Placeholders for the things a real page supplies. The point of this block is</span>
|
|
7007
|
+
<pre data-run="js" data-expect="14" data-covers="config:aboveLimit config:adapter config:ageBy config:binary config:bucket config:bucketFn config:buckets config:cacheLimit config:cardinalityLimit config:checkbox config:checkboxOnly config:coalesceMs config:commit config:confirm config:cumulative config:debounce config:decimals config:delimiter config:display config:download config:enabled config:enterMovesDown config:fetch config:fileName config:fill config:fillHandle config:follow config:format config:from config:granularity config:groupBy config:hasChildren config:headerCheckbox config:headers config:hint config:idleMs config:indexLimit config:isMaster config:join config:label config:limit config:limitPer config:lineEnding config:loadChildren config:lock config:lockMs config:markdown config:maxCachedPages config:maxAge config:maxDecimals config:maxRows config:me config:minDecimals config:mode config:onCreate config:open config:orient config:orphans config:pageSize config:palette config:parentKey config:path config:pendingTimeout config:placement config:processCell config:profile config:promoteToMemoryBelow config:provider config:quote config:ranges config:removeMs config:retainSource config:roster config:scale config:select config:space config:start config:strategy config:system config:target config:throttleMs config:undoDepth config:unit config:unnest config:where"><code><span class="cmt">// Placeholders for the things a real page supplies. The point of this block is</span>
|
|
6740
7008
|
<span class="cmt">// the option names: each one below is a documented key, written where it</span>
|
|
6741
7009
|
<span class="cmt">// belongs, so a key that was renamed or moved stops matching its interface.</span>
|
|
6742
7010
|
<span class="kw">const</span> source = {}, other = {}, provider = {}, compute = {}, adapter = {};
|
|
@@ -6756,7 +7024,7 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
|
|
|
6756
7024
|
enterMovesDown: true, undoDepth: 50, pendingTimeout: 2000 };
|
|
6757
7025
|
|
|
6758
7026
|
<span class="cmt">// Selection — SelectionConfig</span>
|
|
6759
|
-
<span class="kw">const</span> selectionConfig = { checkbox: true, headerCheckbox: true, ranges: true, fill: true, fillHandle: true };
|
|
7027
|
+
<span class="kw">const</span> selectionConfig = { checkbox: true, headerCheckbox: true, checkboxOnly: true, ranges: true, fill: true, fillHandle: true };
|
|
6760
7028
|
|
|
6761
7029
|
<span class="cmt">// Tree data — TreeConfig</span>
|
|
6762
7030
|
<span class="kw">const</span> treeConfig = { parentKey: 'parentId', path: 'path', orphans: 'root',
|
|
@@ -7382,11 +7650,11 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
7382
7650
|
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
7383
7651
|
<tbody>
|
|
7384
7652
|
<tr><td class="name">key</td><td class="type">string</td><td class="desc"></td></tr>
|
|
7385
|
-
<tr><td class="name">colId</td><td class="type">string</td><td class="desc"
|
|
7386
|
-
<tr><td class="name">value</td><td class="type">unknown</td><td class="desc"
|
|
7653
|
+
<tr><td class="name">colId</td><td class="type">string | null</td><td class="desc">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 (BACKLOG-0001153), or on a group row, pivot group row or full-width row. The grid-level menu stands in that case (BACKLOG-0001068).</td></tr>
|
|
7654
|
+
<tr><td class="name">value</td><td class="type">unknown</td><td class="desc">The cell's value; `undefined` when there is no column.</td></tr>
|
|
7387
7655
|
<tr><td class="name">row</td><td class="type">Row</td><td class="desc">The row wrapper.</td></tr>
|
|
7388
7656
|
<tr><td class="name">data</td><td class="type">unknown</td><td class="desc">Your original row object.</td></tr>
|
|
7389
|
-
<tr><td class="name">column</td><td class="type">ResolvedColumn</td><td class="desc"
|
|
7657
|
+
<tr><td class="name">column</td><td class="type">ResolvedColumn | undefined</td><td class="desc">The resolved column; `undefined` when `colId` is `null`.</td></tr>
|
|
7390
7658
|
<tr><td class="name">index</td><td class="type">number</td><td class="desc"></td></tr>
|
|
7391
7659
|
<tr><td class="name">grid</td><td class="type">Grid</td><td class="desc"></td></tr>
|
|
7392
7660
|
</tbody>
|
|
@@ -7818,7 +8086,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
7818
8086
|
<tr><td class="name">template</td><td class="type">string</td><td class="desc">Not read by the header renderer; use `render` to draw a custom heading. <small>(optional)</small></td></tr>
|
|
7819
8087
|
<tr><td class="name">render</td><td class="type">string | RendererCtor</td><td class="desc">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). <small>(optional)</small></td></tr>
|
|
7820
8088
|
<tr><td class="name">props</td><td class="type">Record<string, unknown></td><td class="desc">Props passed to `render` as `params.props`. <small>(optional)</small></td></tr>
|
|
7821
|
-
<tr><td class="name">class</td><td class="type">string | string[]</td><td class="desc">A class, or classes, added to the heading cell. <small>(optional)</small></td></tr>
|
|
8089
|
+
<tr><td class="name">class</td><td class="type">string | string[]</td><td class="desc">A class, or classes, added to the heading cell. A string may hold several space-separated tokens (`'a b'`), each applied individually. <small>(optional)</small></td></tr>
|
|
7822
8090
|
<tr><td class="name">tooltip</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
7823
8091
|
<tr><td class="name">align</td><td class="type">Align</td><td class="desc"><small>(optional)</small></td></tr>
|
|
7824
8092
|
</tbody>
|
|
@@ -7906,7 +8174,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
7906
8174
|
<tr><td class="name">resize</td><td class="type">(id: string, px: number): void</td><td class="desc"></td></tr>
|
|
7907
8175
|
<tr><td class="name">decorate</td><td class="type">(id: string, decoration: DecorationName | DecorationSpec | null, opts?: { variant?: VariantSpec }): void</td><td class="desc">Set, change or clear a column's decoration at runtime (§8.7). 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.</td></tr>
|
|
7908
8176
|
<tr><td class="name">autoSize</td><td class="type">(ids?: string | string[]): void</td><td class="desc"></td></tr>
|
|
7909
|
-
<tr><td class="name">fit</td><td class="type">(): void</td><td class="desc"
|
|
8177
|
+
<tr><td class="name">fit</td><td class="type">(): void</td><td class="desc">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 (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, or after rows arriving later bring a vertical scrollbar in, call it again.</td></tr>
|
|
7910
8178
|
<tr><td class="name">group</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
|
|
7911
8179
|
<tr><td class="name">pivot</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
|
|
7912
8180
|
<tr><td class="name">totals</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
|
|
@@ -8355,8 +8623,8 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
8355
8623
|
<tr><td class="name">cumulative</td><td class="type">{ of: string; upTo: number }</td><td class="desc">Keep rows until their running share of the total reaches `upTo`, 0 to 1. <small>(optional)</small></td></tr>
|
|
8356
8624
|
<tr><td class="name">profile</td><td class="type">string | string[]</td><td class="desc">One row per column, with the statistics as columns. Replaces the pipeline. <small>(optional)</small></td></tr>
|
|
8357
8625
|
<tr><td class="name">orient</td><td class="type">'columns' | 'metrics'</td><td class="desc">With `profile`, emit one row per statistic instead of one per column. <small>(optional)</small></td></tr>
|
|
8358
|
-
<tr><td class="name">statistics</td><td class="type">DerivedStatistics</td><td class="desc">Project a **relational** statistic into rows (BACKLOG-0001046): 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 (BACKLOG-0001092) 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) — 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. <small>(optional)</small></td></tr>
|
|
8359
|
-
<tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">When to re-derive. `idle` by default: coalesced to a frame. <small>(optional)</small></td></tr>
|
|
8626
|
+
<tr><td class="name">statistics</td><td class="type">DerivedStatistics</td><td class="desc">Project a **relational** statistic into rows (BACKLOG-0001046): 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 (BACKLOG-0001092) 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. <small>(optional)</small></td></tr>
|
|
8627
|
+
<tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">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`. <small>(optional)</small></td></tr>
|
|
8360
8628
|
<tr><td class="name">crossFilter</td><td class="type">boolean | string | { col?: string }</td><td class="desc">Let this grid filter the grid it derives from. `true` cross-filters through whatever it groups by; a string names a different source column. <small>(optional)</small></td></tr>
|
|
8361
8629
|
</tbody>
|
|
8362
8630
|
</table>
|
|
@@ -8729,8 +8997,11 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
8729
8997
|
<tr><td class="name">quickState</td><td class="type">(): { text: string; mode: string }</td><td class="desc">The quick filter's text and match mode, for restoring a control.</td></tr>
|
|
8730
8998
|
<tr><td class="name">get</td><td class="type">(): FilterSet</td><td class="desc"></td></tr>
|
|
8731
8999
|
<tr><td class="name">set</td><td class="type">(filters: FilterSet): void</td><td class="desc"></td></tr>
|
|
8732
|
-
<tr><td class="name">clear</td><td class="type">(): void</td><td class="desc"
|
|
9000
|
+
<tr><td class="name">clear</td><td class="type">(): void</td><td class="desc">Drop the condition tree, the quick filter, and every `where` predicate that was not registered `{ pinned: true }`.</td></tr>
|
|
8733
9001
|
<tr><td class="name">quick</td><td class="type">(text: string): void</td><td class="desc"></td></tr>
|
|
9002
|
+
<tr><td class="name">where</td><td class="type">(): string[]</td><td class="desc">The names of the `where` predicates in force, in registration order.</td></tr>
|
|
9003
|
+
<tr><td class="name">where</td><td class="type">(name: string, predicate: ((row: any) => boolean) | null, opts?: WhereOptions): void</td><td class="desc">Register, replace or remove a named row predicate composed with the filter set (BACKLOG-0001202). 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.</td></tr>
|
|
9004
|
+
<tr><td class="name">reapply</td><td class="type">(name?: string): boolean</td><td class="desc">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.</td></tr>
|
|
8734
9005
|
</tbody>
|
|
8735
9006
|
</table>
|
|
8736
9007
|
</div>
|
|
@@ -9002,7 +9273,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9002
9273
|
<tr><td class="name">columns</td><td class="type">(Column | ColumnGroup)[]</td><td class="desc">The columns, in order. A group nests columns under one heading. <small>(optional)</small></td></tr>
|
|
9003
9274
|
<tr><td class="name">columnGroups</td><td class="type">ColumnGroup[]</td><td class="desc">Header groups declared separately from the columns they contain. <small>(optional)</small></td></tr>
|
|
9004
9275
|
<tr><td class="name">rows</td><td class="type">unknown[]</td><td class="desc">The data, for a memory grid. Use `source` for anything fetched. <small>(optional)</small></td></tr>
|
|
9005
|
-
<tr><td class="name">rowKey</td><td class="type">string | ((row: unknown) => string)</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9276
|
+
<tr><td class="name">rowKey</td><td class="type">string | string[] | ((row: unknown) => string | string[])</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9006
9277
|
<tr><td class="name">source</td><td class="type">SourceConfig</td><td class="desc">Where rows come from: memory, paged, remote, stream or derived. <small>(optional)</small></td></tr>
|
|
9007
9278
|
<tr><td class="name">ingest</td><td class="type">IngestConfig</td><td class="desc">How rows are ingested into the column store. <small>(optional)</small></td></tr>
|
|
9008
9279
|
<tr><td class="name">columnDefaults</td><td class="type">Column</td><td class="desc">Applied to every column before its own settings. <small>(optional)</small></td></tr>
|
|
@@ -9016,10 +9287,11 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9016
9287
|
<tr><td class="name">variants</td><td class="type">Record<string, VariantDefinition></td><td class="desc">Named appearance variants a row or cell can be switched into by a rule. <small>(optional)</small></td></tr>
|
|
9017
9288
|
<tr><td class="name">tree</td><td class="type">TreeConfig</td><td class="desc">Hierarchical rows: where the parent link or the path lives. <small>(optional)</small></td></tr>
|
|
9018
9289
|
<tr><td class="name">detail</td><td class="type">DetailConfig</td><td class="desc">The expandable panel beneath a row. <small>(optional)</small></td></tr>
|
|
9019
|
-
<tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="desc">What the user may select, and how selection behaves across groups. <small>(optional)</small></td></tr>
|
|
9290
|
+
<tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9020
9291
|
<tr><td class="name">edit</td><td class="type">EditConfig | boolean</td><td class="desc">Editing, and how a change is committed and validated. <small>(optional)</small></td></tr>
|
|
9021
9292
|
<tr><td class="name">pagination</td><td class="type">PaginationConfig | boolean</td><td class="desc">Page the rows rather than scrolling them. <small>(optional)</small></td></tr>
|
|
9022
9293
|
<tr><td class="name">locale</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9294
|
+
<tr><td class="name">direction</td><td class="type">'ltr' | 'rtl' | 'auto'</td><td class="desc">Writing direction. Omit it, or say `'auto'`, to settle it from the element's own computed `dir` and then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. In a right-to-left grid the logical alignments `start`/`end` mirror while the physical `left`/`right` do not (see {@link Align}). <small>(optional)</small></td></tr>
|
|
9023
9295
|
<tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9024
9296
|
<tr><td class="name">theme</td><td class="type">Theme</td><td class="desc">The visual theme. <small>(optional)</small></td></tr>
|
|
9025
9297
|
<tr><td class="name">density</td><td class="type">Density</td><td class="desc">Row height and padding as a named step, rather than pixel by pixel. <small>(optional)</small></td></tr>
|
|
@@ -9046,7 +9318,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9046
9318
|
<tr><td class="name">headerHeight</td><td class="type">number</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9047
9319
|
<tr><td class="name">overscan</td><td class="type">number</td><td class="desc">How many rows to render beyond the viewport. More costs memory and smooths fast scrolling; fewer is lighter and can show a gap. <small>(optional)</small></td></tr>
|
|
9048
9320
|
<tr><td class="name">autoHeight</td><td class="type">boolean | 'visible'</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9049
|
-
<tr><td class="name">state</td><td class="type">GridState</td><td class="desc">Sort, filters, grouping, widths and the rest, restored at construction. <small>(optional)</small></td></tr>
|
|
9321
|
+
<tr><td class="name">state</td><td class="type">GridState</td><td class="desc">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`. <small>(optional)</small></td></tr>
|
|
9050
9322
|
<tr><td class="name">licence</td><td class="type">string</td><td class="desc">Your licence key. Without one the grid renders in full and watermarks off localhost. <small>(optional)</small></td></tr>
|
|
9051
9323
|
<tr><td class="name">maximise</td><td class="type">boolean</td><td class="desc">Offer a full-screen control. <small>(optional)</small></td></tr>
|
|
9052
9324
|
<tr><td class="name">formulaFunctions</td><td class="type">Record<string, (args: unknown[]) => unknown></td><td class="desc">Extra functions a formula may call, on top of the built-in library. <small>(optional)</small></td></tr>
|
|
@@ -9134,6 +9406,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9134
9406
|
<tr><td class="name">columnOrder</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9135
9407
|
<tr><td class="name">columnGroups</td><td class="type">ColumnGroupState[]</td><td class="desc">The banded-header tree, when the grid has one (BACKLOG-0000739). <small>(optional)</small></td></tr>
|
|
9136
9408
|
<tr><td class="name">filters</td><td class="type">FilterSet</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9409
|
+
<tr><td class="name">where</td><td class="type">string[]</td><td class="desc">The `where` predicates that were in force, as names only (BACKLOG-0001202). 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. <small>(optional)</small></td></tr>
|
|
9137
9410
|
<tr><td class="name">quick</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9138
9411
|
<tr><td class="name">sort</td><td class="type">SortEntry[]</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9139
9412
|
<tr><td class="name">group</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
|
|
@@ -9562,7 +9835,6 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9562
9835
|
<tr><td class="name">nullDisplay</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9563
9836
|
<tr><td class="name">locale</td><td class="type">string</td><td class="desc">The locale for number, date and text formatting. The page's by default. <small>(optional)</small></td></tr>
|
|
9564
9837
|
<tr><td class="name">messages</td><td class="type">Record<string, string | Record<string, string>></td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
9565
|
-
<tr><td class="name">direction</td><td class="type">'ltr' | 'rtl'</td><td class="desc">Writing direction. Omit to settle it from the element's own `dir` and then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. <small>(optional)</small></td></tr>
|
|
9566
9838
|
<tr><td class="name">scale</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9567
9839
|
</tbody>
|
|
9568
9840
|
</table>
|
|
@@ -10270,7 +10542,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10270
10542
|
<tr><td class="name">name</td><td class="type">string</td><td class="desc"></td></tr>
|
|
10271
10543
|
<tr><td class="name">description</td><td class="type">string</td><td class="desc"></td></tr>
|
|
10272
10544
|
<tr><td class="name">shared</td><td class="type">boolean</td><td class="desc"></td></tr>
|
|
10273
|
-
<tr><td class="name">isDefault</td><td class="type">boolean</td><td class="desc"
|
|
10545
|
+
<tr><td class="name">isDefault</td><td class="type">boolean</td><td class="desc">Applied on load when no `config.state` is given. `config.state` wins outright over this flag: with both present, the default view is never applied and the active view id stays `null`.</td></tr>
|
|
10274
10546
|
<tr><td class="name">builtin</td><td class="type">boolean</td><td class="desc">Supplied in `config.views.saved`: listed apart, and not renamable or deletable.</td></tr>
|
|
10275
10547
|
<tr><td class="name">createdAt</td><td class="type">number</td><td class="desc"></td></tr>
|
|
10276
10548
|
<tr><td class="name">updatedAt</td><td class="type">number</td><td class="desc"></td></tr>
|
|
@@ -10320,9 +10592,10 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10320
10592
|
<table>
|
|
10321
10593
|
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
10322
10594
|
<tbody>
|
|
10323
|
-
<tr><td class="name">mode</td><td class="type">'none' | 'single' | 'multiple'</td><td class="desc"
|
|
10595
|
+
<tr><td class="name">mode</td><td class="type">'none' | 'single' | 'multiple'</td><td class="desc">`'none'` also turns off `ranges` and `fillHandle` unless either is set explicitly alongside it. <small>(optional)</small></td></tr>
|
|
10324
10596
|
<tr><td class="name">checkbox</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
10325
10597
|
<tr><td class="name">headerCheckbox</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
10598
|
+
<tr><td class="name">checkboxOnly</td><td class="type">boolean</td><td class="desc">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. <small>(optional)</small></td></tr>
|
|
10326
10599
|
<tr><td class="name">groupSelectsChildren</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
10327
10600
|
<tr><td class="name">groupSelectsFiltered</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
10328
10601
|
<tr><td class="name">ranges</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
@@ -10359,7 +10632,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10359
10632
|
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
10360
10633
|
<tbody>
|
|
10361
10634
|
<tr><td class="name">get</td><td class="type">(): SortEntry[]</td><td class="desc"></td></tr>
|
|
10362
|
-
<tr><td class="name">set</td><td class="type">(entries: SortEntry[]): void</td><td class="desc"
|
|
10635
|
+
<tr><td class="name">set</td><td class="type">(entries: SortEntry[]): void</td><td class="desc">Replace the sort model; an entry naming no known column is dropped with a warning.</td></tr>
|
|
10363
10636
|
<tr><td class="name">clear</td><td class="type">(): void</td><td class="desc"></td></tr>
|
|
10364
10637
|
</tbody>
|
|
10365
10638
|
</table>
|
|
@@ -10437,7 +10710,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10437
10710
|
<tbody>
|
|
10438
10711
|
<tr><td class="name">get</td><td class="type">(): GridState</td><td class="desc"></td></tr>
|
|
10439
10712
|
<tr><td class="name">apply</td><td class="type">(state: GridState, opts?: { skip?: (keyof GridState)[] }): StateApplyReport</td><td class="desc"></td></tr>
|
|
10440
|
-
<tr><td class="name">baseline</td><td class="type">(): GridState | null</td><td class="desc">The
|
|
10713
|
+
<tr><td class="name">baseline</td><td class="type">(): GridState | null</td><td class="desc">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.</td></tr>
|
|
10441
10714
|
<tr><td class="name">reset</td><td class="type">(): StateApplyReport | null</td><td class="desc">Put the grid back the way it started, as one undoable step.</td></tr>
|
|
10442
10715
|
<tr><td class="name">modified</td><td class="type">(): boolean</td><td class="desc">Whether anything has changed since construction.</td></tr>
|
|
10443
10716
|
</tbody>
|
|
@@ -10453,6 +10726,19 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10453
10726
|
</tbody>
|
|
10454
10727
|
</table>
|
|
10455
10728
|
</div>
|
|
10729
|
+
<h3 id="type-StateChangedEvent">StateChangedEvent</h3>
|
|
10730
|
+
<p class="section-note">The `state:changed` event (BACKLOG-0001182). 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. **One known gap** (BACKLOG-0001235): a host predicate registered through `filters.where(name, fn)` changes the `where` section and the rows on screen without raising this event, so a persistence layer does not yet see it.</p>
|
|
10731
|
+
<div class="table-wrap">
|
|
10732
|
+
<table>
|
|
10733
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
10734
|
+
<tbody>
|
|
10735
|
+
<tr><td class="name">cause</td><td class="type">StateChangeCause</td><td class="desc">Why the state changed. `'reset'` is the one a save should ignore.</td></tr>
|
|
10736
|
+
<tr><td class="name">sections</td><td class="type">StateSection[]</td><td class="desc">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.</td></tr>
|
|
10737
|
+
<tr><td class="name">state</td><td class="type">GridState | null</td><td class="desc">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.</td></tr>
|
|
10738
|
+
<tr><td class="name">report</td><td class="type">StateApplyReport | null</td><td class="desc">What an apply could not restore; null for `'user'`.</td></tr>
|
|
10739
|
+
</tbody>
|
|
10740
|
+
</table>
|
|
10741
|
+
</div>
|
|
10456
10742
|
<h3 id="type-StatisticsApi">StatisticsApi</h3>
|
|
10457
10743
|
<div class="table-wrap">
|
|
10458
10744
|
<table>
|
|
@@ -10765,6 +11051,18 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10765
11051
|
</tbody>
|
|
10766
11052
|
</table>
|
|
10767
11053
|
</div>
|
|
11054
|
+
<h3 id="type-WhereOptions">WhereOptions</h3>
|
|
11055
|
+
<p class="section-note">How a `where` predicate is re-evaluated, whether `filters.clear()` may remove it, and what the source may be told about it (BACKLOG-0001202).</p>
|
|
11056
|
+
<div class="table-wrap">
|
|
11057
|
+
<table>
|
|
11058
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
11059
|
+
<tbody>
|
|
11060
|
+
<tr><td class="name">deps</td><td class="type">string[]</td><td class="desc">The columns the predicate reads, in the same spirit as `value.deps` on a computed column (§8.4.2). 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 is called on every pass — never stale, and never skipped either. <small>(optional)</small></td></tr>
|
|
11061
|
+
<tr><td class="name">pinned</td><td class="type">boolean</td><td class="desc">Survive `filters.clear()`. For a predicate that is not the user's filter — row-level permissions, tenant scoping — where a "clear filters" button must never widen what the user can see. <small>(optional)</small></td></tr>
|
|
11062
|
+
<tr><td class="name">condition</td><td class="type">FilterSet</td><td class="desc">A declarative twin of the predicate, pushed to the source while the function stays as the residual. On a pushdown engine this narrows the fetch instead of filtering a page client-side. 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. <small>(optional)</small></td></tr>
|
|
11063
|
+
</tbody>
|
|
11064
|
+
</table>
|
|
11065
|
+
</div>
|
|
10768
11066
|
<h3 id="type-WindowedResult">WindowedResult</h3>
|
|
10769
11067
|
<p class="section-note">One windowed figure and the window it covers.</p>
|
|
10770
11068
|
<div class="table-wrap">
|
|
@@ -10791,7 +11089,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10791
11089
|
<!-- END GENERATED TYPE REFERENCE -->
|
|
10792
11090
|
|
|
10793
11091
|
<footer>
|
|
10794
|
-
Lattice Grid 1.
|
|
11092
|
+
Lattice Grid 1.56.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
10795
11093
|
This document describes the behaviour of the shipped library. Where this guide and the code
|
|
10796
11094
|
disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
10797
11095
|
</footer>
|