@toclocoinc/lattice-grid 1.62.0 → 1.63.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.
Files changed (159) hide show
  1. package/README.md +23 -8
  2. package/docs/API.html +526 -16
  3. package/docs/CHART-CODES.md +141 -2
  4. package/docs/api-detail.html +194 -5
  5. package/lattice-grid.d.ts +235 -5
  6. package/lattice-grid.esm.min.js +856 -78
  7. package/lattice-grid.min.cjs +856 -78
  8. package/lattice-grid.min.js +856 -78
  9. package/modules/ai.d.ts +1 -1
  10. package/modules/ai.esm.min.js +13 -16
  11. package/modules/ai.min.cjs +13 -16
  12. package/modules/ai.min.js +13 -16
  13. package/modules/angular.d.ts +1 -1
  14. package/modules/angular.esm.min.js +2 -2
  15. package/modules/angular.min.cjs +2 -2
  16. package/modules/angular.min.js +2 -2
  17. package/modules/chart-alluvial.d.ts +1 -1
  18. package/modules/chart-alluvial.esm.min.js +1 -1
  19. package/modules/chart-alluvial.min.cjs +147 -0
  20. package/modules/chart-alluvial.min.js +147 -0
  21. package/modules/chart-arc.d.ts +1 -1
  22. package/modules/chart-arc.esm.min.js +1 -1
  23. package/modules/chart-arc.min.cjs +106 -0
  24. package/modules/chart-arc.min.js +106 -0
  25. package/modules/chart-bubblemap.d.ts +1 -1
  26. package/modules/chart-bubblemap.esm.min.js +42 -6
  27. package/modules/chart-bubblemap.min.cjs +126 -0
  28. package/modules/chart-bubblemap.min.js +126 -0
  29. package/modules/chart-bump.d.ts +1 -1
  30. package/modules/chart-bump.esm.min.js +1 -1
  31. package/modules/chart-bump.min.cjs +110 -0
  32. package/modules/chart-bump.min.js +110 -0
  33. package/modules/chart-calendar.d.ts +1 -1
  34. package/modules/chart-calendar.esm.min.js +1 -1
  35. package/modules/chart-calendar.min.cjs +129 -0
  36. package/modules/chart-calendar.min.js +129 -0
  37. package/modules/chart-decomposition.d.ts +1 -1
  38. package/modules/chart-decomposition.esm.min.js +1 -1
  39. package/modules/chart-decomposition.min.cjs +131 -0
  40. package/modules/chart-decomposition.min.js +131 -0
  41. package/modules/chart-diverging.d.ts +1 -1
  42. package/modules/chart-diverging.esm.min.js +1 -1
  43. package/modules/chart-diverging.min.cjs +104 -0
  44. package/modules/chart-diverging.min.js +104 -0
  45. package/modules/chart-dumbbell.d.ts +1 -1
  46. package/modules/chart-dumbbell.esm.min.js +1 -1
  47. package/modules/chart-dumbbell.min.cjs +114 -0
  48. package/modules/chart-dumbbell.min.js +114 -0
  49. package/modules/chart-fan.d.ts +1 -1
  50. package/modules/chart-fan.esm.min.js +1 -1
  51. package/modules/chart-fan.min.cjs +139 -0
  52. package/modules/chart-fan.min.js +139 -0
  53. package/modules/chart-hexbin.d.ts +1 -1
  54. package/modules/chart-hexbin.esm.min.js +1 -1
  55. package/modules/chart-hexbin.min.cjs +148 -0
  56. package/modules/chart-hexbin.min.js +148 -0
  57. package/modules/chart-hexmap.d.ts +1 -1
  58. package/modules/chart-hexmap.esm.min.js +46 -6
  59. package/modules/chart-hexmap.min.cjs +164 -0
  60. package/modules/chart-hexmap.min.js +164 -0
  61. package/modules/chart-icicle.d.ts +1 -1
  62. package/modules/chart-icicle.esm.min.js +1 -1
  63. package/modules/chart-icicle.min.cjs +97 -0
  64. package/modules/chart-icicle.min.js +97 -0
  65. package/modules/chart-parallel.d.ts +1 -1
  66. package/modules/chart-parallel.esm.min.js +1 -1
  67. package/modules/chart-parallel.min.cjs +123 -0
  68. package/modules/chart-parallel.min.js +123 -0
  69. package/modules/chart-ridgeline.d.ts +1 -1
  70. package/modules/chart-ridgeline.esm.min.js +1 -1
  71. package/modules/chart-ridgeline.min.cjs +119 -0
  72. package/modules/chart-ridgeline.min.js +119 -0
  73. package/modules/chart-roc.d.ts +1 -1
  74. package/modules/chart-roc.esm.min.js +1 -1
  75. package/modules/chart-roc.min.cjs +181 -0
  76. package/modules/chart-roc.min.js +181 -0
  77. package/modules/chart-slope.d.ts +1 -1
  78. package/modules/chart-slope.esm.min.js +1 -1
  79. package/modules/chart-slope.min.cjs +100 -0
  80. package/modules/chart-slope.min.js +100 -0
  81. package/modules/chart-splom.d.ts +1 -1
  82. package/modules/chart-splom.esm.min.js +1 -1
  83. package/modules/chart-splom.min.cjs +124 -0
  84. package/modules/chart-splom.min.js +124 -0
  85. package/modules/chart-waffle.d.ts +1 -1
  86. package/modules/chart-waffle.esm.min.js +1 -1
  87. package/modules/chart-waffle.min.cjs +97 -0
  88. package/modules/chart-waffle.min.js +97 -0
  89. package/modules/charts.d.ts +1 -1
  90. package/modules/charts.esm.min.js +1917 -662
  91. package/modules/charts.min.cjs +1917 -662
  92. package/modules/charts.min.js +1917 -662
  93. package/modules/data-router.d.ts +1 -1
  94. package/modules/data-router.esm.min.js +129 -20
  95. package/modules/data-router.min.cjs +129 -20
  96. package/modules/data-router.min.js +129 -20
  97. package/modules/devtools.d.ts +1 -1
  98. package/modules/devtools.esm.min.js +2 -2
  99. package/modules/devtools.min.cjs +2 -2
  100. package/modules/devtools.min.js +2 -2
  101. package/modules/dhtmlx-compat.d.ts +1 -1
  102. package/modules/dhtmlx-compat.esm.min.js +4 -16
  103. package/modules/dhtmlx-compat.min.cjs +4 -16
  104. package/modules/dhtmlx-compat.min.js +4 -16
  105. package/modules/gantt.d.ts +1 -1
  106. package/modules/gantt.esm.min.js +187 -82
  107. package/modules/gantt.min.cjs +187 -82
  108. package/modules/gantt.min.js +187 -82
  109. package/modules/geo-europe-nuts.d.ts +16 -0
  110. package/modules/geo-europe-nuts.esm.min.js +29 -0
  111. package/modules/geo-uk.d.ts +18 -0
  112. package/modules/geo-uk.esm.min.js +29 -0
  113. package/modules/geo-us-states.d.ts +16 -0
  114. package/modules/geo-us-states.esm.min.js +29 -0
  115. package/modules/geo-world-110m.d.ts +17 -0
  116. package/modules/geo-world-110m.esm.min.js +29 -0
  117. package/modules/geo-world-50m.d.ts +16 -0
  118. package/modules/geo-world-50m.esm.min.js +29 -0
  119. package/modules/htmx.d.ts +1 -1
  120. package/modules/htmx.esm.min.js +856 -78
  121. package/modules/htmx.min.cjs +856 -78
  122. package/modules/htmx.min.js +856 -78
  123. package/modules/kanban.d.ts +1 -1
  124. package/modules/kanban.esm.min.js +4 -16
  125. package/modules/kanban.min.cjs +4 -16
  126. package/modules/kanban.min.js +4 -16
  127. package/modules/kpi.d.ts +1 -1
  128. package/modules/kpi.esm.min.js +29 -24
  129. package/modules/kpi.min.cjs +29 -24
  130. package/modules/kpi.min.js +29 -24
  131. package/modules/layout.d.ts +1 -1
  132. package/modules/layout.esm.min.js +4 -16
  133. package/modules/layout.min.cjs +4 -16
  134. package/modules/layout.min.js +4 -16
  135. package/modules/mock-socket.d.ts +1 -1
  136. package/modules/mock-socket.esm.min.js +2 -2
  137. package/modules/mock-socket.min.cjs +2 -2
  138. package/modules/mock-socket.min.js +2 -2
  139. package/modules/react.d.ts +308 -3
  140. package/modules/react.esm.min.js +1072 -22
  141. package/modules/react.min.cjs +1056 -21
  142. package/modules/react.min.js +1056 -21
  143. package/modules/svelte.d.ts +1 -1
  144. package/modules/svelte.esm.min.js +2 -2
  145. package/modules/svelte.min.cjs +2 -2
  146. package/modules/svelte.min.js +2 -2
  147. package/modules/tabs.d.ts +1 -1
  148. package/modules/tabs.esm.min.js +4 -16
  149. package/modules/tabs.min.cjs +4 -16
  150. package/modules/tabs.min.js +4 -16
  151. package/modules/vue.d.ts +1 -1
  152. package/modules/vue.esm.min.js +2 -2
  153. package/modules/vue.min.cjs +2 -2
  154. package/modules/vue.min.js +2 -2
  155. package/modules/webcomponent.d.ts +1 -1
  156. package/modules/webcomponent.esm.min.js +856 -78
  157. package/modules/webcomponent.min.cjs +856 -78
  158. package/modules/webcomponent.min.js +856 -78
  159. package/package.json +1 -1
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.62.0</p>
363
+ <p class="rail__sub">API reference · v1.63.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.62.0</span>
446
+ <span class="chip">Version 1.63.0</span>
447
447
  <span class="chip">Zero dependencies</span>
448
448
  <span class="chip"><a href="api-detail.html">Developer guide &rarr;</a></span>
449
449
  </p>
@@ -518,7 +518,7 @@
518
518
  <table>
519
519
  <thead><tr><th>Entry point</th><th>Factory</th><th>Needs</th></tr></thead>
520
520
  <tbody>
521
- <tr><td class="name">modules/react</td><td class="sig">createLatticeGrid({ React, createGrid })</td><td class="desc">Returns a component. Forwards a ref exposing <code>.grid</code>.</td></tr>
521
+ <tr><td class="name">modules/react</td><td class="sig">createLatticeGrid({ React, createGrid })</td><td class="desc">Returns a component. Forwards a ref exposing <code>.grid</code>. Since 1.63 the same entry point also builds a component for <a href="#react-v2">every other viewer</a> and the data router.</td></tr>
522
522
  <tr><td class="name">modules/vue</td><td class="sig">createLatticeGrid({ vue, createGrid })</td><td class="desc">Returns a Vue 3 component definition.</td></tr>
523
523
  <tr><td class="name">modules/svelte</td><td class="sig">createLatticeAction({ createGrid })</td><td class="desc">Returns a <code>use:</code> action.</td></tr>
524
524
  </tbody>
@@ -540,6 +540,171 @@
540
540
  </table>
541
541
  </div>
542
542
 
