@toclocoinc/lattice-grid 1.53.0 → 1.55.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 +100 -36
- package/docs/api-detail.html +88 -14
- package/lattice-grid.d.ts +122 -15
- package/lattice-grid.esm.min.js +285 -66
- package/lattice-grid.min.cjs +285 -66
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +285 -66
- package/modules/ai.esm.min.js +25 -4
- package/modules/ai.min.cjs +25 -4
- package/modules/ai.min.js +25 -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 +172 -43
- package/modules/charts.min.cjs +172 -43
- package/modules/charts.min.js +172 -43
- package/modules/data-router.esm.min.js +4 -4
- package/modules/data-router.min.cjs +4 -4
- package/modules/data-router.min.js +4 -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 +285 -66
- package/modules/htmx.min.cjs +285 -66
- package/modules/htmx.min.js +285 -66
- 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 +4213 -24
- package/modules/kpi.min.cjs +4213 -24
- package/modules/kpi.min.js +4213 -24
- package/modules/layout.esm.min.js +305 -23
- package/modules/layout.min.cjs +305 -23
- package/modules/layout.min.js +305 -23
- 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 +285 -66
- package/modules/webcomponent.min.cjs +285 -66
- package/modules/webcomponent.min.js +285 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
dependencies, no build step required. Optional adapters for React, Vue, Svelte
|
|
5
5
|
and Web Components ship alongside it.
|
|
6
6
|
|
|
7
|
-
Version 1.
|
|
7
|
+
Version 1.55.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -301,11 +301,13 @@ Alongside those: the stylesheet `lattice-grid.min.css` (required, imported as
|
|
|
301
301
|
published package directly:
|
|
302
302
|
|
|
303
303
|
```
|
|
304
|
-
https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid
|
|
304
|
+
https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1/<file>
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
`@1` pins the major: a page in production picks up fixes within 1.x and never a
|
|
308
|
+
breaking release, where `@latest` would. To freeze a page on one exact build,
|
|
309
|
+
replace `@1` with the full version `getVersion()` reports. The `<file>` column
|
|
310
|
+
in each table below is exactly what you append.
|
|
309
311
|
|
|
310
312
|
### Core
|
|
311
313
|
|
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.55.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.55.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>
|
|
@@ -862,7 +863,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
862
863
|
<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
864
|
<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
865
|
<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
|
|
866
|
+
<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
867
|
<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
868
|
<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
869
|
<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>
|
|
@@ -989,7 +990,8 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
989
990
|
<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
991
|
<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
992
|
<tr><td class="name">tooltip</td><td class="type">string | (p) => string</td><td class="desc"></td></tr>
|
|
992
|
-
<tr><td class="name">align
|
|
993
|
+
<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>
|
|
994
|
+
<tr><td class="name">wrap / autoHeight</td><td class="type">, </td><td class="desc">Presentation flags.</td></tr>
|
|
993
995
|
<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
996
|
</tbody>
|
|
995
997
|
</table>
|
|
@@ -1015,7 +1017,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
1015
1017
|
<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
1018
|
<tr><td class="name">sort</td><td class="desc"><code>enabled</code>, <code>direction</code>, <code>order</code>, <code>nullsFirst</code></td></tr>
|
|
1017
1019
|
<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>
|
|
1020
|
+
<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
1021
|
<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
1022
|
</tbody>
|
|
1021
1023
|
</table>
|
|
@@ -1028,7 +1030,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
|
|
|
1028
1030
|
<table>
|
|
1029
1031
|
<thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1030
1032
|
<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.
|
|
1033
|
+
<tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.55.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
|
|
1032
1034
|
<tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
|
|
1033
1035
|
<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
1036
|
<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>
|
|
@@ -1101,7 +1103,7 @@ grid.overlay.hide();</code></pre>
|
|
|
1101
1103
|
<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
1104
|
<tr><td class="sig">resize(id, px)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1103
1105
|
<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">
|
|
1106
|
+
<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
1107
|
<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
1108
|
<tr><td class="sig">pivot(ids)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1107
1109
|
<tr><td class="sig">totals(ids)</td><td class="type">void</td><td class="desc">Which columns carry an aggregation.</td></tr>
|
|
@@ -4162,7 +4164,7 @@ off(); <span class="cmt">// on() returns i
|
|
|
4162
4164
|
<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
4165
|
<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
4166
|
<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
|
|
4167
|
+
<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
4168
|
<tr><td class="name">sort:changed</td><td class="type">{ sort }</td><td class="desc">The full sort entry list.</td></tr>
|
|
4167
4169
|
<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
4170
|
<tr><td class="name">group:toggled</td><td class="type">{ expanded, all? }</td><td class="desc">A group row opened or closed.</td></tr>
|
|
@@ -4605,7 +4607,7 @@ const chart = createChart({
|
|
|
4605
4607
|
<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
4608
|
<tr><td class="sig">Distribution</td><td><code>histogram</code>, <code>boxplot</code></td><td><code>y</code> alone</td></tr>
|
|
4607
4609
|
<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
|
|
4610
|
+
<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
4611
|
<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
4612
|
<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
4613
|
<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 +4616,7 @@ const chart = createChart({
|
|
|
4614
4616
|
</table>
|
|
4615
4617
|
</div>
|
|
4616
4618
|
<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>
|
|
4619
|
+
<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 a level per click on either.</p>
|
|
4617
4620
|
|
|
4618
4621
|
<h3>The spec</h3>
|
|
4619
4622
|
<div class="table-wrap">
|
|
@@ -4623,7 +4626,7 @@ const chart = createChart({
|
|
|
4623
4626
|
<tr><td class="name">grid</td><td class="type">Grid</td><td class="desc">Required. The grid to read.</td></tr>
|
|
4624
4627
|
<tr><td class="name">container</td><td class="type">Element | string</td><td class="desc">Required. Where to draw.</td></tr>
|
|
4625
4628
|
<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>
|
|
4629
|
+
<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
4630
|
<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
4631
|
<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
4632
|
<tr><td class="name">title</td><td class="type">string</td><td class="desc">Drawn above the plot.</td></tr>
|
|
@@ -4639,7 +4642,7 @@ const chart = createChart({
|
|
|
4639
4642
|
<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
4643
|
<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
4644
|
<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>
|
|
4645
|
+
<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
4646
|
<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
4647
|
<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
4648
|
<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>
|
|
@@ -5419,7 +5422,7 @@ gantt.mount(document.querySelector('#plan'), {
|
|
|
5419
5422
|
width: 'container', // the default: fill the container, and keep following it
|
|
5420
5423
|
});</code></pre>
|
|
5421
5424
|
<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>
|
|
5425
|
+
<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
5426
|
<pre><code>// A relative plan: offsets in the data, real dates on the screen.
|
|
5424
5427
|
gantt.mount(el, {
|
|
5425
5428
|
projectEpoch: '2026-03-02', // plan day 0 is this Monday
|
|
@@ -5517,7 +5520,8 @@ const board = createKanban(document.querySelector('#board'), {
|
|
|
5517
5520
|
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">Serialise and restore the board state — collapsed columns/lanes, column order, quick filter, sprint/epic selection and selection. Also accepted as <code>config.state</code> at construction.</td></tr>
|
|
5518
5521
|
<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
5522
|
<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
|
-
<tr><td class="sig">
|
|
5523
|
+
<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>
|
|
5524
|
+
<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>
|
|
5521
5525
|
<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>
|
|
5522
5526
|
<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>
|
|
5523
5527
|
<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>
|
|
@@ -5692,7 +5696,7 @@ router.destroy();
|
|
|
5692
5696
|
<pre><code>import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
|
|
5693
5697
|
|
|
5694
5698
|
const kpi = createKPI(document.querySelector('#kpis'), {
|
|
5695
|
-
rows, <span class="cmt">// or { grid } to
|
|
5699
|
+
rows, <span class="cmt">// or { grid } to follow a live grid's rows</span>
|
|
5696
5700
|
rowKey: 'id',
|
|
5697
5701
|
columns: 4, <span class="cmt">// responsive tile columns</span>
|
|
5698
5702
|
tiles: [
|
|
@@ -5706,9 +5710,19 @@ const kpi = createKPI(document.querySelector('#kpis'), {
|
|
|
5706
5710
|
onTileClick: ({ tile }) => drillInto(tile.id),
|
|
5707
5711
|
});</code></pre>
|
|
5708
5712
|
<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>
|
|
5709
|
-
<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 panel holds <em>no rows at all</em
|
|
5713
|
+
<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>
|
|
5710
5714
|
<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>
|
|
5711
|
-
<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
|
|
5715
|
+
<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>
|
|
5716
|
+
<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>
|
|
5717
|
+
<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>
|
|
5718
|
+
<pre><code>createKPI(el, {
|
|
5719
|
+
grid,
|
|
5720
|
+
fields: ['priority'], <span class="cmt">// project it, so the filter can read it</span>
|
|
5721
|
+
tiles: [
|
|
5722
|
+
{ label: 'P1 jobs', agg: 'count', filter: (r) => r.priority === 'P1' },
|
|
5723
|
+
],
|
|
5724
|
+
});</code></pre>
|
|
5725
|
+
<p>Forget to, and the panel tells you rather than quietly reporting a zero: a read of a column the bound grid <em>has</em> but the projection does not resolves to the real cell <strong>and</strong> warns once — “the ‘P1 jobs’ tile's filter read <code>priority</code>, which is a column on the bound grid but is not projected — add it to <code>fields</code>” — keyed on the tile and the field, so one bad filter over a 100,000-row grid produces one line, not 100,000. <code>undefined</code> on its own is deliberately <em>not</em> the trigger: a blank cell in a column you did project is a legal value and stays silent, because a warning that fires on ordinary sparse data gets muted and then deleted. A tile <code>field</code> naming no column on the grid at all is a different fault, and <strong>that tile reports no data rather than a number</strong>: reducing over a column that does not exist gives <code>sum</code> and <code>count</code> a <code>0</code>, which grades <code>good</code> under any <code>lowerIsBetter</code> threshold, so warning in the console while leaving a confident green zero on the dashboard would document the lie rather than fix it — and the reader of a dashboard is not reading the console. It reports the <code>unknown</code> status above, renders <code>nullText</code>, and contributes an explicit <code>unknown</code> to any roll-up. That is deliberately not the same case as a tile with a real field whose <code>filter</code> simply matches nothing: that tile measured, and its zero is still graded. The refusal is checked at first read rather than at bind time, because a dynamic grid's columns can arrive after the panel does, and the verdict is recomputed at every read, so a tile refused while the grid was still loading is measured again the moment its column lands. A panel over a plain <code>rows</code> array has whole rows already and none of this applies to it.</p>
|
|
5712
5726
|
<p><strong>Incremental, not recomputed.</strong> Each tile keeps a running accumulator, so a routed <code>rows.apply</code> delta adjusts only the rows it carries — an add contributes, a remove reverses, an update reverses the old row and contributes the new one — rather than re-reading the whole dataset per delta. The two bounded exceptions are honest: an extreme (<code>min</code>/<code>max</code>) removed at its current value triggers a rescan of that tile's own value multiset, and a <code>custom</code> reducer is recomputed over the (filtered) store because an arbitrary function has no inverse.</p>
|
|
5713
5727
|
<div class="table-wrap">
|
|
5714
5728
|
<table>
|
|
@@ -5717,7 +5731,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
|
|
|
5717
5731
|
<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>
|
|
5718
5732
|
<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>
|
|
5719
5733
|
<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>
|
|
5720
|
-
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or re-
|
|
5734
|
+
<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>
|
|
5721
5735
|
<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>
|
|
5722
5736
|
<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>
|
|
5723
5737
|
<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>
|
|
@@ -5984,7 +5998,7 @@ tabs.destroy();
|
|
|
5984
5998
|
|
|
5985
5999
|
<h2 id="layout">The dashboard layout</h2>
|
|
5986
6000
|
<p><code>modules/layout</code> is an opt-in <strong>reconfigurable dashboard surface</strong>: a cell grid inside an element, and a set of windows placed on it that a user can move, resize and close — by drag <em>or</em> by keyboard. It is the thing a customer would otherwise reach for GridStack or react-grid-layout to get, which means a second dependency, a second sizing model, and a seam where the viewers in it do not resize properly.</p>
|
|
5987
|
-
<p><strong>It is payload-agnostic, and that is the whole design.</strong> A window body is a <code>div</code> with an <code>id</code>. The module creates it, sizes it, and never reads or writes its contents — it does not import <code>createGrid</code>, does not know what a payload is, and never calls into one. What it does instead is emit <code>window:resized</code> with the measured content box, which is the contract. That rule is what keeps its own code to <strong>
|
|
6001
|
+
<p><strong>It is payload-agnostic, and that is the whole design.</strong> A window body is a <code>div</code> with an <code>id</code>. The module creates it, sizes it, and never reads or writes its contents — it does not import <code>createGrid</code>, does not know what a payload is, and never calls into one. What it does instead is emit <code>window:resized</code> with the measured content box, which is the contract. That rule is what keeps its own code to <strong>12,890 bytes gzipped</strong> (measured: a 77,190-byte bundle over a 62,206-byte fixed floor, of which 2,094 bytes are the two shared module helpers) and what makes it usable for a payload we have not written yet.</p>
|
|
5988
6002
|
<pre><code>import { createLayout } from '@toclocoinc/lattice-grid/modules/layout';
|
|
5989
6003
|
|
|
5990
6004
|
const layout = createLayout(document.querySelector('#dash'), {
|
|
@@ -6006,7 +6020,7 @@ createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });</code><
|
|
|
6006
6020
|
<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>
|
|
6007
6021
|
<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>
|
|
6008
6022
|
<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>
|
|
6009
|
-
<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
|
|
6023
|
+
<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>
|
|
6010
6024
|
<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>
|
|
6011
6025
|
<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>
|
|
6012
6026
|
<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>
|
|
@@ -6015,12 +6029,14 @@ createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });</code><
|
|
|
6015
6029
|
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
6016
6030
|
<tbody>
|
|
6017
6031
|
<tr><td class="sig">createLayout(el, config)</td><td class="desc">Create a dashboard layout. <code>columns</code>/<code>rows</code> (default 12/6) divide the element; <code>overflowX</code>/<code>overflowY</code> are each <code>'static'</code> or <code>'scroll'</code>; <code>columnWidth</code>/<code>rowHeight</code> are the fixed track sizes a scrolling axis uses; <code>gap</code> (8px), <code>padding</code> (5px) and <code>compact</code> (<code>'vertical'</code>) complete it. A second mount on the same element is refused by name.</td></tr>
|
|
6018
|
-
<tr><td class="sig">config.windows[]</td><td class="desc">Each window: <code>id</code> (required, unique), <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code> in 1-based cells (auto-placed in the first free cell when omitted), <code>title</code>, <code>chrome</code> (default <code>true</code>), and <code>closable</code>/<code>movable</code>/<code>resizable</code> (all default <code>false</code>, so a dashboard the developer wants fixed is fixed without opting out of anything; each also takes a layout-level default of the same name, which a window's own boolean overrides). <code>padding</code> and <code>payloadId</code> (default <code>`${id}-body`</code>) override per window.</td></tr>
|
|
6032
|
+
<tr><td class="sig">config.windows[]</td><td class="desc">Each window: <code>id</code> (required, unique), <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code> in 1-based cells (auto-placed in the first free cell when omitted), <code>title</code>, <code>chrome</code> (default <code>true</code>), and <code>closable</code>/<code>movable</code>/<code>resizable</code>/<code>maximisable</code>/<code>minimisable</code> (all default <code>false</code>, so a dashboard the developer wants fixed is fixed without opting out of anything; each also takes a layout-level default of the same name, which a window's own boolean overrides). <code>padding</code> and <code>payloadId</code> (default <code>`${id}-body`</code>) override per window.</td></tr>
|
|
6019
6033
|
<tr><td class="sig">payload(id) / window(id) / windows()</td><td class="desc">The payload container for a window — the <code>div</code> carrying its <code>payloadId</code>, which you fill; a copy of a window's current descriptor; every window id in mount order.</td></tr>
|
|
6020
6034
|
<tr><td class="sig">add(spec) / move(id, to) / close(id)</td><td class="desc">Add a window after mount (returns its payload container); move or resize one through the same before-events the drag uses; close one through <code>beforeWindowClose</code>. <code>move</code> and <code>close</code> return <code>true</code>/<code>false</code> synchronously with no handler registered, or a <code>Promise<boolean></code> when a handler deferred.</td></tr>
|
|
6021
6035
|
<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>
|
|
6022
6036
|
<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>
|
|
6023
|
-
<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.</td></tr>
|
|
6037
|
+
<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>
|
|
6038
|
+
<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>
|
|
6039
|
+
<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>
|
|
6024
6040
|
<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>
|
|
6025
6041
|
<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>
|
|
6026
6042
|
<tr><td class="sig">destroy()</td><td class="desc">Stop observing, drop every listener including any left by a gesture in flight, and remove the DOM the module built. Whatever you mounted in a payload is yours to destroy.</td></tr>
|
|
@@ -6216,18 +6232,25 @@ createGrid(el, {
|
|
|
6216
6232
|
|
|
6217
6233
|
<p>The same option is accepted <strong>on a column definition</strong>, so a column's menu is
|
|
6218
6234
|
declared where the column is rather than as one more branch inside a single grid-level callback.
|
|
6219
|
-
It takes the same shapes plus a bare array for the common “
|
|
6235
|
+
It takes the same shapes plus a bare array for the common “these items here too”
|
|
6220
6236
|
case: <code>boolean | MenuItem[] | (params, defaults) => items</code>.</p>
|
|
6221
6237
|
<pre><code>createGrid(el, {
|
|
6222
6238
|
columns: [
|
|
6223
|
-
{ field: 'owner', contextMenu: [{ name: 'Reassign', action: reassign }] },
|
|
6239
|
+
{ field: 'owner', contextMenu: [{ name: 'Reassign', action: reassign }] }, <span class="cmt">// appended after the grid's items</span>
|
|
6224
6240
|
{ field: 'amount', contextMenu: (p, defaults) => [...defaults, { name: 'Reprice', action: reprice }] },
|
|
6241
|
+
{ field: 'ref', contextMenu: () => [{ name: 'Copy reference', action: copyRef }] }, <span class="cmt">// only this: ignore defaults</span>
|
|
6225
6242
|
{ field: 'nationalId', contextMenu: <span class="kw">false</span> }, <span class="cmt">// no menu on this column, others unaffected</span>
|
|
6226
6243
|
],
|
|
6227
6244
|
});</code></pre>
|
|
6228
6245
|
<p>The three levels <strong>compose as a chain</strong>: built-in defaults, then the grid-level
|
|
6229
6246
|
<code>contextMenu</code>, then the column's — each handed the previous result as its
|
|
6230
|
-
<code>defaults</code>, so a column adding one item never restates the built-ins.
|
|
6247
|
+
<code>defaults</code>, so a column adding one item never restates the built-ins. <strong>The
|
|
6248
|
+
array form chains too:</strong> <code>contextMenu: [items]</code> on a column is exactly
|
|
6249
|
+
<code>contextMenu: (p, defaults) => [...defaults, ...items]</code>, so the built-ins and every
|
|
6250
|
+
grid-level item stay and the column's items follow them, in the order written. To
|
|
6251
|
+
<strong>replace</strong> a column's menu instead, use the function form and ignore
|
|
6252
|
+
<code>defaults</code>: <code>contextMenu: () => items</code>. (Earlier releases let an array replace the grid-level
|
|
6253
|
+
items; the two forms now agree.) Suppression
|
|
6231
6254
|
follows the same order and the more specific level wins: <code>false</code> on a column is a
|
|
6232
6255
|
statement about that column alone. <strong>The reverse holds too, and is worth knowing before you
|
|
6233
6256
|
rely on grid-level <code>contextMenu: false</code> as a safety property: a column that declares
|
|
@@ -6241,6 +6264,21 @@ createGrid(el, {
|
|
|
6241
6264
|
the <kbd>Context Menu</kbd> key) honour the column exactly as the pointer does. See
|
|
6242
6265
|
<a href="api-detail.html#per-column-menu">the guide</a> for the full table of combinations.</p>
|
|
6243
6266
|
|
|
6267
|
+
<p><strong>The empty tail of a row is the row's.</strong> When the columns do not fill the
|
|
6268
|
+
grid's width, each row has an empty area to the right of the last column. A right-click there
|
|
6269
|
+
opens the grid's menu for that row, never the browser's, exactly as a right-click on a group row
|
|
6270
|
+
does: there is no column under the pointer, so the column link is missing from the chain and the
|
|
6271
|
+
grid-level menu stands, and <code>cell:contextmenu</code> (and so a builder's <code>params</code>)
|
|
6272
|
+
carries <code>colId: null</code>, <code>column: undefined</code> and <code>value: undefined</code>
|
|
6273
|
+
with the row, <code>key</code> and <code>index</code> filled in. The built-in items that act on
|
|
6274
|
+
a cell — Paste, Clear, Fill down, Edit cell — are not offered there (there is no cell
|
|
6275
|
+
for them to act on); the row and grid items are. A builder that reads
|
|
6276
|
+
<code>params.column</code> should expect it to be absent there. The area below the last row
|
|
6277
|
+
belongs to no row and keeps the browser's menu. <em>On 1.54 and earlier</em> a right-click in the
|
|
6278
|
+
tail fell through to the browser's menu, which looked as though the grid had none; on those
|
|
6279
|
+
versions give one column <code>layout: { flex: 1 }</code> so the cells reach the edge and there
|
|
6280
|
+
is no tail to click.</p>
|
|
6281
|
+
|
|
6244
6282
|
<p><code>columnMenu</code> takes the same form for the header's menu: both the 3-dot button
|
|
6245
6283
|
and a right-click on a heading. Its <code>params</code> is
|
|
6246
6284
|
<code>{ colId, column, grid }</code>. Anything of your own that you put on a column definition
|
|
@@ -6296,6 +6334,8 @@ createGrid(el, {
|
|
|
6296
6334
|
window, so a reader on row 500,000 is told so; and rows in a hierarchy carry their position among
|
|
6297
6335
|
their siblings, which a reader cannot count for itself when most of a branch was never rendered.</p>
|
|
6298
6336
|
<p>Focus is real focus rather than <code>aria-activedescendant</code>, and survives row recycling.
|
|
6337
|
+
Tabbing into a grid shows a focus ring around the grid at once; the first arrow key moves focus,
|
|
6338
|
+
and the ring, to a cell, and from then on Tab returns to that cell.
|
|
6299
6339
|
Sorting, filtering, selection, grouping, expanding, paging, undo, paste and a refused edit are all
|
|
6300
6340
|
announced. In Windows High Contrast Mode state is translated into borders and system colours
|
|
6301
6341
|
instead of tints. No information is carried by hue alone.</p>
|
|
@@ -6442,6 +6482,30 @@ createGrid(el, {
|
|
|
6442
6482
|
grid.destroy();
|
|
6443
6483
|
<span class="kw">return</span> n;</code></pre>
|
|
6444
6484
|
|
|
6485
|
+
<h3 id="direction-example">Writing direction and the two alignment vocabularies, executed</h3>
|
|
6486
|
+
<p class="section-note"><code>direction</code> is a recognised configuration key, and a column's resolved
|
|
6487
|
+
<code>align</code> keeps the spelling it was given: <code>left</code>/<code>right</code> are physical edges,
|
|
6488
|
+
<code>start</code>/<code>end</code> are logical and mirror in a right-to-left grid. A number column with no
|
|
6489
|
+
<code>align</code> of its own still defaults to the logical <code>end</code>.</p>
|
|
6490
|
+
<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');
|
|
6491
|
+
|
|
6492
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
6493
|
+
direction: 'rtl',
|
|
6494
|
+
rowKey: 'id',
|
|
6495
|
+
columns: [
|
|
6496
|
+
{ id: 'l', field: 'l', align: 'left' }, <span class="cmt">// physical: the left edge in either direction</span>
|
|
6497
|
+
{ id: 'r', field: 'r', align: 'right' }, <span class="cmt">// physical: the right edge in either direction</span>
|
|
6498
|
+
{ id: 's', field: 's', align: 'start' }, <span class="cmt">// logical: the right edge in this RTL grid</span>
|
|
6499
|
+
{ id: 'e', field: 'e', align: 'end' }, <span class="cmt">// logical: the left edge in this RTL grid</span>
|
|
6500
|
+
{ id: 'n', field: 'n', type: 'number' }, <span class="cmt">// a number column defaults to the logical end</span>
|
|
6501
|
+
],
|
|
6502
|
+
rows: [{ id: '1', l: 'a', r: 'b', s: 'c', e: 'd', n: 1 }],
|
|
6503
|
+
});
|
|
6504
|
+
<span class="kw">const</span> resolved = ['l', 'r', 's', 'e', 'n'].map((id) => grid.columns.get(id).align);
|
|
6505
|
+
<span class="kw">const</span> out = [grid.config().direction, ...resolved].join('|');
|
|
6506
|
+
grid.destroy();
|
|
6507
|
+
<span class="kw">return</span> out;</code></pre>
|
|
6508
|
+
|
|
6445
6509
|
<h3 id="ingest-worker-example">Non-blocking stream ingest, executed</h3>
|
|
6446
6510
|
<p class="section-note">A <code>stream</code> source loaded with <code>ingest.useWorker</code> on. In a browser a
|
|
6447
6511
|
chunk that clears <code>ingest.workerThreshold</code> is columnized on a Worker so the main thread
|
|
@@ -7370,11 +7434,11 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
7370
7434
|
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
7371
7435
|
<tbody>
|
|
7372
7436
|
<tr><td class="name">key</td><td class="type">string</td><td class="desc"></td></tr>
|
|
7373
|
-
<tr><td class="name">colId</td><td class="type">string</td><td class="desc"
|
|
7374
|
-
<tr><td class="name">value</td><td class="type">unknown</td><td class="desc"
|
|
7437
|
+
<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>
|
|
7438
|
+
<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>
|
|
7375
7439
|
<tr><td class="name">row</td><td class="type">Row</td><td class="desc">The row wrapper.</td></tr>
|
|
7376
7440
|
<tr><td class="name">data</td><td class="type">unknown</td><td class="desc">Your original row object.</td></tr>
|
|
7377
|
-
<tr><td class="name">column</td><td class="type">ResolvedColumn</td><td class="desc"
|
|
7441
|
+
<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>
|
|
7378
7442
|
<tr><td class="name">index</td><td class="type">number</td><td class="desc"></td></tr>
|
|
7379
7443
|
<tr><td class="name">grid</td><td class="type">Grid</td><td class="desc"></td></tr>
|
|
7380
7444
|
</tbody>
|
|
@@ -7894,7 +7958,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
7894
7958
|
<tr><td class="name">resize</td><td class="type">(id: string, px: number): void</td><td class="desc"></td></tr>
|
|
7895
7959
|
<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>
|
|
7896
7960
|
<tr><td class="name">autoSize</td><td class="type">(ids?: string | string[]): void</td><td class="desc"></td></tr>
|
|
7897
|
-
<tr><td class="name">fit</td><td class="type">(): void</td><td class="desc"
|
|
7961
|
+
<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>
|
|
7898
7962
|
<tr><td class="name">group</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
|
|
7899
7963
|
<tr><td class="name">pivot</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
|
|
7900
7964
|
<tr><td class="name">totals</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
|
|
@@ -9008,6 +9072,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9008
9072
|
<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>
|
|
9009
9073
|
<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>
|
|
9010
9074
|
<tr><td class="name">locale</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9075
|
+
<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>
|
|
9011
9076
|
<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>
|
|
9012
9077
|
<tr><td class="name">theme</td><td class="type">Theme</td><td class="desc">The visual theme. <small>(optional)</small></td></tr>
|
|
9013
9078
|
<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>
|
|
@@ -9550,7 +9615,6 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9550
9615
|
<tr><td class="name">nullDisplay</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9551
9616
|
<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>
|
|
9552
9617
|
<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>
|
|
9553
|
-
<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>
|
|
9554
9618
|
<tr><td class="name">scale</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9555
9619
|
</tbody>
|
|
9556
9620
|
</table>
|
|
@@ -10779,7 +10843,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
10779
10843
|
<!-- END GENERATED TYPE REFERENCE -->
|
|
10780
10844
|
|
|
10781
10845
|
<footer>
|
|
10782
|
-
Lattice Grid 1.
|
|
10846
|
+
Lattice Grid 1.55.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
10783
10847
|
This document describes the behaviour of the shipped library. Where this guide and the code
|
|
10784
10848
|
disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
10785
10849
|
</footer>
|