@toclocoinc/lattice-grid 1.48.0 → 1.50.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 +2 -1
- package/docs/API.html +75 -3
- package/docs/api-detail.html +93 -2
- package/lattice-grid.d.ts +170 -1
- package/lattice-grid.esm.min.js +294 -27
- package/lattice-grid.min.cjs +294 -27
- package/lattice-grid.min.js +294 -27
- package/modules/ai.esm.min.js +6 -4
- package/modules/ai.min.cjs +6 -4
- package/modules/ai.min.js +6 -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 +1299 -1231
- package/modules/charts.min.cjs +1299 -1231
- package/modules/charts.min.js +1299 -1231
- 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 +78 -4
- package/modules/gantt.min.cjs +77 -4
- package/modules/gantt.min.js +77 -4
- package/modules/htmx.esm.min.js +294 -27
- package/modules/htmx.min.cjs +294 -27
- package/modules/htmx.min.js +294 -27
- package/modules/kanban.esm.min.js +4 -4
- package/modules/kanban.min.cjs +4 -4
- package/modules/kanban.min.js +4 -4
- package/modules/kpi.esm.min.js +4 -4
- package/modules/kpi.min.cjs +4 -4
- package/modules/kpi.min.js +4 -4
- 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 +870 -0
- package/modules/tabs.min.cjs +873 -0
- package/modules/tabs.min.js +873 -0
- 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 +294 -27
- package/modules/webcomponent.min.cjs +294 -27
- package/modules/webcomponent.min.js +294 -27
- 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.50.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -349,6 +349,7 @@ the UMD build or `.cjs` for CommonJS.
|
|
|
349
349
|
| kanban | `@toclocoinc/lattice-grid/modules/kanban` | `modules/kanban.min.js` | `LatticeGridKanban` | Board view: grid rows as cards grouped into columns (`createKanban`). |
|
|
350
350
|
| gantt | `@toclocoinc/lattice-grid/modules/gantt` | `modules/gantt.min.js` | `LatticeGridGantt` | Editable, dependency-aware project plan with a computed critical path (`createGantt`). |
|
|
351
351
|
| kpi | `@toclocoinc/lattice-grid/modules/kpi` | `modules/kpi.min.js` | `LatticeGridKPI` | A grid of stat tiles, each an aggregate over a dataset (`createKPI`). |
|
|
352
|
+
| tabs | `@toclocoinc/lattice-grid/modules/tabs` | `modules/tabs.min.js` | `LatticeGridTabs` | A tab strip where each tab is its own full grid, optionally derived from another (`createTabs`). |
|
|
352
353
|
| ai | `@toclocoinc/lattice-grid/modules/ai` | `modules/ai.min.js` | `LatticeGridAI` | Bring-your-own-model narrative and insights grounded on computed figures (`createAI`). |
|
|
353
354
|
| mock-socket | `@toclocoinc/lattice-grid/modules/mock-socket` | `modules/mock-socket.min.js` | `LatticeGridMockSocket` | A serverless stand-in for a live WebSocket feed (`MockWebSocket`, `opsFeed`). |
|
|
354
355
|
| devtools | `@toclocoinc/lattice-grid/modules/devtools` | `modules/devtools.min.js` | `LatticeGrid` (extends it) | The in-page diagnostic panel, including the accessibility checks (`createDevtools`). |
|
package/docs/API.html
CHANGED
|
@@ -2129,7 +2129,7 @@ createGrid(el, {
|
|
|
2129
2129
|
<tr><td class="sig">delta</td><td class="desc">Current value minus the baseline.</td></tr>
|
|
2130
2130
|
<tr><td class="sig">deltaPercent</td><td class="desc">The same as a percentage. A change from nothing has no percentage and reads null rather than infinity.</td></tr>
|
|
2131
2131
|
<tr><td class="sig">rate</td><td class="desc">Change per second, from the last two readings.</td></tr>
|
|
2132
|
-
<tr><td class="sig">history</td><td class="desc">The recent readings, oldest first. <code>depth</code> sets how many; the default is 20.</td></tr>
|
|
2132
|
+
<tr><td class="sig">history</td><td class="desc">The recent readings, oldest first. <code>depth</code> sets how many; the default is 20. Counts <em>changes</em> by default — see the time-windowed form below for a real time series.</td></tr>
|
|
2133
2133
|
<tr><td class="sig">firstValue</td><td class="desc">The baseline itself.</td></tr>
|
|
2134
2134
|
<tr><td class="sig">streak</td><td class="desc">Consecutive moves in one direction, signed. It resets on a turn, because "seven rises" means something and "seven changes" does not.</td></tr>
|
|
2135
2135
|
</tbody>
|
|
@@ -2164,6 +2164,10 @@ createGrid(el, {
|
|
|
2164
2164
|
]</code></pre>
|
|
2165
2165
|
<p>Computed in one pass over the display rows and cached against that ordering, so a hundred thousand rows are walked once per sort rather than once per cell. A running column is not sortable. Sorting on one asks the sort to depend on its own output (the value is defined by the display order) so the column does not offer a sort unless its definition asks for one, and the query layer refuses such a sort with a warning rather than computing it. Sort by the column it runs over instead. Group headings and totals rows are skipped, a running total that counted a subtotal would double everything below it, and a row with no value carries the figure forward unchanged rather than resetting it.</p>
|
|
2166
2166
|
|
|
2167
|
+
<h4 id="history-window">History has two clocks: a count of changes, or a span of time (BACKLOG-0001043)</h4>
|
|
2168
|
+
<p>Plain <code>history</code> (above) is an <em>event</em> series: it records a reading only when the value changes, so a row that sits still never advances it. Bound to a <code>cell.render</code> sparkline (<code>'line'</code>, <code>'area'</code>, <code>'column'</code>, <code>'winloss'</code>) that means a static row's sparkline freezes, then jumps when a change finally lands — while a title like "last 60s" keeps claiming a span the column never measured. Add a <em>time</em> <code>window</code> to make it a real time series instead:</p>
|
|
2169
|
+
<pre><code>{ id: 'spark', title: 'Last 60s', shadow: { of: 'price', kind: 'history', window: { kind: 'time', span: 60_000 }, depth: 20 } }</code></pre>
|
|
2170
|
+
<p>This is the same <code>window: { kind, span }</code> shape the rolling kinds below already accept — not a second spelling of it — and it changes what <code>history</code> means rather than adding a new kind: <code>depth</code> (20 here) is now how many buckets the <code>span</code> divides into, so this is sixty seconds as twenty three-second buckets. Each bucket is sampled once, at its close, as the row's <em>last known value</em> at that moment — carried forward from the previous bucket when nothing changed in between. A static row therefore draws a flat line that keeps advancing, one sample per bucket, and a change lands in the bucket it actually happened in rather than being appended at the end. The bucket clock runs on its own low-frequency timer (never a render loop), so the series keeps moving even while no data event ever reaches the column. <code>window: { kind: 'count' }</code> and <code>{ kind: 'session' }</code> are refused for <code>history</code> — a plain count is already what <code>depth</code> means without a window, and a session has no fixed span to divide into buckets — and a caller who never sets <code>window</code> gets the original count-based behaviour, unchanged.</p>
|
|
2167
2171
|
<div class="note"><p>Positional kinds rank over every <em>tracked</em> row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. Pass <code>scope: 'filtered'</code> on the shadow spec to rank within what the filters left instead: both answers are legitimate, which is why it is a choice rather than a default.</p></div>
|
|
2168
2172
|
<div class="note"><p>Shadow state is keyed by row key, never by index: after any sort an index-keyed history would report one row's past against another row's present, and the wrong number would be <em>sortable</em>. Memory is capped at 200,000 tracked rows per column; past that the oldest are dropped and <code>tracking().forgotten</code> says how many, rather than a smaller number being reported as though it were the truth.</p></div>
|
|
2169
2173
|
|
|
@@ -5044,6 +5048,7 @@ plan.applyEdit({ id: 'design', duration: 7 }); // recomputes; the critical path
|
|
|
5044
5048
|
<tr><td class="sig">importMSPDI(xml, { hoursPerDay? })</td><td class="desc">Import a Microsoft Project (MSPDI) <code>.xml</code> document into a <code>{ tasks, dependencies, resources, projectStart, calendar }</code> model ready for <code>createGantt</code>: the task tree, typed dependencies with lag, constraints, baseline, %complete, resources with capacity and the resource assignments.</td></tr>
|
|
5045
5049
|
<tr><td class="sig">exportMSPDI(model, { hoursPerDay?, projectName? })</td><td class="desc">Serialise a Gantt model (optionally a scheduled one) back to Microsoft Project (MSPDI) XML — the same fields, round-tripping with <code>importMSPDI</code>. The controller offers <code>gantt.toMSPDI()</code> as a shortcut over the current plan.</td></tr>
|
|
5046
5050
|
<tr><td class="sig">computeEarnedValue(tasks, schedule, { statusDate?, costField?, actualCostField? })</td><td class="desc">Earned-value management (EVM) from the baseline and %complete at a status date: Planned Value (PV/BCWS), Earned Value (EV/BCWP), Actual Cost (AC/ACWP, from a per-task <code>actualCost</code>), plus Schedule Variance (EV−PV), Cost Variance (EV−AC), SPI (EV/PV) and CPI (EV/AC) — per task, rolled up to summaries and the project. Budget (BAC) is the task's <code>cost</code>, or its duration when no cost is given. The controller exposes <code>gantt.earnedValue({ statusDate })</code> as the shortcut; the split view surfaces the metrics through <code>kind: 'evm'</code> columns.</td></tr>
|
|
5051
|
+
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">Serialise and restore the Gantt's own VIEWER state (BACKLOG-0001042) — which view is mounted (<code>'plain'</code>/<code>'split'</code>/<code>null</code>), zoom, the arrow/progress/baseline toggles, calendar/non-working shading, the plain view's swimlane grouping, the split view's left-panel width and its collapsed summary rows. Not the task data (already covered by <code>setTasks</code>/<code>rows.apply</code>). Versioned, JSON-safe, and <code>setState</code> tolerates an unknown, wrong-typed or newer-version snapshot without throwing, applying only what it recognises.</td></tr>
|
|
5047
5052
|
</tbody>
|
|
5048
5053
|
</table>
|
|
5049
5054
|
</div>
|
|
@@ -5526,6 +5531,67 @@ grid.destroy();
|
|
|
5526
5531
|
<span class="cmt">// Two incomplete tasks (Build, Ship) are on the critical path — at risk.</span>
|
|
5527
5532
|
<span class="kw">return</span> `${by['risk.atRisk'].display} at risk | SPI ${by['risk.spi'].display} | ${by['risk.sla.breaches'].display} breaches`;</code></pre>
|
|
5528
5533
|
|
|
5534
|
+
<h2 id="tabs">The tabbed grid</h2>
|
|
5535
|
+
<p><code>modules/tabs</code> is an opt-in top-of-grid tab strip where <strong>each tab is its own full, independently-configured grid instance</strong> — "configure each tab as per a normal grid" rather than one grid whose state is swapped. That is a deliberate rejection of the cheaper alternative: <code>grid.state.get()</code>/<code>.apply()</code> only repositions, hides, resizes and sorts <strong>existing</strong> columns by id (no field, type, editor or row data), so a state-swap only works when every tab shares one column schema and one source — strictly less than the ask. A tab may instead declare <code>from: '<tabId>'</code> plus a narrowing (<code>where</code>, <code>group</code>, <code>join</code>, …), and the module wires a <code>source: { mode: 'derived', from: <the parent tab's live grid>, … }</code> for it — the shipped derived-source mechanism, not a new config-inheritance one. <code>createGrid</code> is <strong>injected</strong> (the same pattern the React/Vue/Svelte adapters use), so the module imports no engine code regardless of how it is loaded — its own minified ESM build (<code>tabs.esm.min.js</code>) is ~65KB gzipped, the same size class as the KPI and framework-adapter modules (~61–66KB), rather than the ~700KB a module that inlines the whole engine (the web component, htmx) ships.</p>
|
|
5536
|
+
<pre><code>import { createGrid } from '@toclocoinc/lattice-grid';
|
|
5537
|
+
import { createTabs } from '@toclocoinc/lattice-grid/modules/tabs';
|
|
5538
|
+
|
|
5539
|
+
const tabs = createTabs(document.querySelector('#tabs'), {
|
|
5540
|
+
createGrid, <span class="cmt">// injected -- see below</span>
|
|
5541
|
+
tabs: [
|
|
5542
|
+
{ id: 'all', label: 'All', config: { rowKey: 'id', rows, columns } },
|
|
5543
|
+
{ id: 'open', label: 'Open', from: 'all', where: (r) => r.stage === 'Open',
|
|
5544
|
+
follow: 'filtered', refresh: 'live', config: { columns } },
|
|
5545
|
+
{ id: 'breached', label: 'Breached', from: 'open', <span class="cmt">// derives from Open, not All -- a chain</span>
|
|
5546
|
+
where: (r) => r.daysOverdue > 0, config: { columns } },
|
|
5547
|
+
],
|
|
5548
|
+
onBeforeTabChange: ({ id }) => !hasUnsavedEdit(), <span class="cmt">// veto a switch</span>
|
|
5549
|
+
});</code></pre>
|
|
5550
|
+
<p><strong>Lifecycle.</strong> A tab's grid mounts on <em>first activation</em>, not up front, and then stays alive — hidden, never destroyed — until the whole strip is. Per-tab scroll, selection, filters, sort, grouping, expansion — and an open cell/row editor — therefore survive a switch away and back natively, by simply not touching that grid instance, rather than through a lossy serialise/restore round-trip: leave a tab mid-edit, switch away, switch back, and the editor is exactly as it was left, uncommitted and undiscarded. Activating a derived tab materialises its whole ancestor chain first (mounted, hidden), and a cyclic <code>from</code> graph is refused — naming the exact cycle — when <code>createTabs</code> is called, not at first click.</p>
|
|
5551
|
+
<p><strong>A hidden tab costs nothing this module can spend.</strong> An inactive panel carries the <code>hidden</code> attribute (<code>display:none</code>); the module runs no timer, observer or repaint of its own against it. Measured with a real browser (<code>bench/tabs-idle.mjs</code>): several mounted-but-hidden, untouched tabs cost the same idle CPU as none at all. The one honestly-reported exception is not this module's: a <em>derived</em> tab's row model still re-derives on every change to its (possibly hidden) parent — by design, so reactivating it is instant rather than a stale flash — and the engine's own repaint listener for a derived source's <code>rows:changed</code> calls the renderer directly, bypassing <code>grid.updates.pause()</code> (which only holds the streaming-ingestion path). A hidden derived tab therefore still runs a read/compute/write pass on every parent change, even though the write phase paints a zero-size viewport; the bench measures and reports the size of that gap rather than leaving it inferred.</p>
|
|
5552
|
+
<p><strong>Accessibility.</strong> A real <code>role="tablist"</code>/<code>"tab"</code>/<code>"tabpanel"</code> with <code>aria-selected</code> and a roving <code>tabindex</code>, imitating the grid's own column-header keyboard model rather than the tool panel's tablist (which has the roles but no arrow-key handling). This is <strong>manual activation</strong>: <kbd>←</kbd>/<kbd>→</kbd> and <kbd>Home</kbd>/<kbd>End</kbd> move the roving tab stop without switching the panel or mounting a grid; <kbd>Enter</kbd>/<kbd>Space</kbd>, or a click, activates. The newly active tab's label is announced through a polite live region.</p>
|
|
5553
|
+
<div class="table-wrap">
|
|
5554
|
+
<table>
|
|
5555
|
+
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
5556
|
+
<tbody>
|
|
5557
|
+
<tr><td class="sig">createTabs(el, config)</td><td class="desc">Create a tabbed grid. <code>config.createGrid</code> is required (injected, not imported); <code>config.tabs</code> is a non-empty array of tab descriptors, each an <code>id</code>, a <code>label</code>, a grid <code>config</code>, and optionally <code>from</code> plus the derivation narrowing (<code>where</code>, <code>group</code>, <code>groupBy</code>, <code>bucket</code>, <code>join</code>, <code>unnest</code>, <code>refresh</code>, <code>crossFilter</code>, <code>follow</code>, <code>limit</code>, <code>sort</code>, <code>profile</code>) forwarded onto the derived source built for it.</td></tr>
|
|
5558
|
+
<tr><td class="sig">tabs() / tab(id) / isMounted(id)</td><td class="desc">The configured tab ids, in order; a tab's live grid instance (or <code>null</code> before its first activation); whether a tab has been materialised yet.</td></tr>
|
|
5559
|
+
<tr><td class="sig">activate(id, opts)</td><td class="desc">Switch the active tab, gated by <code>beforeTabChange</code>. Returns <code>true</code>/<code>false</code> synchronously with no handler registered, or a <code>Promise<boolean></code> when a handler deferred.</td></tr>
|
|
5560
|
+
<tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>tab:changed</code>, the cancellable <code>beforeTabChange</code> (call <code>preventDefault(reason?)</code> or return <code>false</code> to veto), and its paired <code>tabChange:cancelled</code>. Config sugar: <code>onTabChange</code>, <code>onBeforeTabChange</code>, <code>onTabChangeCancelled</code>.</td></tr>
|
|
5561
|
+
<tr><td class="sig">destroy()</td><td class="desc">Tear the whole strip down; destroys every mounted tab's grid (each isolated, so one throwing does not strand the rest).</td></tr>
|
|
5562
|
+
</tbody>
|
|
5563
|
+
</table>
|
|
5564
|
+
</div>
|
|
5565
|
+
<h3 id="tabs-live-example">All / Open / Breached, a two-deep derivation chain, executed</h3>
|
|
5566
|
+
<p class="section-note"><code>createTabs</code> deliberately has no headless mode — it requires a real host element, the same way <code>createGrid</code> itself does — so this executed example reaches for the same in-tree DOM test double the suite itself runs the renderer against headlessly (<code>packages/dom/src/renderer/testdom.js</code>), rather than a real browser. <code>demo/tabs.html</code> is the browser version of the same chain, with buttons that edit All directly and let Open and Breached follow.</p>
|
|
5567
|
+
<pre data-run="js" data-expect="4,3,2" data-covers="export:createTabs"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
|
|
5568
|
+
<span class="kw">const</span> { root } = createTestDom();
|
|
5569
|
+
<span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
|
|
5570
|
+
<span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
|
|
5571
|
+
|
|
5572
|
+
<span class="kw">const</span> rows = [
|
|
5573
|
+
{ id: 1, stage: 'Open', daysOverdue: 0 },
|
|
5574
|
+
{ id: 2, stage: 'Open', daysOverdue: 5 },
|
|
5575
|
+
{ id: 3, stage: 'Won', daysOverdue: 0 },
|
|
5576
|
+
{ id: 4, stage: 'Open', daysOverdue: 2 },
|
|
5577
|
+
];
|
|
5578
|
+
<span class="kw">const</span> columns = [{ field: 'id' }, { field: 'stage' }, { field: 'daysOverdue' }];
|
|
5579
|
+
|
|
5580
|
+
<span class="kw">const</span> tabs = createTabs(root, {
|
|
5581
|
+
createGrid,
|
|
5582
|
+
tabs: [
|
|
5583
|
+
{ id: 'all', label: 'All', config: { rowKey: 'id', rows, columns } },
|
|
5584
|
+
{ id: 'open', label: 'Open', from: 'all', where: (r) => r.stage === 'Open', config: { columns } },
|
|
5585
|
+
<span class="cmt">// derives from Open, not All -- a two-deep chain</span>
|
|
5586
|
+
{ id: 'breached', label: 'Breached', from: 'open', where: (r) => r.daysOverdue > 0, config: { columns } },
|
|
5587
|
+
],
|
|
5588
|
+
});
|
|
5589
|
+
|
|
5590
|
+
tabs.activate('breached'); <span class="cmt">// materialises 'open' too, automatically</span>
|
|
5591
|
+
<span class="kw">const</span> counts = ['all', 'open', 'breached'].map((id) => tabs.tab(id).rows.count());
|
|
5592
|
+
tabs.destroy();
|
|
5593
|
+
<span class="kw">return</span> counts.join(','); <span class="cmt">// 4,3,2</span></code></pre>
|
|
5594
|
+
|
|
5529
5595
|
<h2 id="mocksocket">The mock socket</h2>
|
|
5530
5596
|
<p><code>modules/mock-socket</code> is a serverless stand-in for a live <code>WebSocket</code> feed, for building and demonstrating a real-time UI with <strong>no backend</strong>. <code>MockWebSocket</code> presents the same surface as the browser's <code>WebSocket</code> — the same <code>readyState</code> and state constants, the same <code>onopen</code>, <code>onmessage</code>, <code>onclose</code> and <code>onerror</code>, <code>addEventListener</code>, <code>send</code> and <code>close</code> — so the code that reads it does not change when it is swapped for a real one. It fires an initial snapshot the moment it opens, then a stream of deltas on a timer, all from a generator you hand it. It is a dev and test utility: optional, imports nothing from the grid, and is never pulled into the core bundle. It pairs naturally with the data router (one mock stream, partitioned to many grids), but depends on it no more than a real socket does.</p>
|
|
5531
5597
|
<pre><code>import { MockWebSocket, opsFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
|
|
@@ -6168,7 +6234,7 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
|
|
|
6168
6234
|
<h3 id="nested-config-example">Nested configuration, executed</h3>
|
|
6169
6235
|
<p class="section-note">Thirteen option blocks, each key written where it belongs. Parsed and evaluated on
|
|
6170
6236
|
every build, so a key that was renamed or moved shows up here.</p>
|
|
6171
|
-
<pre data-run="js" data-expect="14" data-covers="config:aboveLimit config:adapter 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: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>
|
|
6237
|
+
<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>
|
|
6172
6238
|
<span class="cmt">// the option names: each one below is a documented key, written where it</span>
|
|
6173
6239
|
<span class="cmt">// belongs, so a key that was renamed or moved stops matching its interface.</span>
|
|
6174
6240
|
<span class="kw">const</span> source = {}, other = {}, provider = {}, compute = {}, adapter = {};
|
|
@@ -6209,7 +6275,10 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
|
|
|
6209
6275
|
<span class="kw">const</span> pagedSourceConfig = { mode: 'paged', pageSize: 100, maxCachedPages: 5, fetch };
|
|
6210
6276
|
|
|
6211
6277
|
<span class="cmt">// A streaming source — StreamSourceConfig</span>
|
|
6212
|
-
<span class="kw">const</span> streamSourceConfig = { mode: 'stream', open, coalesceMs: 16, maxRows: 1e6, promoteToMemoryBelow: 5e5
|
|
6278
|
+
<span class="kw">const</span> streamSourceConfig = { mode: 'stream', open, coalesceMs: 16, maxRows: 1e6, promoteToMemoryBelow: 5e5,
|
|
6279
|
+
<span class="cmt">// A rolling *time* window beside the count one: keep five minutes, aged by the</span>
|
|
6280
|
+
<span class="cmt">// row's own clock. Omit ageBy and rows age from when they arrived instead.</span>
|
|
6281
|
+
maxAge: 5 * 60 * 1000, ageBy: 'ts' };
|
|
6213
6282
|
|
|
6214
6283
|
<span class="cmt">// A pushdown source — PushdownSourceConfig</span>
|
|
6215
6284
|
<span class="kw">const</span> pushdownSourceConfig = { adapter, compute, pageSize: 200 };
|
|
@@ -6916,6 +6985,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
6916
6985
|
<tr><td class="name">labels</td><td class="type">boolean</td><td class="desc">Draw the tick labels. <small>(optional)</small></td></tr>
|
|
6917
6986
|
<tr><td class="name">every</td><td class="type">number</td><td class="desc">Show every nth category label, on a crowded category axis. <small>(optional)</small></td></tr>
|
|
6918
6987
|
<tr><td class="name">rotate</td><td class="type">boolean | 'auto'</td><td class="desc">Force the category labels' rotation rather than deciding it. <small>(optional)</small></td></tr>
|
|
6988
|
+
<tr><td class="name">window</td><td class="type">Pick<WindowSpec, 'kind' | 'span'></td><td class="desc">A rolling window for the axis domain (BACKLOG-0001036), in the shipped `WindowSpec` vocabulary that rolling statistics already use. Only `{ kind: 'time', span }` applies to an axis: the domain becomes the last `span` milliseconds ending **now**, so the chart keeps scrolling left while the feed is silent — the thing a count window cannot do, because with no rows arriving nothing changes. Advanced on a low-frequency clock (a quarter of the window, between 50 ms and 1 s), never per frame, and stopped when the chart is destroyed or its document is hidden. Needs a continuous x axis carrying wall-clock times; `{ kind: 'count' }` is the source's `maxRows` and is refused here rather than given a second meaning. <small>(optional)</small></td></tr>
|
|
6919
6989
|
</tbody>
|
|
6920
6990
|
</table>
|
|
6921
6991
|
</div>
|
|
@@ -9900,6 +9970,8 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9900
9970
|
<tr><td class="name">mode</td><td class="type">'stream'</td><td class="desc"></td></tr>
|
|
9901
9971
|
<tr><td class="name">open</td><td class="type">(req: {</td><td class="desc"></td></tr>
|
|
9902
9972
|
<tr><td class="name">maxRows</td><td class="type">number</td><td class="desc">The most rows to keep. A stream has no end, so an unbounded grid dies overnight; this makes it a sliding window and the oldest rows are dropped. Omit for no limit. Set on the source, not passed to `open`, it bounds what the grid retains rather than what the producer sends. <small>(optional)</small></td></tr>
|
|
9973
|
+
<tr><td class="name">maxAge</td><td class="type">number</td><td class="desc">The longest a row is kept, in milliseconds — a rolling *time* window, sitting beside `maxRows` as a second, independent bound (BACKLOG-0001036). Rows older than the span are evicted through the same path, the same `evicted` counters and the same `stream:evicted` event as the count bound, so an existing readout keeps working. Set both and whichever bites first applies. Eviction continues on a low-frequency timer while the feed is idle, so "the last five minutes" keeps shrinking through a silent period rather than freezing — which is the thing `maxRows` cannot do. Retention is a *bound, not a guillotine*: rows live a little past the span before a block is dropped. Two things add to it. First the eviction slack, ten per cent of the span, exactly as `maxRows` overshoots its count, so the row permutation is rebuilt once per block rather than once per row. Second, when the feed is idle, up to one tick of the eviction timer, which runs at a quarter of the span clamped to between 50 ms and one second. So the real ceiling is roughly `span * 1.1 + tick`, and because the tick has a floor it is proportionally larger the shorter the window: negligible at a five-minute window (about 10%), around 1.25x at ten seconds, and as much as ~1.35x at three. That is the deliberate trade for an idle grid that costs no CPU. Omit for no age limit. <small>(optional)</small></td></tr>
|
|
9974
|
+
<tr><td class="name">ageBy</td><td class="type">string | ((row: unknown) => unknown)</td><td class="desc">Which clock `maxAge` reads: a column id (or dotted path), or a function of the row returning a `Date`, epoch milliseconds, or an ISO string (BACKLOG-0001036). Given, the window follows the **data's own** clock, so it means what the producer means — and inherits the producer's clock skew. Omitted, `maxAge` falls back to **arrival time**: when the row reached this source. Arrival time needs no timestamp column and cannot be skewed, but it is not event time — a row delayed in transit counts as young. A row whose time value cannot be read is never aged out. <small>(optional)</small></td></tr>
|
|
9903
9975
|
<tr><td class="name">promoteToMemoryBelow</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9904
9976
|
<tr><td class="name">coalesceMs</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9905
9977
|
</tbody>
|
package/docs/api-detail.html
CHANGED
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
<div class="shell">
|
|
438
438
|
<aside class="rail">
|
|
439
439
|
<p class="rail__brand">Lattice Grid</p>
|
|
440
|
-
<p class="rail__sub">Developer guide · v1.
|
|
440
|
+
<p class="rail__sub">Developer guide · v1.50.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -4470,6 +4470,96 @@ grid.updates.log({ since: Date.now() - 60000 }); <span class="cmt">// what arr
|
|
|
4470
4470
|
left up overnight holds every row it was ever sent. Set <code>source.maxRows</code> and the
|
|
4471
4471
|
stream becomes a sliding window, dropping the oldest as new ones arrive and reporting how
|
|
4472
4472
|
many it let go through <code>evicted</code> on the progress report.</p>
|
|
4473
|
+
</div>
|
|
4474
|
+
|
|
4475
|
+
<h3 id="time-window-guide">A time window, not just a row count</h3>
|
|
4476
|
+
<p class="lead-in">
|
|
4477
|
+
<code>maxRows</code> is a <em>count</em> window. <code>maxAge</code> is a <em>time</em>
|
|
4478
|
+
window. They are different promises, and a live feed usually wants the second one.
|
|
4479
|
+
</p>
|
|
4480
|
+
<div class="example">
|
|
4481
|
+
<p class="example__label">Keep the last five minutes, and show the last five minutes</p>
|
|
4482
|
+
<pre><code>const grid = createGrid(el, {
|
|
4483
|
+
columns,
|
|
4484
|
+
source: {
|
|
4485
|
+
mode: 'stream',
|
|
4486
|
+
open,
|
|
4487
|
+
maxAge: 5 * 60 * 1000, <span class="cmt">// keep five minutes of rows</span>
|
|
4488
|
+
ageBy: 'ts', <span class="cmt">// ...aged by this column; omit for arrival time</span>
|
|
4489
|
+
maxRows: 20000, <span class="cmt">// ...and never more than this many, whichever bites first</span>
|
|
4490
|
+
},
|
|
4491
|
+
});
|
|
4492
|
+
|
|
4493
|
+
createChart({
|
|
4494
|
+
grid, container, type: 'line', x: 'ts', y: { col: 'value', fn: 'avg' },
|
|
4495
|
+
<span class="cmt">// The x domain is the last five minutes ending *now*, so the chart</span>
|
|
4496
|
+
<span class="cmt">// keeps scrolling left even while the feed is silent.</span>
|
|
4497
|
+
axis: { x: { window: { kind: 'time', span: 5 * 60 * 1000 } } },
|
|
4498
|
+
});</code></pre>
|
|
4499
|
+
</div>
|
|
4500
|
+
<div class="why">
|
|
4501
|
+
<p><strong>A count window drifts, and it freezes.</strong> <code>maxRows</code> equals
|
|
4502
|
+
“the last five minutes” only while the feed rate is steady: a burst silently
|
|
4503
|
+
shrinks the window to two minutes, a quiet spell stretches it to twenty, and the x axis
|
|
4504
|
+
changes span under the reader. Worse, when the feed goes quiet nothing is evicted and the
|
|
4505
|
+
chart stops moving, even though time is still passing — and the silence is usually the
|
|
4506
|
+
thing worth seeing. <code>maxAge</code> is a span of wall clock, so it means the same thing
|
|
4507
|
+
whatever the feed is doing.</p>
|
|
4508
|
+
<p><strong>Two bounds, one eviction path.</strong> <code>maxAge</code> and
|
|
4509
|
+
<code>maxRows</code> are independent and compose: both are applied on the same pass and
|
|
4510
|
+
whichever bites first is simply the one that drops rows. Neither is silently ignored.
|
|
4511
|
+
Age eviction reuses the count bound’s machinery outright, so <code>evicted</code> on
|
|
4512
|
+
the progress report and the <code>stream:evicted</code> event carry age evictions exactly as
|
|
4513
|
+
they always carried count evictions — an existing “dropped off the back of the
|
|
4514
|
+
window” readout keeps working with nothing changed.</p>
|
|
4515
|
+
<p><strong>The span is a bound, not a guillotine.</strong> A row lives a little past the span
|
|
4516
|
+
before it goes, and two things add to that. First the eviction slack, ten per cent of the
|
|
4517
|
+
span — exactly the overshoot <code>maxRows</code> already allows on its count — so
|
|
4518
|
+
the row permutation is rebuilt once per block rather than once per arriving row. Second, when
|
|
4519
|
+
the feed is idle, up to one tick of the eviction timer, which runs at a quarter of the span
|
|
4520
|
+
clamped to between 50 ms and one second. The real ceiling is therefore about
|
|
4521
|
+
<code>span × 1.1 + tick</code>, and because the tick has a floor it is
|
|
4522
|
+
<em>proportionally larger the shorter the window</em>: about 10% over at a five-minute
|
|
4523
|
+
window, around 1.25× at ten seconds, and as much as ~1.35× at three. That is the
|
|
4524
|
+
deliberate price of an idle grid that costs no CPU, and it is why the number to reach for is
|
|
4525
|
+
the window you want the reader to see rather than a hard retention limit. The chart’s
|
|
4526
|
+
domain is exact either way — it ends at <em>now</em> — so the extra rows sit off
|
|
4527
|
+
the left edge rather than being drawn.</p>
|
|
4528
|
+
<p><strong>Which clock: <code>ageBy</code>, or arrival.</strong> Given
|
|
4529
|
+
<code>ageBy</code> — a column id, a dotted path, or a function of the row returning a
|
|
4530
|
+
<code>Date</code>, epoch milliseconds or an ISO string — the window follows the
|
|
4531
|
+
<em>data’s own</em> clock, so it means what the producer means. That also inherits the
|
|
4532
|
+
producer’s clock skew: if their clock runs five minutes fast, their rows live five
|
|
4533
|
+
minutes longer than yours. Omit <code>ageBy</code> and rows age from <strong>arrival
|
|
4534
|
+
time</strong>, when the row reached the source. Arrival time needs no timestamp column and
|
|
4535
|
+
cannot be skewed, but it is not event time — a row delayed in transit is treated as
|
|
4536
|
+
young. A synthetic or metric feed usually wants arrival; a log or event feed usually wants
|
|
4537
|
+
<code>ageBy</code>. A row whose time value cannot be read is never aged out: dropping data
|
|
4538
|
+
because a timestamp was malformed is the worse failure.</p>
|
|
4539
|
+
<p><strong>Out of order is handled, not reordered.</strong> With <code>ageBy</code> the
|
|
4540
|
+
row’s clock need not be monotonic in arrival order, so eviction scans the window rather
|
|
4541
|
+
than walking the head; a late row that is already older than the span is dropped on the same
|
|
4542
|
+
pass it arrived on and counted as evicted, rather than being painted and then withdrawn a
|
|
4543
|
+
moment later. Nothing is re-sorted: a row’s <em>position</em> is still arrival order,
|
|
4544
|
+
only its <em>retention</em> is decided by its time.</p>
|
|
4545
|
+
<p><strong>The chart axis rolls independently.</strong> The axis takes the same
|
|
4546
|
+
<code>WindowSpec</code> vocabulary rolling statistics use —
|
|
4547
|
+
<code>window: { kind: 'time', span }</code> — and its domain ends at <em>now</em>
|
|
4548
|
+
rather than at the newest point, which is what makes the chart keep scrolling with zero new
|
|
4549
|
+
rows. It works with or without <code>maxAge</code> on the source; set both to the same span
|
|
4550
|
+
and the retained data and the drawn domain agree. Only <code>kind: 'time'</code> applies to
|
|
4551
|
+
an axis: a count window over a chart is the source’s <code>maxRows</code>, and
|
|
4552
|
+
<code>kind: 'count'</code> is refused with a warning rather than quietly given a second
|
|
4553
|
+
meaning. The x column has to be continuous and carry wall-clock times — a banded or
|
|
4554
|
+
categorical axis has no domain to roll.</p>
|
|
4555
|
+
<p><strong>Idle costs nothing.</strong> Both halves advance on a plain interval — a
|
|
4556
|
+
quarter of the window, clamped to between 50 ms and one second — and never on an
|
|
4557
|
+
animation frame. The source’s wake returns after a single number comparison unless a
|
|
4558
|
+
row is actually due, and the chart’s wake does nothing at all when the document is
|
|
4559
|
+
hidden or the chart is detached. Both timers are cleared on destroy. Measured over a
|
|
4560
|
+
five-minute window holding 50,000 rows with no feed at all, the window’s CPU cost is
|
|
4561
|
+
inside the run-to-run noise of the same source with no bound set: under 0.04% of one core
|
|
4562
|
+
(<code>bench/idle-window.mjs</code>).</p>
|
|
4473
4563
|
<p><strong>The log keeps the raw sequence, not the merged one.</strong> Merging is right for
|
|
4474
4564
|
applying a backlog quickly and wrong for looking at what happened, because the intermediate
|
|
4475
4565
|
states are exactly what a time scrubber would move between. It survives the flush,
|
|
@@ -6553,7 +6643,8 @@ grid.import.apply(preview);</code></pre>
|
|
|
6553
6643
|
<tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane×column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
|
|
6554
6644
|
<tr><td class="name">createKPI</td><td class="desc">A KPI / stat-tile view (module <code>kpi</code>) of a dataset as a panel of aggregate tiles — each tile a <code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code> or a <code>custom</code> reducer over the routed rows, with an optional <code>filter</code> predicate, number <code>format</code> (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>), a <code>baseline</code> for a delta, semantic threshold bands (<code>thresholds</code> with two cut points and a direction, or an explicit <code>bands</code> list, giving a <code>good</code>/<code>warn</code>/<code>critical</code> status kept separate from any accent), and an optional <code>sparkline</code> series. It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, kpi)</code> and drive a KPI panel beside a grid, a kanban and a chart off one feed. Updates are incremental — a delta adjusts each tile's running accumulator by only the rows it carries (an add contributes, a remove reverses, an update reverses-then-contributes), the two bounded exceptions being a <code>min</code>/<code>max</code> whose current extreme is removed (a rescan of that tile's own value multiset) and a <code>custom</code> reducer (recomputed over the filtered store, an arbitrary function having no inverse). Each tile is a labelled <code><figure></code>, focusable and keyboard-activatable, its value announced, the sparkline respecting <code>prefers-reduced-motion</code>; a <code>tile:click</code> event (also from the keyboard) lets a host drill down or filter a routed grid. Pass <code>null</code> as the element for a headless panel that computes the same tile model without a DOM. Not a dashboard layout engine (that is the parked dashboard generator) and no charting beyond the minimal sparkline (that is the charts module).</td></tr>
|
|
6555
6645
|
<tr><td class="name">createAI</td><td class="desc">Create an AI narrative / insights controller (module <code>ai</code>) over a live grid. It produces a short, plain-language narrative of the grid's <em>computed</em> figures — a per-KPI / per-chart / per-column <code>explain</code>, or an <code>insights</code> panel over the current filtered view. The grid makes no AI call of its own: <code>createAI</code> imports no provider SDK, holds no key, and makes no network request; it calls one host callback, <code>ask({ system, messages, prompt, tools?, schema?, signal })</code> — your model, your key, your privacy decision — the same philosophy as the data adapters. Two grounding paths feed one guard: where the provider offers tool-use the model is given a curated read-only tool set (<code>getSchema</code>, <code>getProfile</code>, <code>getStatistics</code>, <code>getForecast</code>, <code>runQuery</code>) and the grid's own engine computes what it asks for; otherwise the module builds a facts packet from <code>grid.statistics</code>/the profile/forecasts/view counts and passes it in the prompt. Every figure in the narrative is reconciled against the values the engine produced this render — an ungrounded number is stripped before display (the number-reconciliation guard), so a hallucinated figure never reaches the user. The prompt is constrained to narrate-only; the layer is read-only and never mutates data. <code>redact</code> (a column id, a list, or a predicate) and <code>maxRows</code> bound what the module hands <code>ask()</code>, and the module sends nothing itself; an <code>ask()</code> error surfaces a friendly message and the grid stays fully usable, AI being additive rather than load-bearing. Complementary to <code>grid.ai</code> (the intent/plan skill layer): pass no <code>ask</code> and it adopts the grid's configured <code>ai.ask</code>, running the facts-packet path over it. Pass a headless grid for a DOM-free narrative; <code>facts(target)</code> returns the exact grounded packet without calling <code>ask()</code>. UMD global <code>LatticeGridAI</code>.</td></tr>
|
|
6556
|
-
|
|
6646
|
+
<tr><td class="name">createTabs</td><td class="desc">Create a tabbed grid (module <code>tabs</code>): a <code>role="tablist"</code> strip above a stack of <code>role="tabpanel"</code> regions, each hosting its own, independently-configured <code>createGrid</code> instance — "configure each tab as per a normal grid" rather than one grid whose state is swapped (<code>ColumnModel#applyState</code> only repositions/hides/resizes existing columns by id; it carries no field, type or row data, so a state-swap only works when every tab shares one schema). <code>createGrid</code> is injected (<code>createTabs(el, { createGrid, tabs })</code>), the same pattern the React/Vue/Svelte adapters use, so the module imports no engine code and adds nothing to a page that does not load it. A tab that names <code>from: '<tabId>'</code> gets a <code>source: { mode: 'derived', from: <the parent tab’s live grid>, where, group, join, … }</code> wired for it automatically — reusing the shipped derived-source mechanism rather than a new config-inheritance one — and activating a derived tab materialises its whole ancestor chain first; a cyclic <code>from</code> graph is refused (naming the exact cycle) when <code>createTabs</code> is called, not at first click. A tab’s grid mounts on first activation and then stays alive, hidden, so its scroll/selection/filters/sort/grouping/expansion — and an open cell/row editor, left exactly as it was, uncommitted and undiscarded — survive a switch natively; <code>destroy()</code> tears every mounted tab down. The strip is a real tablist with <code>aria-selected</code>, a roving <code>tabindex</code>, and manual-activation keyboard handling (arrows/Home/End move focus, Enter/Space or a click activates). Events: <code>tab:changed</code>, a cancellable <code>beforeTabChange</code> paired with <code>tabChange:cancelled</code>. UMD global <code>LatticeGridTabs</code>.</td></tr>
|
|
6647
|
+
<tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
|
|
6557
6648
|
<tr><td class="name">createGantt</td><td class="desc">Create a project-planning Gantt controller (module <code>gantt</code>) over a task list and a dependency list. A CPM engine (<code>computeSchedule</code>) computes each task's early/late start and finish, its slack and the zero-float critical path, honouring the four link types (<code>LINK_TYPES</code>: FS/SS/FF/SF) with lag, and recomputes on every edit — emitting <code>schedule</code> or, on a dependency cycle or bad input, <code>error</code> (a code from <code>SCHEDULE_ERROR</code>). Milestones are zero-duration points; summary (WBS) tasks are derived from their children (earliest start, latest finish, weighted progress) rather than scheduled; <code>findViolations</code> flags a task placed earlier than its predecessors allow, and <code>toISODate</code> maps an engine day-number back to a calendar date.</td></tr>
|
|
6558
6649
|
<tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
|
|
6559
6650
|
<tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>
|
package/lattice-grid.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Lattice Grid 1.
|
|
2
|
+
* Lattice Grid 1.50.0, type declarations
|
|
3
3
|
* Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
4
4
|
* https://latticegrid.dev
|
|
5
5
|
*/
|
|
@@ -729,6 +729,12 @@ export interface Column {
|
|
|
729
729
|
shadow?: ShadowKind | {
|
|
730
730
|
of?: string;
|
|
731
731
|
kind: ShadowKind;
|
|
732
|
+
/**
|
|
733
|
+
* For `kind: 'history'`, how many past readings to keep (20 by default). With
|
|
734
|
+
* a time `window` (BACKLOG-0001043) this is instead how many buckets the span
|
|
735
|
+
* divides into — `window: {kind: 'time', span: 60_000}, depth: 20` is sixty
|
|
736
|
+
* one-second buckets. Ignored by every other kind.
|
|
737
|
+
*/
|
|
732
738
|
depth?: number;
|
|
733
739
|
/**
|
|
734
740
|
* For a positional kind, what to rank against. `'all'` (the default) uses
|
|
@@ -768,6 +774,16 @@ export interface Column {
|
|
|
768
774
|
* (`time`), or everything so far (`session`). The first rows of a series
|
|
769
775
|
* carry a partial window, stamped by a `windowCoverage` companion rather than
|
|
770
776
|
* dressed as full.
|
|
777
|
+
*
|
|
778
|
+
* For `kind: 'history'` (BACKLOG-0001043), only `{kind: 'time', span}` (or
|
|
779
|
+
* `minutes`) applies, and it changes what `history` means rather than what it
|
|
780
|
+
* aggregates: the `depth` buckets that span divides into are read once each,
|
|
781
|
+
* carrying the row's last known value forward into any bucket in which it did
|
|
782
|
+
* not change, so a static row still draws a flat, advancing line instead of
|
|
783
|
+
* freezing — the plain count-based history (no `window`) is a count of
|
|
784
|
+
* *changes* and stays exactly as it was. `count` and `session` are refused
|
|
785
|
+
* here: a plain count is already what `depth` means, and a session has no
|
|
786
|
+
* fixed span to divide into buckets.
|
|
771
787
|
*/
|
|
772
788
|
window?: {
|
|
773
789
|
kind: 'count' | 'time' | 'session';
|
|
@@ -1145,6 +1161,41 @@ export interface StreamSourceConfig {
|
|
|
1145
1161
|
* rather than what the producer sends.
|
|
1146
1162
|
*/
|
|
1147
1163
|
maxRows?: number;
|
|
1164
|
+
/**
|
|
1165
|
+
* The longest a row is kept, in milliseconds — a rolling *time* window, sitting
|
|
1166
|
+
* beside `maxRows` as a second, independent bound (BACKLOG-0001036). Rows older
|
|
1167
|
+
* than the span are evicted through the same path, the same `evicted` counters
|
|
1168
|
+
* and the same `stream:evicted` event as the count bound, so an existing
|
|
1169
|
+
* readout keeps working. Set both and whichever bites first applies. Eviction
|
|
1170
|
+
* continues on a low-frequency timer while the feed is idle, so "the last five
|
|
1171
|
+
* minutes" keeps shrinking through a silent period rather than freezing —
|
|
1172
|
+
* which is the thing `maxRows` cannot do.
|
|
1173
|
+
*
|
|
1174
|
+
* Retention is a *bound, not a guillotine*: rows live a little past the span
|
|
1175
|
+
* before a block is dropped. Two things add to it. First the eviction slack,
|
|
1176
|
+
* ten per cent of the span, exactly as `maxRows` overshoots its count, so the
|
|
1177
|
+
* row permutation is rebuilt once per block rather than once per row. Second,
|
|
1178
|
+
* when the feed is idle, up to one tick of the eviction timer, which runs at a
|
|
1179
|
+
* quarter of the span clamped to between 50 ms and one second. So the real
|
|
1180
|
+
* ceiling is roughly `span * 1.1 + tick`, and because the tick has a floor it
|
|
1181
|
+
* is proportionally larger the shorter the window: negligible at a five-minute
|
|
1182
|
+
* window (about 10%), around 1.25x at ten seconds, and as much as ~1.35x at
|
|
1183
|
+
* three. That is the deliberate trade for an idle grid that costs no CPU.
|
|
1184
|
+
*
|
|
1185
|
+
* Omit for no age limit.
|
|
1186
|
+
*/
|
|
1187
|
+
maxAge?: number;
|
|
1188
|
+
/**
|
|
1189
|
+
* Which clock `maxAge` reads: a column id (or dotted path), or a function of
|
|
1190
|
+
* the row returning a `Date`, epoch milliseconds, or an ISO string
|
|
1191
|
+
* (BACKLOG-0001036). Given, the window follows the **data's own** clock, so it
|
|
1192
|
+
* means what the producer means — and inherits the producer's clock skew.
|
|
1193
|
+
* Omitted, `maxAge` falls back to **arrival time**: when the row reached this
|
|
1194
|
+
* source. Arrival time needs no timestamp column and cannot be skewed, but it
|
|
1195
|
+
* is not event time — a row delayed in transit counts as young. A row whose
|
|
1196
|
+
* time value cannot be read is never aged out.
|
|
1197
|
+
*/
|
|
1198
|
+
ageBy?: string | ((row: unknown) => unknown);
|
|
1148
1199
|
promoteToMemoryBelow?: number;
|
|
1149
1200
|
coalesceMs?: number;
|
|
1150
1201
|
}
|
|
@@ -6238,6 +6289,19 @@ export interface ChartAxis {
|
|
|
6238
6289
|
every?: number;
|
|
6239
6290
|
/** Force the category labels' rotation rather than deciding it. */
|
|
6240
6291
|
rotate?: boolean | 'auto';
|
|
6292
|
+
/**
|
|
6293
|
+
* A rolling window for the axis domain (BACKLOG-0001036), in the shipped
|
|
6294
|
+
* `WindowSpec` vocabulary that rolling statistics already use. Only
|
|
6295
|
+
* `{ kind: 'time', span }` applies to an axis: the domain becomes the last
|
|
6296
|
+
* `span` milliseconds ending **now**, so the chart keeps scrolling left while
|
|
6297
|
+
* the feed is silent — the thing a count window cannot do, because with no
|
|
6298
|
+
* rows arriving nothing changes. Advanced on a low-frequency clock (a quarter
|
|
6299
|
+
* of the window, between 50 ms and 1 s), never per frame, and stopped when the
|
|
6300
|
+
* chart is destroyed or its document is hidden. Needs a continuous x axis
|
|
6301
|
+
* carrying wall-clock times; `{ kind: 'count' }` is the source's `maxRows` and
|
|
6302
|
+
* is refused here rather than given a second meaning.
|
|
6303
|
+
*/
|
|
6304
|
+
window?: Pick<WindowSpec, 'kind' | 'span'>;
|
|
6241
6305
|
}
|
|
6242
6306
|
|
|
6243
6307
|
/**
|
|
@@ -8760,3 +8824,108 @@ declare module 'lattice-grid/modules/ai' {
|
|
|
8760
8824
|
|
|
8761
8825
|
export default createAI;
|
|
8762
8826
|
}
|
|
8827
|
+
|
|
8828
|
+
declare module 'lattice-grid/modules/tabs' {
|
|
8829
|
+
/**
|
|
8830
|
+
* One tab: an id, a display label, a grid config, and — for a derived tab —
|
|
8831
|
+
* the parent tab id plus the narrowing forwarded onto the derived source
|
|
8832
|
+
* built for it (`source: { mode: 'derived', from: <parent's grid>, ... }`).
|
|
8833
|
+
* The derivation keys are the ones `packages/core/src/source/derive.js`
|
|
8834
|
+
* already understands; this module invents none of its own.
|
|
8835
|
+
*/
|
|
8836
|
+
interface TabDescriptor {
|
|
8837
|
+
/** A stable, unique id. Required. */
|
|
8838
|
+
id: string;
|
|
8839
|
+
/** The tab button's text. Defaults to `id`. */
|
|
8840
|
+
label?: string;
|
|
8841
|
+
/** The grid config passed to `createGrid` for this tab (merged with the derived `source`, when `from` is set). */
|
|
8842
|
+
config?: object;
|
|
8843
|
+
/** The parent tab id to derive from. When set, `config.source` is built for you and any of your own is replaced (with a warning). */
|
|
8844
|
+
from?: string;
|
|
8845
|
+
/** Row predicate forwarded to the derived source. */
|
|
8846
|
+
where?: (row: unknown) => boolean;
|
|
8847
|
+
/** Group-by forwarded to the derived source. */
|
|
8848
|
+
group?: unknown;
|
|
8849
|
+
groupBy?: unknown;
|
|
8850
|
+
/** Time-bucketing forwarded to the derived source. */
|
|
8851
|
+
bucket?: unknown;
|
|
8852
|
+
/** Join spec forwarded to the derived source. */
|
|
8853
|
+
join?: unknown;
|
|
8854
|
+
/** Array-field unnesting forwarded to the derived source. */
|
|
8855
|
+
unnest?: unknown;
|
|
8856
|
+
/** `'live' | 'idle' | 'manual' | number` forwarded to the derived source. */
|
|
8857
|
+
refresh?: 'live' | 'idle' | 'manual' | number;
|
|
8858
|
+
/** Cross-filter wiring forwarded to the derived source. */
|
|
8859
|
+
crossFilter?: unknown;
|
|
8860
|
+
/** Which slice of the parent's rows to derive from: `'filtered' | 'all' | 'selected' | 'grouped'`. */
|
|
8861
|
+
follow?: 'filtered' | 'all' | 'selected' | 'grouped';
|
|
8862
|
+
/** Row limit forwarded to the derived source. */
|
|
8863
|
+
limit?: number;
|
|
8864
|
+
/** Sort forwarded to the derived source. */
|
|
8865
|
+
sort?: unknown;
|
|
8866
|
+
/** Statistical-profile derivation, forwarded to the derived source. */
|
|
8867
|
+
profile?: unknown;
|
|
8868
|
+
/** This tab's panel's own `aria-label`, when the label alone is not enough context. */
|
|
8869
|
+
ariaLabel?: string;
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8872
|
+
/** The payload every tab-change event carries. */
|
|
8873
|
+
interface TabChangeEvent {
|
|
8874
|
+
id: string;
|
|
8875
|
+
previousId: string | null;
|
|
8876
|
+
origin?: 'api' | 'user' | 'init';
|
|
8877
|
+
reason?: string | null;
|
|
8878
|
+
/** Cancel the switch (only meaningful on `beforeTabChange`). */
|
|
8879
|
+
preventDefault?: (reason?: string) => void;
|
|
8880
|
+
defaultPrevented?: boolean;
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
/** Tabbed-grid configuration. */
|
|
8884
|
+
interface TabsConfig {
|
|
8885
|
+
/** The grid factory to mount each tab with, e.g. `import { createGrid } from 'lattice-grid'`. Required. */
|
|
8886
|
+
createGrid: (el: HTMLElement, config: object) => unknown;
|
|
8887
|
+
/** The tabs, in display order. Required, at least one. */
|
|
8888
|
+
tabs: TabDescriptor[];
|
|
8889
|
+
/** The initially active tab id. Defaults to the first tab. */
|
|
8890
|
+
active?: string;
|
|
8891
|
+
/** The tablist landmark's accessible name. */
|
|
8892
|
+
ariaLabel?: string;
|
|
8893
|
+
/** An explicit message-catalogue override; otherwise a mounted tab's own `grid.messages` is used. */
|
|
8894
|
+
messages?: { t(key: string, params?: Record<string, unknown>): string };
|
|
8895
|
+
onTabChange?: (event: TabChangeEvent) => void;
|
|
8896
|
+
onBeforeTabChange?: (event: TabChangeEvent) => boolean | void | Promise<boolean>;
|
|
8897
|
+
onTabChangeCancelled?: (event: TabChangeEvent) => void;
|
|
8898
|
+
}
|
|
8899
|
+
|
|
8900
|
+
/**
|
|
8901
|
+
* A tabbed grid: a `role="tablist"` strip above a stack of `role="tabpanel"`
|
|
8902
|
+
* regions, each hosting its own, independently-configured grid instance
|
|
8903
|
+
* (BACKLOG-0001039). A tab's grid mounts on first activation and is kept
|
|
8904
|
+
* alive, hidden, until `destroy()`.
|
|
8905
|
+
*/
|
|
8906
|
+
interface Tabs {
|
|
8907
|
+
readonly el: HTMLElement;
|
|
8908
|
+
/** The currently active tab id. */
|
|
8909
|
+
readonly activeId: string;
|
|
8910
|
+
/** The configured tab ids, in order. */
|
|
8911
|
+
tabs(): string[];
|
|
8912
|
+
/** The live grid instance for a tab, or `null` before it has been materialised. */
|
|
8913
|
+
tab(id: string): unknown | null;
|
|
8914
|
+
/** Whether a tab's grid has been created yet. */
|
|
8915
|
+
isMounted(id: string): boolean;
|
|
8916
|
+
/** Switch the active tab, gated by `beforeTabChange`. */
|
|
8917
|
+
activate(id: string, opts?: { origin?: 'api' | 'user' }): boolean | Promise<boolean>;
|
|
8918
|
+
on(name: 'beforeTabChange' | 'tab:changed' | 'tabChange:cancelled' | string, fn: (event: TabChangeEvent) => void): () => void;
|
|
8919
|
+
off(name: string, fn: (event: TabChangeEvent) => void): void;
|
|
8920
|
+
/** Tear the whole strip down; destroys every mounted tab's grid. */
|
|
8921
|
+
destroy(): void;
|
|
8922
|
+
}
|
|
8923
|
+
|
|
8924
|
+
/**
|
|
8925
|
+
* Create a tabbed grid over a host element. Each tab is a full,
|
|
8926
|
+
* independently-configured grid instance; a tab may derive from another via
|
|
8927
|
+
* `from`, reusing the shipped `source: { mode: 'derived' }` mechanism.
|
|
8928
|
+
*/
|
|
8929
|
+
export function createTabs(el: HTMLElement, config: TabsConfig): Tabs;
|
|
8930
|
+
export default createTabs;
|
|
8931
|
+
}
|