543
+ <h3 id="react-v2">React: every viewer, not only the grid</h3>
544
+ <p>
545
+ Until 1.63 the React adapter wrapped <code>createGrid</code> and nothing else. Every other
546
+ shipped viewer &mdash; the KPI panel, a chart, the board, the Gantt, the layout, the tab
547
+ strip &mdash; and the data router had no React surface, so a React application wrote its own
548
+ <code>useEffect</code> per viewer. It does not have to any more: there is one component per
549
+ viewer, and each keeps the contract the grid component already kept.
550
+ </p>
551
+ <pre><code><span class="kw">import</span> React <span class="kw">from</span> 'react';
552
+ <span class="kw">import</span> ReactDOM <span class="kw">from</span> 'react-dom';
553
+ <span class="kw">import</span> { createGrid } <span class="kw">from</span> '@toclocoinc/lattice-grid';
554
+ <span class="kw">import</span> { createKPI } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/kpi';
555
+ <span class="kw">import</span> { createChart } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/charts';
556
+ <span class="kw">import</span> { createDataRouter } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/data-router';
557
+ <span class="kw">import</span> { createLatticeReact } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/react';
558
+
559
+ <span class="cmt">// Built once, at module scope. Building components inside a component hands React a</span>
560
+ <span class="cmt">// new type on every render, and a new type is a different element: the subtree would</span>
561
+ <span class="cmt">// unmount and remount, destroying and rebuilding the grid on every keystroke.</span>
562
+ <span class="kw">const</span> L = createLatticeReact({
563
+ React, ReactDOM, createGrid, createKPI, createChart, createDataRouter,
564
+ });
565
+
566
+ <span class="kw">function</span> Dashboard({ rows }) {
567
+ <span class="kw">const</span> router = L.useLatticeRouter(ROUTER_CONFIG);
568
+ <span class="kw">return</span> (
569
+ &lt;L.LatticeRouterProvider router={router}&gt;
570
+ &lt;L.LatticeGridProvider&gt;
571
+ &lt;L.LatticeGrid name="quakes" route="all" {...GRID_CONFIG} rows={rows} /&gt;
572
+ &lt;L.LatticeKPI gridName="quakes" tiles={TILES} columns={5} /&gt;
573
+ &lt;L.LatticeChart gridName="quakes" type="bar" x="region" y="count" /&gt;
574
+ &lt;/L.LatticeGridProvider&gt;
575
+ &lt;/L.LatticeRouterProvider&gt;
576
+ );
577
+ }</code></pre>
578
+ <div class="table-wrap">
579
+ <table>
580
+ <thead><tr><th>Export</th><th>Signature</th><th>What it is</th></tr></thead>
581
+ <tbody>
582
+ <tr><td class="name">createLatticeGrid</td><td class="sig">({ React, createGrid })</td><td class="desc">The grid component. Extended in 1.63 with <code>rowUpdates</code>, <code>predicates</code>, <code>onGridReady</code>, <code>onGridDestroy</code>, <code>name</code> and <code>route</code>.</td></tr>
583
+ <tr><td class="name">createLatticeKPI</td><td class="sig">({ React, createKPI })</td><td class="desc">The KPI panel. Grid-bound through context by default; pass <code>rows</code> instead for a panel with no grid.</td></tr>
584
+ <tr><td class="name">createLatticeChart</td><td class="sig">({ React, createChart })</td><td class="desc">A chart. Requires a grid, so nothing is mounted until one exists; a changed spec key goes to <code>chart.update()</code> and the chart redraws rather than being rebuilt.</td></tr>
585
+ <tr><td class="name">createLatticeKanban</td><td class="sig">({ React, createKanban })</td><td class="desc">The board. <code>rows</code>, <code>quickFilter</code>, <code>sprint</code>, <code>epic</code>, <code>loading</code> and <code>error</code> are live props.</td></tr>
586
+ <tr><td class="name">createLatticeGantt</td><td class="sig">({ React, createGantt })</td><td class="desc">The Gantt. <code>tasks</code> and <code>dependencies</code> are live props.</td></tr>
587
+ <tr><td class="name">createLatticeLayout</td><td class="sig">({ React, createLayout })</td><td class="desc">The layout. Windows are driven through the ref; its events arrive as <code>onLayoutChanged</code>, <code>onWindowMoved</code> and the rest.</td></tr>
588
+ <tr><td class="name">createLatticeTabs</td><td class="sig">({ React, ReactDOM, createTabs, createGrid? })</td><td class="desc">The tab strip, with <strong>React-rendered tab content</strong>: a tab's <code>content</code> is a React element (or a function returning one) rendered into the strip's own panel through <code>createPortal</code>, so a tab's grid is a real <code>&lt;LatticeGrid&gt;</code> with props, a ref and the surrounding context. A tab with no <code>content</code> is left to the module, so the two kinds mix on one strip.</td></tr>
589
+ <tr><td class="name">createLatticeGridContext</td><td class="sig">({ React })</td><td class="desc">Returns <code>{ LatticeGridProvider, useLatticeGrid }</code>. A grid-bound viewer needs the grid <em>instance</em>, which appears after the first render; a ref cannot help because writing to a ref re-renders nobody. Several grids may publish under one provider &mdash; <code>name</code> on the grid, <code>gridName</code> on the viewer.</td></tr>
590
+ <tr><td class="name">createLatticeRouter</td><td class="sig">({ React, createDataRouter })</td><td class="desc">Returns <code>{ useLatticeRouter, LatticeRouterProvider, useRouter }</code>. The hook creates the router in an effect and destroys it in that effect's cleanup, so it returns <code>null</code> on the first render; the config is read once, because rebuilding would drop every attached grid and every row held.</td></tr>
591
+ <tr><td class="name">createLatticeViewer</td><td class="sig">({ React, viewer, mount, … })</td><td class="desc">The generic behind all of the above, and the escape hatch for a viewer with no named factory yet.</td></tr>
592
+ <tr><td class="name">createLatticeReact</td><td class="sig">({ React, ReactDOM?, …factories })</td><td class="desc">Every binding from one call: pass the factories the application uses and it builds those components, leaving the rest undefined. Nothing is imported, so an application that never uses the board never loads the board.</td></tr>
593
+ <tr><td class="name">createViewerController</td><td class="sig">({ viewer, mount, element, props })</td><td class="desc">The framework-free viewer lifecycle &mdash; mount once, push changed props into the live instance, destroy &mdash; shared by every adapter.</td></tr>
594
+ <tr><td class="name">VIEWER_EVENTS</td><td class="type">Record&lt;string, readonly string[]&gt;</td><td class="desc">Every event each non-grid viewer emits, keyed by viewer name.</td></tr>
595
+ <tr><td class="name">VIEWER_APPLY</td><td class="type">Record&lt;string, Record&lt;string, Function&gt;&gt;</td><td class="desc">Which props each viewer can take <em>live</em>, and the instance call each becomes. Anything not listed is mount-time configuration.</td></tr>
596
+ <tr><td class="name">viewerHandlerName</td><td class="type">(event) =&gt; string</td><td class="desc">A viewer event as its React prop: <code>card:move</code> becomes <code>onCardMove</code>.</td></tr>
597
+ <tr><td class="name">DEFAULT_GRID_NAME</td><td class="type">string</td><td class="desc">The name a grid publishes itself under when you do not choose one: <code>default</code>.</td></tr>
598
+ </tbody>
599
+ </table>
600
+ </div>
601
+
602
+ <pre data-run="js" data-expect="8 components, onCardMove, default, rows 2, events 5/6" data-covers="export:createLatticeReact export:createLatticeGrid export:createLatticeKPI export:createLatticeChart export:createLatticeKanban export:createLatticeGantt export:createLatticeLayout export:createLatticeTabs export:createLatticeViewer export:createLatticeGridContext export:createLatticeRouter export:createViewerController export:viewerHandlerName export:VIEWER_EVENTS export:VIEWER_APPLY export:DEFAULT_GRID_NAME"><code><span class="kw">const</span> A = <span class="kw">await</span> import('../packages/modules/react/index.js');
603
+
604
+ <span class="cmt">// A stand-in for React and react-dom. The adapter never imports either, so a</span>
605
+ <span class="cmt">// factory only needs the handful of names it destructures — which is exactly</span>
606
+ <span class="cmt">// why this example runs in Node with no framework installed.</span>
607
+ <span class="kw">const</span> React = { createElement: () =&gt; ({}), createContext: () =&gt; ({}), forwardRef: (f) =&gt; f };
608
+ <span class="kw">const</span> ReactDOM = { createPortal: () =&gt; ({}) };
609
+ <span class="kw">const</span> stub = () =&gt; ({ on: () =&gt; () =&gt; {}, destroy() {} });
610
+
611
+ <span class="cmt">// One call builds whichever components the factories you pass support.</span>
612
+ <span class="kw">const</span> L = A.createLatticeReact({
613
+ React, ReactDOM, createGrid: stub, createKPI: stub, createChart: stub, createDataRouter: stub,
614
+ });
615
+
616
+ <span class="cmt">// …or one factory at a time.</span>
617
+ <span class="kw">const</span> components = [
618
+ A.createLatticeGrid({ React, createGrid: stub }),
619
+ A.createLatticeKPI({ React, createKPI: stub }),
620
+ A.createLatticeChart({ React, createChart: stub }),
621
+ A.createLatticeKanban({ React, createKanban: stub }),
622
+ A.createLatticeGantt({ React, createGantt: stub }),
623
+ A.createLatticeLayout({ React, createLayout: stub }),
624
+ A.createLatticeTabs({ React, ReactDOM, createTabs: stub }),
625
+ A.createLatticeViewer({ React, viewer: 'kpi', mount: stub }),
626
+ ].filter((c) =&gt; typeof c === 'function');
627
+
628
+ <span class="cmt">// The provider/hook pair and the router hook are built the same way.</span>
629
+ <span class="kw">const</span> { LatticeGridProvider, useLatticeGrid } = A.createLatticeGridContext({ React });
630
+ <span class="kw">const</span> { useLatticeRouter } = A.createLatticeRouter({ React, createDataRouter: stub });
631
+
632
+ <span class="cmt">// And the lifecycle underneath, driven with no framework at all: mount once,</span>
633
+ <span class="cmt">// push a changed live prop into the instance that already exists, destroy.</span>
634
+ <span class="kw">const</span> seen = [];
635
+ <span class="kw">const</span> controller = A.createViewerController({
636
+ viewer: 'kpi',
637
+ element: {},
638
+ props: { rows: [{ id: 1 }] },
639
+ mount: () =&gt; ({ setRows: (r) =&gt; seen.push(r.length), on: () =&gt; () =&gt; {}, destroy() {} }),
640
+ });
641
+ controller.update({ rows: [{ id: 1 }, { id: 2 }] });
642
+ controller.destroy();
643
+
644
+ <span class="kw">return</span> `${components.length} components, ${A.viewerHandlerName('card:move')}, `
645
+ + `${A.DEFAULT_GRID_NAME}, rows ${seen.join('/')}, `
646
+ + `events ${A.VIEWER_EVENTS.kpi.length}/${Object.keys(A.VIEWER_APPLY).length}`;</code></pre>
647
+
648
+ <p class="section-note">
649
+ <strong>Live props versus mount-time props.</strong> The grid takes any changed configuration
650
+ key through one call (<code>grid.setAll</code>); no other viewer does. So each viewer declares
651
+ which props it can take while it is running &mdash; the table above, and
652
+ <code>VIEWER_APPLY</code> at runtime &mdash; and everything else is mount-time configuration.
653
+ A mount-time prop that changes is <em>not</em> silently ignored and <em>not</em> silently
654
+ remounted (that would throw away scroll position, selection and expansion): it is named once
655
+ in a warning that tells you to give the component a <code>key</code> that changes when the
656
+ rebuild is wanted, or to drive the instance through the ref.
657
+ </p>
658
+ <p class="section-note">
659
+ <strong>Two props rebuild a viewer rather than update it:</strong> the grid it is bound to,
660
+ and anything it cannot exist without. A chart holding a destroyed grid is not stale, it is
661
+ invalid, so a new grid tears the old chart down and builds a new one against it &mdash; in
662
+ that order, never the reverse.
663
+ </p>
664
+ <p class="section-note">
665
+ <strong>A live feed: <code>rowUpdates</code> and <code>predicates</code>.</strong>
666
+ <code>rowUpdates</code> is a keyed diff handed straight to <code>grid.rows.apply()</code>,
667
+ applied when the object's identity changes &mdash; a feed produces a new change object per
668
+ batch, so identity is the right trigger and re-applying the same object would re-land rows the
669
+ grid already has. <code>predicates</code> is <code>{ name: fn }</code> mapped to
670
+ <code>grid.filters.where(name, fn)</code>, diffed by name, with a name that has gone removed.
671
+ Those <em>compose</em> with whatever filter the reader set in the tool panel; the
672
+ <code>filters</code> prop cannot, because it maps to <code>filters.set</code> and replaces the
673
+ whole condition tree.
674
+ </p>
675
+ <p class="section-note">
676
+ <strong>StrictMode creates one instance.</strong> React 18's StrictMode deliberately mounts,
677
+ unmounts and mounts again in development. Every component here builds its instance in an
678
+ effect with an empty dependency list and destroys it in that effect's cleanup, so the first is
679
+ destroyed before the second is built and exactly one survives. There is no module-level
680
+ "already mounted" flag, because that would defeat a genuine remount.
681
+ </p>
682
+ <p class="section-note">
683
+ <strong>Props are diffed with <code>Object.is</code>, and that is a promise about identity.</strong>
684
+ An inline <code>columns={[{ field: 'a' }]}</code> is a new array on every render, so it counts
685
+ as changed every render and reconfigures the grid every render. Hoist it to module scope or
686
+ wrap it in <code>useMemo</code>. This is not a defect to work around: a deep compare of a
687
+ million-row array on every render would cost more than the reload it prevents.
688
+ </p>
689
+ <p class="section-note">
690
+ <strong>Sharing one <code>rows</code> array between grids is safe.</strong> Since 1.63 the
691
+ grid copies the array it is handed on ingest, so two grids given the same
692
+ <code>rows={EMPTY}</code> default no longer contaminate each other. The row <em>objects</em>
693
+ are still shared, as they always were &mdash; mutate one and both grids see it.
694
+ </p>
695
+ <p class="section-note">
696
+ <strong>React 18 and 19, no SSR.</strong> Nothing in the adapter uses an API added in 19 or
697
+ removed in 19 (<code>forwardRef</code> is still supported there). Every component owns a real
698
+ DOM element, so there is no server rendering and no React Server Component support: render
699
+ them on the client.
700
+ </p>
701
+ <p class="section-note">
702
+ <strong>One build warning is gone.</strong> The version resolver carried a Node-only fallback
703
+ whose <code>import('node:' + 'module')</code> Vite could not analyse statically, so every Vite
704
+ build of every application printed "The above dynamic import cannot be analyzed by Vite" about
705
+ a line that could never run. The build now deletes that branch from every emitted artefact.
706
+ </p>
707
+
543
708
  <div class="note">
544
709
  <p><strong>Published as <code>@toclocoinc/lattice-grid</code></strong>: <code>npm install
545
710
  @toclocoinc/lattice-grid</code>, then <code>import { createLatticeGrid } from
@@ -845,7 +1010,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
845
1010
  <tbody>
846
1011
  <tr><td class="name">columns</td><td class="type">(Column | ColumnGroup)[]</td><td class="dflt">, </td><td class="desc">Column definitions. Groups may nest.</td></tr>
847
1012
  <tr><td class="name">columnGroups</td><td class="type">ColumnGroup[]</td><td class="dflt">, </td><td class="desc">Header grouping declared separately from the columns.</td></tr>
848
- <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="dflt">, </td><td class="desc">Row objects. Held by reference; not copied.</td></tr>
1013
+ <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="dflt">, </td><td class="desc">Row objects. <strong>The array is copied on ingest; the row objects in it are not.</strong> The grid keeps a shallow copy of the array you pass here (and of <code>source.rows</code>, and of the array given to <code>rows.load()</code>), so your array is never written to: after <code>rows.apply</code>, a sort, a group or an edit it holds exactly what it held when you passed it, and two grids built from one array are independent. The <em>objects</em> inside it are still yours &mdash; <code>row.data</code> is the object you supplied and <code>rows.data()</code> returns those same objects, so identity round-trips (see <code>ingest.retainSource</code>). <code>rows.apply({ update })</code> does not write through either: it merges into a <em>new</em> object, which replaces that slot in the grid's copy only, so <code>row.data</code> for an updated row is a new object and the one you passed is untouched. An <strong>in-place cell edit does</strong>: <code>edit.setCells</code>, or typing in a cell, writes the new value into the shared object, which is the other face of <code>row === sourceObject</code>. <code>ingest.retainSource: false</code> and <code>ingest.dropSourceRows</code> opt out of sharing altogether.</td></tr>
849
1014
  <tr><td class="name">rowKey</td><td class="type">string | (row) =&gt; string</td><td class="dflt">, </td><td class="desc">Stable row identity. Without it the grid assigns a key per row object and warns: enough for sorting, filtering, selection and copying within a session, but change tracking, streaming dedupe, selection persistence and remote reload all switch off, because new objects are new rows. If it is configured but resolves to nothing for some rows — a field absent, or present on some rows only — those rows collapse onto one key and the grid warns once, naming the field(s) and how many rows were affected, on <code>rows.load()</code> as well as at construction.</td></tr>
850
1015
  <tr><td class="name">source</td><td class="type">SourceConfig</td><td class="dflt">memory</td><td class="desc">Where rows come from: <code>memory</code>, <code>paged</code>, <code>remote</code> or <code>stream</code>. See <a href="#sources">Sources</a>.</td></tr>
851
1016
  <tr><td class="name">ingest</td><td class="type">IngestConfig</td><td class="dflt">, </td><td class="desc"><code>{ retainSource, dropSourceRows }</code>. How rows enter the column store. <code>retainSource</code> defaults to <code>true</code>: the caller's row objects are held by reference so <code>rows.data()</code> returns them unchanged and <code>row === sourceObject</code> holds. Set it <code>false</code> to stop the store retaining them and reconstruct a row on demand — but the source layer and grid config still hold the array, so the resident footprint does not actually fall. <code>dropSourceRows: true</code> closes that gap: it releases the objects from the source layer too, so the packed columns become the only copy and the footprint drops by roughly an order of magnitude at scale. Either way <code>rows.data()</code> then returns freshly reconstructed objects, so identity checks and <code>row.sourceObject</code> no longer hold and equality becomes value-based. Cell values are unchanged.</td></tr>
@@ -1023,7 +1188,16 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
1023
1188
  <code>type: 'date'</code> column groups into day buckets. Date filters are unaffected either way
1024
1189
  — they compare on the day and return the same rows. Declare <code>type</code> and none of this applies:
1025
1190
  <code>'date'</code> truncates on purpose, <code>'datetime'</code> keeps a wall clock, and
1026
- <code>'timestamp'</code> keeps the instant to the millisecond. A column of ISO timestamps that
1191
+ <code>'timestamp'</code> keeps the instant to the millisecond.
1192
+ <strong><code>rows.value()</code> returns that stored form &mdash; and the same one on every
1193
+ path:</strong> the rows you passed at construction, <code>rows.load()</code>,
1194
+ <code>rows.apply({ add })</code> and <code>rows.apply({ update })</code>, <code>edit.setCells</code>
1195
+ and a typed cell edit, a stream chunk and a stream re-send, a store-backed or columnar store,
1196
+ off-thread ingest, a bound KPI tile and a chart binding all answer the same shape for the same
1197
+ instant, so a host can do arithmetic on it without testing what it got.
1198
+ <code>rows.text()</code> is the formatted form, and <code>row.data</code> is always the raw
1199
+ value you supplied, unconverted. For millisecond arithmetic use <code>type: 'timestamp'</code>,
1200
+ which answers the epoch number on every one of those paths. A column of ISO timestamps that
1027
1201
  must stay a calendar day opts out with an explicit <code>type: 'date'</code> — no warning is
1028
1202
  logged for that column, because the value is preserved (declared, not narrowed) and there is
1029
1203
  nothing to disclose.</p>
@@ -1218,7 +1392,7 @@ grid.destroy();
1218
1392
  <table>
1219
1393
  <thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
1220
1394
  <tbody>
1221
- <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.62.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1395
+ <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.63.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1222
1396
  <tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
1223
1397
  <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>
1224
1398
  <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>
@@ -1257,6 +1431,7 @@ grid.overlay.hide();</code></pre>
1257
1431
  <tr><td class="sig">get(index)</td><td class="type">Row</td><td class="desc">By display index, after filtering, grouping and flattening.</td></tr>
1258
1432
  <tr><td class="sig">byKey(key)</td><td class="type">Row</td><td class="desc">By row key, whether or not it is on screen.</td></tr>
1259
1433
  <tr><td class="sig">matchCount()</td><td class="type">number</td><td class="desc">Data rows passing the filters, across every page. Excludes group headers, footers and totals, the numerator of "1,204 of 100,000".</td></tr>
1434
+ <tr><td class="sig">coverage()</td><td class="type">{ covered, total, windowed }</td><td class="desc">How much of the data a figure computed from this grid covers &mdash; the question the counters above cannot answer, because on a windowed source they all report the rows it is <em>holding</em> and so agree with each other while the data that has been through is larger. <code>covered</code> is the rows a figure would be computed over; <code>total</code> is the rows the source knows about, or <strong><code>null</code> when it cannot know</strong> (a stream still open that has evicted nothing has no idea how many rows are coming, and says so rather than repeating <code>covered</code>); <code>windowed</code> is true when a window bounded the computation. <strong><code>covered &lt; total</code>, or <code>total === null</code>, means the figure is approximate</strong> &mdash; that is the test to write. False <code>windowed</code> means the figure is over all of it: a memory source, or a stream that finished having dropped nothing. Read on demand and cheap (one <code>matchCount()</code> and one <code>progress()</code> on the source, nothing cached), so call it beside every figure you publish rather than once at setup &mdash; on a live stream the answer moves.</td></tr>
1260
1435
  <tr><td class="sig">count()</td><td class="type">number</td><td class="desc">Display rows: group rows included, collapsed children excluded.</td></tr>
1261
1436
  <tr><td class="sig">totalCount()</td><td class="type">number</td><td class="desc">Source rows before filtering. The denominator of "1,204 of 100,000".</td></tr>
1262
1437
  <tr><td class="sig">value(key, colId)</td><td class="type">unknown</td><td class="desc">The stored value.</td></tr>
@@ -2495,6 +2670,7 @@ createGrid(el, {
2495
2670
 
2496
2671
  <h3 id="stationarity">Stationarity (ADF)</h3>
2497
2672
  <p>Before you compare two series or detrend one, it helps to know whether it is <strong>stationary</strong> — reverting to a level or trend — or wandering with a unit root. <code>grid.statistics.adf</code> runs the Augmented Dickey-Fuller test (BACKLOG-0000873) and returns a scalar readout, not a per-row column: the statistic, the augmenting lag chosen by AIC, MacKinnon's critical values, an interpolated p-value (stamped approximate), and a plain-language verdict at the 5% level. The constant+trend regression and the AIC lag choice match statsmodels' <code>adfuller</code>, against which the statistic and lag are verified.</p>
2673
+ <p><strong>The lag search and what it costs.</strong> <code>maxlag</code> caps the number of augmenting lags the AIC search considers; left out, it is the Schwert rule <code>⌈12·(n/100)^0.25⌉</code> — 34 candidates on 7,000 rows — itself capped so the fixed sample keeps degrees of freedom. The candidates are nested, so the search builds one design matrix at the cap and reads every smaller candidate off it — one pass to accumulate the normal equations, then a small solve and a single residual pass per candidate, rather than a fresh fit each time (BACKLOG-0001347). The default search over 7,000 rows is a matter of milliseconds. Setting <code>maxlag</code> narrows the search, never the arithmetic: the lag chosen, the statistic and the p-value are whatever the data says, and a cap that still contains the AIC-preferred lag returns exactly the same readout.</p>
2498
2674
  <pre data-run="js" data-expect="non-stationary|0" data-covers="method:statistics"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2499
2675
  <span class="cmt">// A random walk (a unit root): it wanders rather than reverting.</span>
2500
2676
  <span class="kw">const</span> walk = [0.138, -0.725, -1.26, -0.536, -0.267, 0.167, -0.765, -0.146, -0.311, -0.586,
@@ -2921,6 +3097,18 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'tria
2921
3097
 
2922
3098
  <h2 id="sources">Sources</h2>
2923
3099
  <p class="section-note">Where rows come from. <code>memory</code> is the default and needs no configuration.</p>
3100
+ <p><strong>A memory grid's rows may be declared either way.</strong> The top-level
3101
+ <code>rows</code> option is the usual one and what every example here uses; the same array
3102
+ inside the block works identically &mdash; <code>createGrid(el, { columns, source: { mode:
3103
+ 'memory', rows } })</code> opens with exactly those rows, with the same
3104
+ <code>count()</code>, <code>value()</code>, <code>text()</code>, type inference and bound KPI
3105
+ and chart readings as <code>createGrid(el, { columns, rows })</code>, and
3106
+ <code>rows.load()</code> and <code>rows.apply()</code> behave the same afterwards. Either
3107
+ array is copied on ingest, so the one you passed is never written to (see
3108
+ <code>rows</code> under <a href="#config">Configuration</a>). <strong>Declare them once:</strong>
3109
+ a grid given <em>both</em> uses the top-level <code>rows</code>, ignores the block's, and warns
3110
+ once naming both places. Only <code>memory</code> reads a <code>rows</code> key from the block
3111
+ &mdash; the fetching modes take theirs from the server.</p>
2924
3112
  <div class="table-wrap">
2925
3113
  <table>
2926
3114
  <thead><tr><th>Mode</th><th>Needs</th><th>Description</th></tr></thead>
@@ -3498,7 +3686,7 @@ createGrid(host, { source, columns: [...] });</code></pre>
3498
3686
  <tbody>
3499
3687
  <tr><td class="name">odataAdapter</td><td class="desc">Any OData v4 endpoint</td><td class="desc">Writes <code>$filter</code>, <code>$orderby</code>, <code>$top</code>, <code>$skip</code> and <code>$count</code>. System options keep their <code>$</code> unencoded, which several servers require.</td></tr>
3500
3688
  <tr><td class="name">restAdapter</td><td class="desc">The API you already have</td><td class="desc">Parameter names are yours to choose. Paging and sorting are assumed; filtering is assumed absent until you declare <code>operators</code>, because an adapter that claims to filter when the endpoint ignores it returns the wrong rows silently.</td></tr>
3501
- <tr><td class="name">duckdbAdapter</td><td class="desc">A DuckDB connection</td><td class="desc">Writes SQL and takes the whole query: filter tree, multi-column sort and paging. <code>from</code> is any FROM expression, so <code>read_parquet('s3://bucket/*.parquet')</code> is as valid as a table name. The engine is yours to create and install; this imports nothing, so the bundle is unchanged whether you use it or not.</td></tr>
3689
+ <tr><td class="name">duckdbAdapter</td><td class="desc">A DuckDB connection</td><td class="desc">Writes SQL and takes the whole query: filter tree, multi-column sort, paging and <a href="#duckdb-grouping">grouping</a> — a grouped grid is answered by <code>GROUP BY</code>, one level at a time, with the group counts, the subtotals, the matching count and the grand total all computed in the engine. <code>from</code> is any FROM expression, so <code>read_parquet('s3://bucket/*.parquet')</code> is as valid as a table name. The engine is yours to create and install; this imports nothing, so the bundle is unchanged whether you use it or not.</td></tr>
3502
3690
  <tr><td class="name">dfqlAdapter</td><td class="desc">DemandFlow entities</td><td class="desc">Speaks <code>POST /v1/query</code>. Sends the entity, the key attribute and the prefix to match, a field projection and one field-and-term filter, matched as a case-insensitive substring. It cannot sort or page, so the grid does both, and every request carries a <code>countOnly</code> line because <code>limit</code> caps rows <em>scanned</em> rather than matched: a filtered query returns an arbitrary subset, and the count is the only thing that reveals it.</td></tr>
3503
3691
  <tr><td class="name">graphqlAdapter</td><td class="desc">Any GraphQL endpoint</td><td class="desc">Configured, not zero-config: GraphQL has no fixed query semantics, so you pass <code>buildQuery</code> to turn the plan into a <code>{ query, variables }</code> operation and <code>parseResponse</code> to read <code>data</code> back into rows and total. Defaults cover an offset/limit list with <code>totalCount</code> and a Relay cursor connection (<code>first</code>/<code>after</code> with <code>pageInfo</code>). The default pushes only the window and the total; declare <code>operators</code> or <code>capabilities</code> for filter/sort only alongside a <code>buildQuery</code> that emits them. A cursor connection is forward-only, so a deep window costs round trips proportional to its offset.</td></tr>
3504
3692
  </tbody>
@@ -3620,7 +3808,9 @@ createGrid(host, { source, columns: [...] });</code></pre>
3620
3808
  before <code>LIMIT</code>, so one round trip returns both the window and the size of the set it
3621
3809
  was cut from. Against a local table it is free. Against a remote Parquet it is the most
3622
3810
  expensive thing the adapter does — a window function has to see every matching row of the
3623
- <em>projected</em> columns, so row-group pruning and range reads cannot help and the whole file
3811
+ <em>projected</em> columns, so whatever row-group pruning or range reads DuckDB and the file
3812
+ host might otherwise manage between them (see <a href="#duckdb-range-reads">whether a Parquet
3813
+ file streams or downloads whole</a>) cannot help, and the whole file
3624
3814
  crosses the wire to produce one page. Measured in Chrome on <code>duckdb-eh.wasm</code> against
3625
3815
  a 10,000,000-row Parquet of 162,386,227&nbsp;bytes — <strong>162.4&nbsp;MB</strong> decimal,
3626
3816
  154.9&nbsp;MiB binary, and every transfer figure on this page is decimal MB so that it can be
@@ -3696,6 +3886,25 @@ createGrid(host, { source, columns: [...] });</code></pre>
3696
3886
  real row 900 means paging to it. Turn the count off for a grid whose users scroll; leave it on
3697
3887
  for one whose users jump.
3698
3888
  </p>
3889
+ <p class="section-note" id="duckdb-range-reads">
3890
+ <strong>Whether a Parquet file streams or downloads whole is DuckDB's and the file host's
3891
+ doing, not the grid's.</strong> <code>duckdbAdapter</code> only writes SQL; it never opens a
3892
+ file, so it has no say in whether <code>read_parquet(...)</code> reads the whole thing or only
3893
+ the row groups a query needs. Measured against a 1.5&nbsp;MB Parquet on GitHub Pages
3894
+ (BACKLOG-0001324): DuckDB-Wasm 1.32.0's default HTTP path issued <strong>0 Range requests and
3895
+ read 100%</strong> of the file, for every query including a single-value chip filter. Running
3896
+ <code>LOAD httpfs;</code> on the connection before the first <code>read_parquet(...)</code>
3897
+ call changed that to <strong>25 Range requests and 30%</strong> of the file for the same query.
3898
+ (DuckDB-Wasm 1.29.0 read 119% of the file on the same test — its ranges overlapped — so the
3899
+ exact figures are a version's, not a promise.) A file registered with
3900
+ <code>db.registerFileBuffer(...)</code> is always read whole, whatever version is loaded: a
3901
+ buffer has already been downloaded in full before DuckDB ever sees it. And the file host has to
3902
+ cooperate: it must answer <code>HEAD</code>, advertise <code>Accept-Ranges: bytes</code> and
3903
+ answer a range request with <code>206 Partial Content</code> and a <code>Content-Range</code>
3904
+ header — GitHub Pages does all three — and, cross-origin, its CORS policy must expose
3905
+ <code>Content-Range</code> and <code>Content-Length</code> or the browser cannot read them back.
3906
+ Any of that missing and DuckDB falls back to reading the file whole, silently.
3907
+ </p>
3699
3908
  <p class="section-note">
3700
3909
  <strong>Typed binding for timestamp and date columns.</strong> A prepared statement binds a
3701
3910
  filter value with the value's own type, not the column's: the grid sends an instant as an
@@ -3732,6 +3941,94 @@ createGrid(host, { source, columns: [...] });</code></pre>
3732
3941
  changed). Against a <code>DATE</code> column an instant is truncated to its UTC day.
3733
3942
  </p>
3734
3943
 
3944
+ <h5 id="duckdb-grouping"><code>duckdbAdapter</code>: grouping runs in the engine, one level at a time</h5>
3945
+ <p>
3946
+ Grouping a hundred million Parquet rows used to mean fetching a hundred million Parquet rows:
3947
+ the adapter declared no <code>group</code> capability, so the push router never sent it a
3948
+ grouped request and the grid grouped whatever it held. <code>duckdbAdapter</code> now declares
3949
+ <code>group: true</code> and answers the grouped view with <code>GROUP BY</code> — one
3950
+ statement per grid level, paged like any other window.
3951
+ </p>
3952
+ <p class="section-note">
3953
+ Nothing is configured. Group a grid over a DuckDB source and the grouping is pushed:
3954
+ </p>
3955
+ <pre><code><span class="kw">const</span> grid = createGrid(el, {
3956
+ columns: [
3957
+ { id: <span class="str">'region'</span> },
3958
+ { id: <span class="str">'tier'</span> },
3959
+ { id: <span class="str">'amount'</span>, type: <span class="str">'number'</span>, total: <span class="str">'sum'</span> },
3960
+ ],
3961
+ groupBy: [<span class="str">'region'</span>, <span class="str">'tier'</span>],
3962
+ source: createPushdownSource({
3963
+ adapter: duckdbAdapter({ connection, from: <span class="str">"read_parquet('s3://bucket/sales/*.parquet')"</span> }),
3964
+ }),
3965
+ });</code></pre>
3966
+ <p class="section-note">
3967
+ The root level is one statement — <code>SELECT "region", count(*), sum("amount") … GROUP BY
3968
+ "region" ORDER BY "region" ASC NULLS LAST LIMIT ? OFFSET ?</code> — so the group rows on screen
3969
+ cost a grouped scan and no leaf crosses the wire. Expanding a group narrows the next level by
3970
+ its parent's key; expanding the deepest one runs the ordinary row query with the same predicate
3971
+ ANDed on, so the leaves arrive paged and sorted exactly as they would without grouping. Every
3972
+ identifier goes through the same validation the row query uses and every value is bound, so the
3973
+ grouped path is no more exposed than the read path.
3974
+ </p>
3975
+ <p class="section-note">
3976
+ <strong>Subtotals come from the engine, and a statistic it cannot express shows nothing rather
3977
+ than something.</strong> Each totalled column contributes one aggregate expression, taken from
3978
+ the same verified pushdown map the statistics panel uses (see
3979
+ <a href="#pushdown-aggregates">pushing statistics down</a>) — <code>sum</code>,
3980
+ <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, the quantiles, and the
3981
+ rest. A column whose total is a host function, a two-column statistic such as
3982
+ <code>weightedAvg</code> (a grouped request carries no weight column), and the one genuine
3983
+ fallback <code>weightedQuantile</code> are <em>not</em> sent, are named in
3984
+ <code>source.lastPlan().aggregates.client</code> with the reason, and warn once. The group row
3985
+ then carries no value for that column. That is deliberate: the leaves of an unexpanded group
3986
+ are not in the browser, so the only alternative to the engine's figure is a figure computed
3987
+ over something that is not the group.
3988
+ </p>
3989
+ <p class="section-note">
3990
+ <strong>The counts a grouped grid shows are the engine's.</strong> Under grouping the display
3991
+ count is group headers plus whatever is expanded, which is how a grid showing three rows under
3992
+ one group once reported &ldquo;4 of 3&rdquo;. The root level's fetch therefore also asks for
3993
+ <code>count(*)</code> over the matching set and the grand total over it, in one extra
3994
+ statement, and an unfiltered <code>count(*)</code> once per adapter (on a Parquet file that is
3995
+ a footer read, not a scan). <code>rows.matchCount()</code> and <code>rows.totalCount()</code>
3996
+ read them, and <code>grandTotalRow: 'bottom'</code> draws its row from them.
3997
+ </p>
3998
+ <p class="section-note">
3999
+ <strong>Group order, and the collation decision.</strong> Group rows are ordered by their own
4000
+ key, ascending unless the sort names that column, which is exactly what the grid does to
4001
+ sibling group rows in memory — a sort naming some other column does not reorder groups in
4002
+ either. Absent keys sort last ascending and first descending, written as explicit
4003
+ <code>NULLS LAST</code>/<code>NULLS FIRST</code> rather than left to the connection's
4004
+ <code>default_null_order</code>. <strong>No <code>COLLATE</code> is written and the ICU
4005
+ extension is not loaded</strong>: the grid compares group keys with <code>&lt;</code> on the
4006
+ JavaScript string (UTF-16 code-unit order) and DuckDB's default <code>VARCHAR</code> ordering
4007
+ is UTF-8 byte order, and the two agree for every character in the Basic Multilingual Plane.
4008
+ They part only for supplementary-plane characters (emoji, CJK extension B and above) compared
4009
+ against U+E000–U+FFFF. An ICU collation would disagree with the grid everywhere instead, so
4010
+ binary ordering is the pin. Practically: <code>'North'</code> sorts before <code>'north'</code>
4011
+ in both.
4012
+ </p>
4013
+ <p class="section-note">
4014
+ <strong>When grouping is <em>not</em> pushed, it says so.</strong> Grouping is all or nothing —
4015
+ group rows counted over the wrong set are wrong rows, not slow ones — so the whole level is
4016
+ refused if anything else in the query stayed behind: a filter that did not fully push, a sort
4017
+ the engine could not take, a quick search, a host <code>where</code> predicate, a grouping key
4018
+ that is not a plain column, or <code>fullDataset</code> (which holds the whole set and groups it
4019
+ client-side on purpose). <code>source.lastPlan()</code> then reports
4020
+ <code>grouped: false</code>, <code>'group'</code> in <code>unpushed</code> and a
4021
+ <code>groupReason</code> sentence, and a one-time warning names it.
4022
+ </p>
4023
+ <p class="section-note">
4024
+ <strong>One known difference from a memory grid.</strong> A column holding both
4025
+ <code>NULL</code> and the empty string produces two groups in the engine and one in a memory
4026
+ grid, because the grid keys its group nodes on a display path where an absent value and an
4027
+ empty string are both <code>''</code>. The engine's answer is the right one; the two are
4028
+ otherwise identical group for group, count for count and subtotal for subtotal, which the
4029
+ parity suite asserts at every level.
4030
+ </p>
4031
+
3735
4032
  <h5 id="dfql-options"><code>dfqlAdapter</code></h5>
3736
4033
  <div class="table-wrap">
3737
4034
  <table>
@@ -4959,6 +5256,42 @@ grid.destroy();
4959
5256
  naming the span and how old the newest reading is, so a dead feed reads as no data rather
4960
5257
  than as a chart that quietly stopped moving.
4961
5258
  </p>
5259
+ <p class="section-note">
5260
+ <strong>The x scale comes from the column's type, and nothing else</strong>
5261
+ (BACKLOG-0001344). A temporal type &mdash; <code>date</code>, <code>datetime</code>,
5262
+ <code>timestamp</code> or <code>dateString</code> &mdash; draws a time axis; a numeric type
5263
+ draws a linear axis <em>whatever its distinct count</em>; every other type draws bands, one
5264
+ per distinct value. A declared numeric or temporal column is therefore never demoted to a
5265
+ band, which it used to be below thirteen distinct values &mdash; silently turning off
5266
+ <code>fit</code>, <code>band</code> and everything else that needs a continuous x. Where the
5267
+ column's type is not what you want, pin the scale with
5268
+ <code>axis: { x: { scale: 'band' | 'linear' | 'time' } }</code>: <code>'band'</code> is how a
5269
+ numeric code column (a quarter, a rating, a star count) asks for its bands back, and
5270
+ <code>'time'</code> or <code>'linear'</code> lifts a column the grid types as
5271
+ <code>text</code> onto a continuous axis. That last case is worth knowing about: a grid built
5272
+ with <strong>no rows</strong> infers <code>text</code> and does not revisit it when rows
5273
+ arrive, so a streamed date column binds bands where the same column beside a populated grid
5274
+ binds time. The chart warns once when it bands a column whose values all read as dates or as
5275
+ numbers, naming the column, its type and the option that overrules it. A band scale always
5276
+ draws: a line, area or step on one is drawn through the band centres, and its labels are
5277
+ thinned to the pitch the font can be read at rather than one per row
5278
+ (<code>axis.x.every</code> overrides the count).
5279
+ </p>
5280
+ <p class="section-note">
5281
+ <strong>The margin a chart leaves for its labels is measured from the labels</strong>
5282
+ (BACKLOG-0001343). Where a chart names its rows down the left &mdash; a
5283
+ <code>correlogram</code>'s columns, a <code>horizontalBar</code>'s categories, a
5284
+ <code>gantt</code>'s tasks, a <code>forest</code>'s coefficients, a <code>heatmap</code>'s
5285
+ rows &mdash; the gutter is the widest name it will draw, <strong>capped at two fifths of the
5286
+ chart</strong>. Past that cap a name is ellipsised and keeps the whole of itself as
5287
+ <code>aria-label</code>, so a screen reader announces the real name and the glyphs still stop
5288
+ inside the chart; no label is ever cut mid-glyph, and the gutter is re-measured whenever the
5289
+ chart is resized. Widening the chart therefore gives the names more room, which is the thing
5290
+ a fixed margin could not do. Where the left-hand gutter holds a <em>measure</em> axis the
5291
+ room is estimated from five digits instead, because the numbers on it are not known until
5292
+ after the plot has been laid out. <code>margin: { left }</code> is added to whatever the
5293
+ labels need rather than competing with it.
5294
+ </p>
4962
5295
 
4963
5296
  <h3>The chart</h3>
4964
5297
  <div class="table-wrap">
@@ -5062,7 +5395,7 @@ createGrid(el, {
5062
5395
  explicit-bound error-bar primitive. A preset a given model cannot support (no multicollinearity
5063
5396
  for one predictor, no band for several) is returned as a null spec carrying a machine-readable
5064
5397
  reason rather than silently dropped.</p>
5065
- <pre data-run="js" data-expect="scatter|5|scatter|qq|bubble|cook|scatter|forest" data-covers="export:regressionPlots"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
5398
+ <pre data-run="js" data-expect="scatter|5|scatter|qq|bubble|cook|scatter|forest" data-covers="export:regressionPlots config:columnarBelow"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
5066
5399
  <span class="kw">const</span> { regressionPlots } = <span class="kw">await</span> import('../packages/modules/charts/index.js');
5067
5400
 
5068
5401
  <span class="cmt">// x=1..5, y=2,4,5,4,5. Shadow columns carry the model's per-row diagnostics —</span>
@@ -5103,8 +5436,52 @@ createGrid(el, {
5103
5436
  ].join('|');</code></pre>
5104
5437
 
5105
5438
  <h3>Maps</h3>
5106
- <p>A <code>geomap</code> takes an ISO code from one column and a value from another. Alpha-2, alpha-3 and numeric codes are all accepted, and continent codes draw a continent map without any outline data. Country outlines are yours to supply through <code>shapes</code>, because a world atlas is larger than the whole library and this package fetches nothing at runtime.</p>
5107
- <p>Codes that match nothing are counted and reported on the chart rather than dropped, a map missing half its data looks exactly like a map of a world where half the data is zero. The full code tables are in <a href="CHART-CODES.md">CHART-CODES.md</a>.</p>
5439
+ <p>A <code>geomap</code> takes an ISO code from one column and a value from another. Alpha-2, alpha-3 and numeric codes are all accepted, and continent codes draw a continent map without any outline data. Codes that match nothing are counted and reported on the chart rather than dropped, a map missing half its data looks exactly like a map of a world where half the data is zero. The full code tables are in <a href="CHART-CODES.md">CHART-CODES.md</a>.</p>
5440
+
5441
+ <h4 id="geometry-packs">Real outlines: geometry packs</h4>
5442
+ <p><strong>A pack is an optional module you import only if you draw that map.</strong> Real boundaries are tens to hundreds of kilobytes, so none of them are in the charts bundle and the grid still fetches nothing at runtime: you import the pack you want, exactly as you import an extension chart type, and hand it to <code>shapes</code>. Each pack is <a href="https://github.com/topojson/topojson-specification">TopoJSON</a> — quantised and delta-encoded, decoded by the chart — generated from the published source below by <code>tools/build-geo-packs.mjs</code>, and it carries its own provenance: the source URL, the version, the date it was retrieved, the licence, and the attribution line that licence requires.</p>
5443
+ <pre><code><span class="kw">import</span> { pack } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/geo-world-110m';
5444
+
5445
+ createChart({ grid, container: '#map', type: 'geomap', code: 'iso', y: 'revenue',
5446
+ shapes: pack }); <span class="cmt">// Equal Earth, fitted to the pack</span></code></pre>
5447
+ <div class="table-wrap">
5448
+ <table>
5449
+ <thead><tr><th>Module</th><th>Regions</th><th>Source and licence</th><th>Size (gzipped)</th><th>Attribution required</th></tr></thead>
5450
+ <tbody>
5451
+ <tr><td class="sig">modules/geo-world-110m</td><td>177 countries</td><td>Natural Earth 1:110m Admin 0, via world-atlas — public domain</td><td>39&nbsp;KB</td><td>None</td></tr>
5452
+ <tr><td class="sig">modules/geo-world-50m</td><td>241 countries</td><td>Natural Earth 1:50m Admin 0, via world-atlas — public domain</td><td>225&nbsp;KB</td><td>None</td></tr>
5453
+ <tr><td class="sig">modules/geo-us-states</td><td>50 states + DC</td><td>US Census cartographic boundaries, via us-atlas — public domain</td><td>36&nbsp;KB</td><td>None</td></tr>
5454
+ <tr><td class="sig">modules/geo-europe-nuts</td><td>NUTS 0–2</td><td>Eurostat GISCO NUTS 2021 1:20m — free re-use with attribution</td><td>95&nbsp;KB</td><td><code>© EuroGeographics for the administrative boundaries</code></td></tr>
5455
+ <tr><td class="sig">modules/geo-uk</td><td>9 regions, 361 local authorities, 650 constituencies</td><td>ONS Open Geography, generalised clipped — Open Government Licence v3.0</td><td>292&nbsp;KB</td><td><code>Contains OS data © Crown copyright and database right 2026; Source: Office for National Statistics licensed under the Open Government Licence v.3.0</code></td></tr>
5456
+ </tbody>
5457
+ </table>
5458
+ </div>
5459
+ <p><strong>Joining.</strong> A pack is keyed by the code its source is published under and by the codes that source also knows: the world packs by ISO alpha-2, with alpha-3 and numeric accepted; <code>geo-us-states</code> by the two-letter USPS abbreviation, with the FIPS code accepted; <code>geo-europe-nuts</code> by NUTS id (<code>DE</code>, <code>DE1</code>, <code>DE11</code>); <code>geo-uk</code> by ONS code (<code>E12000007</code>). A region the pack does not know is reported as unmatched exactly as before.</p>
5460
+ <p><strong>Choosing a grain.</strong> <code>geo-uk</code> ships three layers in one module because they share a coastline: <code>layer: 'regions'</code> (the default), <code>'local-authorities'</code> or <code>'constituencies'</code>.</p>
5461
+ <p><strong>Two notes from the data, not from us.</strong> Natural Earth at 1:110m leaves out the micro-states — Singapore, Malta, Monaco have no outline at that scale — so bind country-level data to <code>geo-world-50m</code> if those matter. And <code>geo-us-states</code> places Alaska and Hawaii at their true longitudes rather than in the insets an Albers&nbsp;USA composite uses, so a map of all 51 spans the Pacific; the five US territories are left out of the pack for the same reason.</p>
5462
+
5463
+ <h4 id="map-projections">Projections</h4>
5464
+ <p>Every map names a projection, and a pack declares the right one for itself, so <code>shapes: pack</code> alone gives a sensible map. <code>projection</code> overrides it; <code>projectionOptions</code> passes <code>parallels</code> and <code>centre</code> to the two that take them. The drawn geometry is then <strong>fitted</strong> to the panel, so a map of the UK fills its box rather than sitting inside the whole globe's.</p>
5465
+ <div class="table-wrap">
5466
+ <table>
5467
+ <thead><tr><th>Name</th><th>What it is</th><th>Use it for</th></tr></thead>
5468
+ <tbody>
5469
+ <tr><td class="sig">equalEarth</td><td>Equal-area (Šavrič, Patterson &amp; Jenny 2018)</td><td>A world map — the default. Areas are honest and the shapes are recognisable.</td></tr>
5470
+ <tr><td class="sig">robinson</td><td>Compromise, tabulated</td><td>A world map where the poles matter more than area.</td></tr>
5471
+ <tr><td class="sig">mercator</td><td>Conformal, cut at ±85.05°</td><td>Matching a web-map basemap.</td></tr>
5472
+ <tr><td class="sig">albers</td><td>Conic equal-area, two standard parallels</td><td>A country or continent in the mid-latitudes — the US and Europe packs default to it.</td></tr>
5473
+ <tr><td class="sig">transverseMercator</td><td>Conformal about a central meridian</td><td>A tall, narrow country. The UK pack defaults to it through 2°W, which is what stands Britain upright.</td></tr>
5474
+ <tr><td class="sig">equirectangular</td><td>Longitude and latitude straight onto x and y</td><td>Back-compatibility: the projection every map here drew before 1.63.</td></tr>
5475
+ </tbody>
5476
+ </table>
5477
+ </div>
5478
+ <pre><code>createChart({ grid, container: '#uk', type: 'geomap', code: 'lad', y: 'claims',
5479
+ shapes: ukPack, layer: 'local-authorities' }); <span class="cmt">// transverse Mercator</span>
5480
+
5481
+ createChart({ grid, container: '#us', type: 'geomap', code: 'state', y: 'sales',
5482
+ shapes: usPack, projection: 'albers',
5483
+ projectionOptions: { parallels: [29.5, 45.5], centre: [-96, 37.5] } });</code></pre>
5484
+ <p><strong>The antimeridian is handled in the chart.</strong> A country whose outline crosses ±180° — Russia, Fiji, New Zealand's Chathams — is split there before it is projected, so it draws as the parts it is rather than as a band running the wrong way across the map. Antarctica is cropped until it carries a value, by its country code <code>AQ</code> as well as the continent code <code>AN</code>.</p>
5108
5485
 
5109
5486
  <div class="note"><p>The module imports nothing from the grid: <code>createChart</code> is handed a grid rather than importing one. That is what keeps the charts bundle to the drawing, and it is why the grid must be created first, and why a chart cannot outlive it.</p></div>
5110
5487
 
@@ -5781,6 +6158,22 @@ gantt.mount(document.querySelector('#plan'), {
5781
6158
  width: 'container', // the default: fill the container, and keep following it
5782
6159
  });</code></pre>
5783
6160
  <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 &mdash; a hidden tab, or an element that has not been laid out yet &mdash; 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 &mdash; 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> &mdash; it fixes the pixels-per-day and lets the plot scroll past the container &mdash; and passing both now warns rather than discarding the <code>width</code> in silence.</p>
6161
+ <p><strong>The time axis thins its own labels (BACKLOG-0001319).</strong> A Gantt picks its tick
6162
+ rhythm from the calendar &mdash; every day at <code>zoom: 'day'</code>, every seven days at
6163
+ <code>'week'</code>, every month above that &mdash; and that rhythm knows nothing about how wide
6164
+ a date is. Where a label is wider than the gap between two ticks, only <strong>every nth</strong>
6165
+ label is drawn: the largest regular stride that leaves at least 6px of clear space between
6166
+ neighbours, so the axis keeps an even rhythm a reader can count on rather than the uneven gaps a
6167
+ greedy left-to-right fit would leave. <strong>The gridlines and the split view's week separators
6168
+ are not thinned with the text</strong> &mdash; the fine rhythm is information and costs nothing to
6169
+ read; it was only ever the text that collided. Both surfaces do it: <code>mount</code>'s axis and
6170
+ <code>mountSplit</code>'s timeline header, which is where a week-zoom header used to read
6171
+ <code>Sun 05 Oct 2025 Sun 12 Oct 2025 Sun 19 Oct 2025</code> with each date painted across the
6172
+ next. Widening the Gantt now shows more dates rather than the same overlap, and the thinning is
6173
+ re-decided on every draw, so a resize or a zoom change re-fits the labels. An <strong>un-zoomed</strong>
6174
+ axis is untouched: it already chooses how many ticks to draw from the plot's own width, so its
6175
+ labels always fitted, and a host that passed an explicit <code>width</code> keeps exactly the
6176
+ drawing it had.</p>
5784
6177
  <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&hellip;</code>) legitimately renders as January 1970 &mdash; 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 &mdash; <code>'2026-03-02'</code> is 2 March in Sydney and in New York alike &mdash; 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 &mdash; 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 &mdash; 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>
5785
6178
  <pre><code>// A relative plan: offsets in the data, real dates on the screen.
5786
6179
  gantt.mount(el, {
@@ -6723,6 +7116,15 @@ createGrid(el, {
6723
7116
  versions give one column <code>layout: { flex: 1 }</code> so the cells reach the edge and there
6724
7117
  is no tail to click.</p>
6725
7118
 
7119
+ <p><strong>Expand all</strong> and <strong>Collapse all</strong> are built-in row/grid items
7120
+ (BACKLOG-0001305): present, in this order, on the row context menu and on every column's
7121
+ header menu (the 3-dot button and a right-click on the heading) whenever the grid is grouped,
7122
+ hidden rather than disabled otherwise. Each drives the public
7123
+ <code>grid.rows.expandAll()</code> / <code>collapseAll()</code>, is matched by its translated
7124
+ <code>name</code> like any other built-in item (catalogue keys <code>menu.expandAll</code> /
7125
+ <code>menu.collapseAll</code>), and passes through the same <code>contextMenu</code> /
7126
+ <code>columnMenu</code> chain above.</p>
7127
+
6726
7128
  <p><code>columnMenu</code> takes the same form for the header's menu: both the 3-dot button
6727
7129
  and a right-click on a heading. Its <code>params</code> is
6728
7130
  <code>{ colId, column, grid }</code>. Anything of your own that you put on a column definition
@@ -7291,7 +7693,7 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
7291
7693
 
7292
7694
  <h3 id="module-exports-example">Every module export, executed</h3>
7293
7695
  <p class="section-note">Every shipped module’s exports, resolved against its own barrel on every build.</p>
7294
- <pre data-run="js" data-expect="67" data-covers="export:ContextMenu export:Messages export:Registry export:autoInit export:createGrid export:createLocalViewStorage export:createMessages export:createStat export:deltaOf export:gridElementsWithin export:hydrateTable export:mountPanel export:readTable export:toneOf export:Chart export:PALETTE export:SCHEMES export:TYPES export:createChart export:chartRange export:canChartRange export:deriveRangeSpec export:regressionPlots export:registerScheme export:resolveScheme export:schemeNames export:setDefaultScheme export:HTML_ROW_WARNING_THRESHOLD export:QUERY_CHANGED_EVENT export:SCROLL_NEAR_END_EVENT export:attach export:destroyWithin export:driveInfiniteScroll export:driveOobUpdates export:driveServerMode export:ingestResponse export:initWithin export:queryParams export:restoreStateWithin export:rowsFromFragment export:rowsFromJson export:saveStateWithin export:ATTRIBUTE_CONFIG export:EVENT_PREFIX export:GridElementController export:LatticeGrid export:TAG_NAME export:createLatticeGridElement export:defineLatticeGrid export:domEventName export:observedAttributeNames export:CONSOLE_ACTIVATION export:createDevtools export:expose export:EVENT_NAMES export:handlerName export:createLatticeGrid export:dashedName export:createLatticeAction export:Grid export:warnIfLargeHtmlPayload export:createDataRouter export:MockWebSocket export:rng export:opsFeed export:priceFeed export:createKanban"><code><span class="cmt">// Every declared export of every shipped module, resolved against its own</span>
7696
+ <pre data-run="js" data-expect="68" data-covers="export:ContextMenu export:Messages export:Registry export:autoInit export:createGrid export:createLocalViewStorage export:createMessages export:createStat export:deltaOf export:gridElementsWithin export:hydrateTable export:mountPanel export:readTable export:toneOf export:Chart export:PALETTE export:SCHEMES export:TYPES export:createChart export:chartRange export:canChartRange export:deriveRangeSpec export:regressionPlots export:registerScheme export:resolveScheme export:schemeNames export:setDefaultScheme export:HTML_ROW_WARNING_THRESHOLD export:QUERY_CHANGED_EVENT export:SCROLL_NEAR_END_EVENT export:attach export:destroyWithin export:driveInfiniteScroll export:driveOobUpdates export:driveServerMode export:ingestResponse export:initWithin export:queryParams export:restoreStateWithin export:rowsFromFragment export:rowsFromJson export:saveStateWithin export:ATTRIBUTE_CONFIG export:EVENT_PREFIX export:GridElementController export:LatticeGrid export:TAG_NAME export:createLatticeGridElement export:defineLatticeGrid export:domEventName export:observedAttributeNames export:CONSOLE_ACTIVATION export:createDevtools export:expose export:EVENT_NAMES export:handlerName export:createLatticeGrid export:dashedName export:createLatticeAction export:Grid export:warnIfLargeHtmlPayload export:createDataRouter export:MockWebSocket export:rng export:opsFeed export:priceFeed export:createKanban export:pack"><code><span class="cmt">// Every declared export of every shipped module, resolved against its own</span>
7295
7697
  <span class="cmt">// barrel. A module that stopped exporting something fails here.</span>
7296
7698
  <span class="kw">const</span> modules = [
7297
7699
  [<span class="kw">await</span> import('../packages/dom/src/index.js'), [
@@ -7341,6 +7743,9 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
7341
7743
  [<span class="kw">await</span> import('../packages/modules/kanban/index.js'), [
7342
7744
  'createKanban',
7343
7745
  ]],
7746
+ [<span class="kw">await</span> import('../packages/modules/geo-world-110m/index.js'), [
7747
+ 'pack',
7748
+ ]],
7344
7749
  ];
7345
7750
 
7346
7751
  <span class="kw">let</span> present = 0;
@@ -8336,6 +8741,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8336
8741
  <tr><td class="name">format</td><td class="type">string | ((value: unknown) =&gt; string)</td><td class="desc">A format mask, or a function of the value. <small>(optional)</small></td></tr>
8337
8742
  <tr><td class="name">grid</td><td class="type">boolean</td><td class="desc">Draw the gridlines this axis owns. Default true for the measure axis. <small>(optional)</small></td></tr>
8338
8743
  <tr><td class="name">labels</td><td class="type">boolean</td><td class="desc">Draw the tick labels. <small>(optional)</small></td></tr>
8744
+ <tr><td class="name">scale</td><td class="type">'auto' | 'linear' | 'time' | 'band' | 'category'</td><td class="desc">Pin the x axis's scale rather than taking it from the column's type (BACKLOG-0001344). The default, `'auto'`, is the rule stated in the charts section: a temporal column type (`date`, `datetime`, `timestamp`, `dateString`) draws a time axis, a numeric one draws a linear axis whatever its distinct count, and everything else draws bands. `'band'` is how a numeric code column — a quarter, a rating, a star count — asks for its bands back; `'linear'` and `'time'` put a column the grid types as text onto a continuous axis. Only the x axis reads it. <small>(optional)</small></td></tr>
8339
8745
  <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>
8340
8746
  <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>
8341
8747
  <tr><td class="name">window</td><td class="type">Pick&lt;WindowSpec, 'kind' | 'span'&gt;</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>
@@ -8404,8 +8810,12 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8404
8810
  <tr><td class="name">annotations</td><td class="type">ChartAnnotation[]</td><td class="desc">The declarative annotation layer: reference and target lines, shaded bands and callouts, each naming the axis it reads and each described into the accessible table as a sentence. A value may be a constant or `compute`d from the data it annotates, so it follows the chart as the grid is filtered. <small>(optional)</small></td></tr>
8405
8811
  <tr><td class="name">buckets</td><td class="type">number</td><td class="desc">Bins for a histogram; the default is twelve. <small>(optional)</small></td></tr>
8406
8812
  <tr><td class="name">diverging</td><td class="type">boolean</td><td class="desc">A diverging colour ramp, for heatmap and geomap. <small>(optional)</small></td></tr>
8407
- <tr><td class="name">shapes</td><td class="type">unknown</td><td class="desc">Country outlines, for a geomap drawing countries rather than continents. <small>(optional)</small></td></tr>
8813
+ <tr><td class="name">shapes</td><td class="type">unknown</td><td class="desc">Country outlines, for a geomap drawing countries rather than continents. Either GeoJSON, an object of code to SVG path data, or a geometry {@link GeoPack} imported from an optional `modules/geo-*` package (BACKLOG-0001321) — as the pack itself, or as `{ pack: id }` once its module has been imported and registered. <small>(optional)</small></td></tr>
8408
8814
  <tr><td class="name">codeProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
8815
+ <tr><td class="name">layer</td><td class="type">string</td><td class="desc">Which layer of a multi-layer geometry pack to draw — the UK pack, for instance, ships `regions`, `local-authorities` and `constituencies` together (BACKLOG-0001321). Ignored for a single-layer pack. <small>(optional)</small></td></tr>
8816
+ <tr><td class="name">projection</td><td class="type">'equalEarth' | 'robinson' | 'mercator' | 'equirectangular' | 'albers'</td><td class="desc">The map projection a geomap draws through (BACKLOG-0001321): `'equalEarth'` (the default for a world), `'robinson'`, `'mercator'`, `'equirectangular'`, `'albers'`, `'transverseMercator'`, or a projection function of the caller's own `(lon: number, lat: number) =&gt; [number, number]`. Left unset, a geometry pack draws through the projection it declares. <small>(optional)</small></td></tr>
8817
+ <tr><td class="name">projectionOptions</td><td class="type">{ parallels?: [number, number]; centre?: [number, number] }</td><td class="desc">Parameters for the projections that take them: `parallels` and `centre` for `albers`, `centre` for `transverseMercator` (BACKLOG-0001321). <small>(optional)</small></td></tr>
8818
+ <tr><td class="name">graticule</td><td class="type">boolean | { step?: number }</td><td class="desc">A lon/lat reference grid under a geomap's regions, off by default (BACKLOG-0001321 part 2). Only drawn over a geometry pack's fitted projection — the schematic continents have no fitted projection to draw one against. `step` is the spacing between lines in degrees (default 30). <small>(optional)</small></td></tr>
8409
8819
  <tr><td class="name">multiples</td><td class="type">string</td><td class="desc">One chart per distinct value of this column. <small>(optional)</small></td></tr>
8410
8820
  <tr><td class="name">canvas</td><td class="type">boolean | number</td><td class="desc">Draw to canvas past this many points. <small>(optional)</small></td></tr>
8411
8821
  <tr><td class="name">downsample</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
@@ -10113,6 +10523,26 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10113
10523
  </tbody>
10114
10524
  </table>
10115
10525
  </div>
10526
+ <h3 id="type-GeoPack">GeoPack</h3>
10527
+ <p class="section-note">An optional geometry pack for a geomap, as one of the `modules/geo-*` packages exports (BACKLOG-0001321). Generated at build time from a named public source; `source`, `licence` and `attribution` record where the geometry came from and what its licence requires. A single-layer pack carries `topology` directly; a multi-layer pack (the UK) carries `layers` instead, keyed by layer name, each with its own `topology`.</p>
10528
+ <div class="table-wrap">
10529
+ <table>
10530
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10531
+ <tbody>
10532
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10533
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc"></td></tr>
10534
+ <tr><td class="name">kind</td><td class="type">string</td><td class="desc"></td></tr>
10535
+ <tr><td class="name">projection</td><td class="type">ChartSpec['projection']</td><td class="desc"><small>(optional)</small></td></tr>
10536
+ <tr><td class="name">projectionOptions</td><td class="type">ChartSpec['projectionOptions']</td><td class="desc"><small>(optional)</small></td></tr>
10537
+ <tr><td class="name">source</td><td class="type">{ name: string; url: string; version: string; retrieved: string }</td><td class="desc"></td></tr>
10538
+ <tr><td class="name">licence</td><td class="type">{ name: string; url: string }</td><td class="desc"></td></tr>
10539
+ <tr><td class="name">attribution</td><td class="type">string</td><td class="desc">The attribution line the licence requires, verbatim, or `''` when it asks for none.</td></tr>
10540
+ <tr><td class="name">topology</td><td class="type">object</td><td class="desc"><small>(optional)</small></td></tr>
10541
+ <tr><td class="name">layers</td><td class="type">Record&lt;string, { name: string; topology: object }&gt;</td><td class="desc"><small>(optional)</small></td></tr>
10542
+ <tr><td class="name">defaultLayer</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10543
+ </tbody>
10544
+ </table>
10545
+ </div>
10116
10546
  <h3 id="type-Grid">Grid</h3>
10117
10547
  <div class="table-wrap">
10118
10548
  <table>
@@ -11115,6 +11545,54 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11115
11545
  </tbody>
11116
11546
  </table>
11117
11547
  </div>
11548
+ <h3 id="type-LatticeGridHandle">LatticeGridHandle</h3>
11549
+ <p class="section-note">The live instance a `&lt;LatticeGrid&gt;` ref exposes; `null` before mount.</p>
11550
+ <div class="table-wrap">
11551
+ <table>
11552
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11553
+ <tbody>
11554
+ <tr><td class="name">grid</td><td class="type">Grid | null</td><td class="desc"><small>(read-only)</small></td></tr>
11555
+ </tbody>
11556
+ </table>
11557
+ </div>
11558
+ <h3 id="type-LatticeTabSpec">LatticeTabSpec</h3>
11559
+ <p class="section-note">One tab of a `&lt;LatticeTabs&gt;`; `content` makes it React's rather than the module's.</p>
11560
+ <div class="table-wrap">
11561
+ <table>
11562
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11563
+ <tbody>
11564
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11565
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11566
+ <tr><td class="name">content</td><td class="type">unknown | (() =&gt; unknown)</td><td class="desc">A React element, or a function returning one, rendered through a portal. <small>(optional)</small></td></tr>
11567
+ </tbody>
11568
+ </table>
11569
+ </div>
11570
+ <h3 id="type-LatticeViewerCommonProps">LatticeViewerCommonProps</h3>
11571
+ <p class="section-note">What every viewer component takes beyond its own configuration (BACKLOG-0001307): the grid it binds to, which published grid to take when that is left off, the lifecycle callbacks, and the host-element props.</p>
11572
+ <div class="table-wrap">
11573
+ <table>
11574
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11575
+ <tbody>
11576
+ <tr><td class="name">grid</td><td class="type">Grid | null</td><td class="desc">The grid this viewer is built against; taken from context when absent. <small>(optional)</small></td></tr>
11577
+ <tr><td class="name">gridName</td><td class="type">string</td><td class="desc">Which published grid to take from context; `'default'` when absent. <small>(optional)</small></td></tr>
11578
+ <tr><td class="name">onReady</td><td class="type">(instance: Instance) =&gt; void</td><td class="desc">Told when the viewer exists. <small>(optional)</small></td></tr>
11579
+ <tr><td class="name">onDestroy</td><td class="type">() =&gt; void</td><td class="desc">Told just before it is destroyed. <small>(optional)</small></td></tr>
11580
+ <tr><td class="name">className</td><td class="type">string</td><td class="desc">Applied to the host element rather than to the viewer. <small>(optional)</small></td></tr>
11581
+ <tr><td class="name">style</td><td class="type">Record&lt;string, unknown&gt;</td><td class="desc">Applied to the host element rather than to the viewer. <small>(optional)</small></td></tr>
11582
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc">Applied to the host element rather than to the viewer. <small>(optional)</small></td></tr>
11583
+ </tbody>
11584
+ </table>
11585
+ </div>
11586
+ <h3 id="type-LatticeViewerHandle">LatticeViewerHandle</h3>
11587
+ <p class="section-note">The live instance a viewer component's ref exposes; `null` before mount.</p>
11588
+ <div class="table-wrap">
11589
+ <table>
11590
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11591
+ <tbody>
11592
+ <tr><td class="name">instance</td><td class="type">Instance | null</td><td class="desc"><small>(read-only)</small></td></tr>
11593
+ </tbody>
11594
+ </table>
11595
+ </div>
11118
11596
  <h3 id="type-Layout">Layout</h3>
11119
11597
  <p class="section-note">A reconfigurable dashboard: a cell grid inside an element, and a set of windows on it that a user can move, resize and close by pointer or by keyboard (BACKLOG-0001108). The module is **payload-agnostic**: a window body is a container with an id, which this module creates and sizes and never reads. It tells a payload it was resized by emitting `window:resized`; it never calls into one, because it cannot know what one is.</p>
11120
11598
  <div class="table-wrap">
@@ -11373,6 +11851,17 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11373
11851
  </tbody>
11374
11852
  </table>
11375
11853
  </div>
11854
+ <h3 id="type-MemorySourceConfig">MemorySourceConfig</h3>
11855
+ <div class="table-wrap">
11856
+ <table>
11857
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11858
+ <tbody>
11859
+ <tr><td class="name">mode</td><td class="type">'memory'</td><td class="desc"></td></tr>
11860
+ <tr><td class="name">columnarBelow</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
11861
+ <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="desc">The rows a memory source opens with; equivalent to top-level `rows`, which wins if both are given. <small>(optional)</small></td></tr>
11862
+ </tbody>
11863
+ </table>
11864
+ </div>
11376
11865
  <h3 id="type-MenuItem">MenuItem</h3>
11377
11866
  <div class="table-wrap">
11378
11867
  <table>
@@ -11483,6 +11972,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11483
11972
  <tr><td class="name">notation</td><td class="type">'standard' | 'compact' | 'scientific'</td><td class="desc"><small>(optional)</small></td></tr>
11484
11973
  <tr><td class="name">negative</td><td class="type">'minus' | 'parentheses' | 'suffix'</td><td class="desc"><small>(optional)</small></td></tr>
11485
11974
  <tr><td class="name">negativeClass</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11975
+ <tr><td class="name">signed</td><td class="type">boolean</td><td class="desc">Show a leading `+` on a positive value (`+5`, `+£5.00`, `+12%`). A negative value keeps whatever `negative` says regardless of this flag, and zero shows no sign either way (BACKLOG-0001095). Off by default. <small>(optional)</small></td></tr>
11486
11976
  <tr><td class="name">prefix</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11487
11977
  <tr><td class="name">suffix</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11488
11978
  <tr><td class="name">zeroDisplay</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
@@ -11769,6 +12259,8 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11769
12259
  <tr><td class="name">name</td><td class="type">string</td><td class="desc">Used in diagnostics and in the message when work cannot be pushed. <small>(optional)</small></td></tr>
11770
12260
  <tr><td class="name">capabilities</td><td class="type">PushdownCapabilities</td><td class="desc"><small>(optional)</small></td></tr>
11771
12261
  <tr><td class="name">execute</td><td class="type">(query: RemoteRequest, request?: RemoteRequest):</td><td class="desc">Run the part of the query the adapter declared it could handle.</td></tr>
12262
+ <tr><td class="name">executeGroupLevel</td><td class="type">(</td><td class="desc">Answer one level of a grouped grid (BACKLOG-0001325). Present only when `capabilities.group` opts in. The level is `query.groupValues.length`: the root asks for the outermost grouping column's distinct values, expanding a group asks for the next column's values within it, and past the last grouping column the children are the leaves (`leaves: true`). A group row comes back in the shape the remote source already reads from a grouping server: the grouping column's own id carries the key, `leafCount` the group's row count, `totals` the subtotals keyed by column id. `total` is how many group rows the level holds. At the root, `matchCount` and `grand` carry the whole-set figures a grouped window cannot derive — the rows the filter matched, and the grand total over them. `aggregates` is the subtotal list the source routed to the engine; anything it could not route is named in `PushdownPlan.aggregates.client` and left absent from the group row rather than computed over the wrong set. <small>(optional)</small></td></tr>
12263
+ <tr><td class="name">unfilteredCount</td><td class="type">(): Promise&lt;number | null&gt;</td><td class="desc">The row count before any filter (BACKLOG-0001325) — the denominator of "1,204 of 100,000" under grouping, where the display count is group headers rather than rows. Optional; a source falls back to the display count. <small>(optional)</small></td></tr>
11772
12264
  <tr><td class="name">mutate</td><td class="type">(op: MutationOp, request?: RemoteRequest): Promise&lt;MutationResult&gt;</td><td class="desc">Persist one mutation (§4.2). Present only when `capabilities.mutate` opts in. `createPushdownSource` synthesises an `edit.commit` that calls this for cell updates (§4.3 Option A); `request` threads the abort signal through the way `execute` receives it, and auth already lives on the adapter. <small>(optional)</small></td></tr>
11773
12265
  </tbody>
11774
12266
  </table>
@@ -11796,7 +12288,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11796
12288
  <tr><td class="name">quick</td><td class="type">boolean</td><td class="desc">Whether a free-text search across columns can be pushed. <small>(optional)</small></td></tr>
11797
12289
  <tr><td class="name">range</td><td class="type">boolean</td><td class="desc">Whether the engine can return a window rather than the whole result. <small>(optional)</small></td></tr>
11798
12290
  <tr><td class="name">total</td><td class="type">boolean</td><td class="desc">Whether it can report the count of matching rows. <small>(optional)</small></td></tr>
11799
- <tr><td class="name">group</td><td class="type">boolean</td><td class="desc">Whether it can group and aggregate. <small>(optional)</small></td></tr>
12291
+ <tr><td class="name">group</td><td class="type">boolean</td><td class="desc">Whether it can answer the grid's grouped view — group rows, their counts, their subtotals and their order — one level at a time, instead of returning the leaves for the grid to group in the browser (BACKLOG-0001325). All or nothing, unlike `filter`. A filter splits because the engine narrowing a superset and the grid narrowing what is left reach the same set; a grouping cannot, because group rows counted over the wrong set are wrong rows, not slow ones. So the push router refuses the whole grouped level — and says why in `PushdownPlan.groupReason` — whenever anything else in the query failed to push. An adapter declaring this must implement `executeGroupLevel`; one that declares it without the method is re-planned without grouping and warned about, rather than half-pushed. <small>(optional)</small></td></tr>
11800
12292
  <tr><td class="name">mutate</td><td class="type">false | MutateCapability</td><td class="desc">What the adapter can persist back — the write-back contract (§4.1). `false` (the default) is read-only by declaration. A declared block opts kinds in; `capabilitiesOf` resolves it to a full `MutateCapability` (or `false`). <small>(optional)</small></td></tr>
11801
12293
  </tbody>
11802
12294
  </table>
@@ -11822,7 +12314,10 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11822
12314
  <tr><td class="name">pushed</td><td class="type">RemoteRequest</td><td class="desc">The query the adapter was given.</td></tr>
11823
12315
  <tr><td class="name">residual</td><td class="type">{</td><td class="desc">What the grid applied afterwards. `where` is the host predicate runtime when one survived the `whereRowLimit` gate, and `null` when none was registered or the gate refused it (BACKLOG-0001268).</td></tr>
11824
12316
  <tr><td class="name">needsAll</td><td class="type">boolean</td><td class="desc">Whether the whole result had to be fetched rather than a window.</td></tr>
11825
- <tr><td class="name">unpushed</td><td class="type">string[]</td><td class="desc">Which parts could not be pushed: `filter`, `sort`, `quick`, `where`.</td></tr>
12317
+ <tr><td class="name">unpushed</td><td class="type">string[]</td><td class="desc">Which parts could not be pushed: `filter`, `sort`, `quick`, `where`, `group`.</td></tr>
12318
+ <tr><td class="name">grouped</td><td class="type">boolean</td><td class="desc">Whether the engine answered the grid's grouped view for this request (BACKLOG-0001325). False for an ungrouped query and for a grouped one the engine was refused — `groupReason` says which.</td></tr>
12319
+ <tr><td class="name">groupLevel</td><td class="type">number</td><td class="desc">Which grouping level a pushed grouped request asked for: 0 at the root, 1 inside a group, and so on. Zero when nothing was grouped.</td></tr>
12320
+ <tr><td class="name">groupReason</td><td class="type">string</td><td class="desc">Why a grouped request was *not* pushed, in a sentence, or `''` when it was pushed or when nothing was grouped. Grouping is all or nothing, so this is the whole story rather than a residual.</td></tr>
11826
12321
  <tr><td class="name">full</td><td class="type">boolean</td><td class="desc">Whether the whole result was fetched because `fullDataset` is on, rather than only because residual work forced it. When true, totals and statistics reduce over the whole matching set and the windowed-stat warning is silent.</td></tr>
11827
12322
  <tr><td class="name">aggregates</td><td class="type">{</td><td class="desc">Per-aggregate provenance, present only when the last request computed aggregates (BACKLOG-0000730 Part B): which statistics the engine computed and which the client did, with the class the pushdown map assigned each. Under grouping it also carries the `groupBy` the subtotals were computed over. Build-time inspection, not a runtime per-figure marker. <small>(optional)</small></td></tr>
11828
12323
  </tbody>
@@ -12004,8 +12499,10 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12004
12499
  <tr><td class="name">protocol</td><td class="type">1</td><td class="desc"></td></tr>
12005
12500
  <tr><td class="name">range</td><td class="type">{ start: number; end: number }</td><td class="desc"></td></tr>
12006
12501
  <tr><td class="name">groupPath</td><td class="type">string[]</td><td class="desc"></td></tr>
12502
+ <tr><td class="name">groupValues</td><td class="type">unknown[]</td><td class="desc">The same ancestry as `groupPath`, but as the values the server returned rather than their display strings (BACKLOG-0001325). Always present, empty at the root, so a source can tell "no ancestors" from "a host that does not send this". `groupPath` is stringified because it is a stable *identity* for expansion state, and that is what it must stay: a numeric key `3` is `'3'` there and an absent key is `''`, indistinguishable from a group whose key really is the empty string. Useless for narrowing a query, then — which is what a grouping engine needs it for — so the typed values travel beside it.</td></tr>
12007
12503
  <tr><td class="name">groupBy</td><td class="type">ColumnRef[]</td><td class="desc"></td></tr>
12008
12504
  <tr><td class="name">totals</td><td class="type">ColumnRef[]</td><td class="desc"></td></tr>
12505
+ <tr><td class="name">totalFns</td><td class="type">Record&lt;string, string&gt;</td><td class="desc">The named statistic each totalled column reduces with — `{ amount: 'sum' }` (BACKLOG-0001325). `totals` has always said *which* columns want a subtotal and never *what*, because the client reads the reduction off the column model and a server had no way to. Only string reductions appear: a column totalling with a host function has no name to send, and naming one that merely resembles it would put a plausible wrong number on every group row. `groupTotal` wins over `total`, the same precedence the client applies for the group scope.</td></tr>
12009
12506
  <tr><td class="name">pivotBy</td><td class="type">ColumnRef[]</td><td class="desc"></td></tr>
12010
12507
  <tr><td class="name">pivotMode</td><td class="type">boolean</td><td class="desc"></td></tr>
12011
12508
  <tr><td class="name">filters</td><td class="type">FilterSet</td><td class="desc"></td></tr>
@@ -12200,6 +12697,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12200
12697
  <tr><td class="name">count</td><td class="type">(): number</td><td class="desc"></td></tr>
12201
12698
  <tr><td class="name">totalCount</td><td class="type">(): number</td><td class="desc">Rows in the source before filtering; under pagination, across every page.</td></tr>
12202
12699
  <tr><td class="name">matchCount</td><td class="type">(): number</td><td class="desc">Data rows matching the filters, excluding group, footer and total rows.</td></tr>
12700
+ <tr><td class="name">coverage</td><td class="type">(): StatCoverage</td><td class="desc">How much of the data a figure computed from this grid covers, so a statistic over a windowed source can say it is approximate.</td></tr>
12203
12701
  <tr><td class="name">data</td><td class="type">(): unknown[]</td><td class="desc"></td></tr>
12204
12702
  <tr><td class="name">forEach</td><td class="type">(fn: (row: Row, index: number) =&gt; void): void</td><td class="desc"></td></tr>
12205
12703
  <tr><td class="name">forEachAll</td><td class="type">(fn: (row: Row, index: number) =&gt; void): void</td><td class="desc">Every row in the data, before any filter. Leaf rows, in physical order.</td></tr>
@@ -12402,6 +12900,18 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12402
12900
  </tbody>
12403
12901
  </table>
12404
12902
  </div>
12903
+ <h3 id="type-StatCoverage">StatCoverage</h3>
12904
+ <p class="section-note">How much of the data a computed figure actually covers. `covered &lt; total`, or `total === null`, means the figure is approximate.</p>
12905
+ <div class="table-wrap">
12906
+ <table>
12907
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12908
+ <tbody>
12909
+ <tr><td class="name">covered</td><td class="type">number</td><td class="desc">Rows the figure was computed over.</td></tr>
12910
+ <tr><td class="name">total</td><td class="type">number | null</td><td class="desc">Rows the source knows about, or `null` when it cannot know — never a guess.</td></tr>
12911
+ <tr><td class="name">windowed</td><td class="type">boolean</td><td class="desc">True when a window bounded the computation, so the figure covers part of the data.</td></tr>
12912
+ </tbody>
12913
+ </table>
12914
+ </div>
12405
12915
  <h3 id="type-StateApi">StateApi</h3>
12406
12916
  <div class="table-wrap">
12407
12917
  <table>
@@ -12934,7 +13444,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12934
13444
  <!-- END GENERATED TYPE REFERENCE -->
12935
13445
 
12936
13446
  <footer>
12937
- Lattice Grid 1.62.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
13447
+ Lattice Grid 1.63.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
12938
13448
  This document describes the behaviour of the shipped library. Where this guide and the code
12939
13449
  disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
12940
13450
  </footer>