@toclocoinc/lattice-grid 1.62.1 → 1.63.1

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 +1 -1
  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 +870 -73
  7. package/lattice-grid.min.cjs +870 -73
  8. package/lattice-grid.min.js +870 -73
  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 +1 -1
  20. package/modules/chart-alluvial.min.js +1 -1
  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 +1 -1
  24. package/modules/chart-arc.min.js +1 -1
  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 +42 -6
  28. package/modules/chart-bubblemap.min.js +42 -6
  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 +1 -1
  32. package/modules/chart-bump.min.js +1 -1
  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 +1 -1
  36. package/modules/chart-calendar.min.js +1 -1
  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 +1 -1
  40. package/modules/chart-decomposition.min.js +1 -1
  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 +1 -1
  44. package/modules/chart-diverging.min.js +1 -1
  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 +1 -1
  48. package/modules/chart-dumbbell.min.js +1 -1
  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 +1 -1
  52. package/modules/chart-fan.min.js +1 -1
  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 +1 -1
  56. package/modules/chart-hexbin.min.js +1 -1
  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 +46 -6
  60. package/modules/chart-hexmap.min.js +46 -6
  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 +1 -1
  64. package/modules/chart-icicle.min.js +1 -1
  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 +1 -1
  68. package/modules/chart-parallel.min.js +1 -1
  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 +1 -1
  72. package/modules/chart-ridgeline.min.js +1 -1
  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 +1 -1
  76. package/modules/chart-roc.min.js +1 -1
  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 +1 -1
  80. package/modules/chart-slope.min.js +1 -1
  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 +1 -1
  84. package/modules/chart-splom.min.js +1 -1
  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 +1 -1
  88. package/modules/chart-waffle.min.js +1 -1
  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 +870 -73
  121. package/modules/htmx.min.cjs +870 -73
  122. package/modules/htmx.min.js +870 -73
  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 +76 -28
  129. package/modules/kpi.min.cjs +76 -28
  130. package/modules/kpi.min.js +76 -28
  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 +870 -73
  157. package/modules/webcomponent.min.cjs +870 -73
  158. package/modules/webcomponent.min.js +870 -73
  159. package/package.json +1 -1
@@ -63,7 +63,42 @@ hundred bytes. They are *schematic, not cartographic*: recognisable, the right
63
63
  shape for "which continent is biggest", and not a basemap. Nothing should be
64
64
  measured off them.
65
65
 
66
- **Country outlines do not ship.** That is a size decision: a usable world at
66
+ **Country outlines ship as optional packs** (1.63). A geometry pack is a module a
67
+ page imports only if it draws that map, so the charts bundle carries none of
68
+ them and the grid still fetches nothing at runtime. Each is TopoJSON — quantised
69
+ and delta-encoded — generated from a published public source by
70
+ `tools/build-geo-packs.mjs`, and each carries its own source URL, version,
71
+ retrieval date, licence and the attribution line that licence requires.
72
+
73
+ | Module | Regions | Source | Licence | Size (gz) |
74
+ |---|---|---|---|---|
75
+ | `modules/geo-world-110m` | 177 countries | Natural Earth 1:110m Admin 0, via world-atlas | Public domain | 39 KB |
76
+ | `modules/geo-world-50m` | 241 countries | Natural Earth 1:50m Admin 0, via world-atlas | Public domain | 225 KB |
77
+ | `modules/geo-us-states` | 50 states + DC | US Census, via us-atlas | Public domain | 36 KB |
78
+ | `modules/geo-europe-nuts` | NUTS levels 0–2 | Eurostat GISCO NUTS 2021 1:20m | Free re-use **with attribution** | 95 KB |
79
+ | `modules/geo-uk` | 9 regions, 361 local authorities, 650 constituencies | ONS Open Geography (BGC) | **OGL v3.0** | 292 KB |
80
+
81
+ Two of the five require an attribution line, which the pack carries and the map
82
+ renders: GISCO asks for `© EuroGeographics for the administrative boundaries`,
83
+ and the ONS for `Contains OS data © Crown copyright and database right 2026;
84
+ Source: Office for National Statistics licensed under the Open Government
85
+ Licence v.3.0`. The other three are public domain and ask for nothing.
86
+
87
+ ```js
88
+ import { pack } from '@toclocoinc/lattice-grid/modules/geo-world-110m';
89
+
90
+ createChart({ grid, container: '#map', type: 'geomap', code: 'iso', y: 'revenue',
91
+ shapes: pack });
92
+ ```
93
+
94
+ Two things the data does rather than we do: Natural Earth at 1:110m omits the
95
+ micro-states (Singapore, Malta, Monaco have no outline at that scale), so bind
96
+ country-level data to the 50m pack where those matter; and `geo-us-states`
97
+ places Alaska and Hawaii at their true longitudes rather than in an Albers-USA
98
+ composite's insets, so a map of all 51 spans the Pacific. The five US
99
+ territories are left out of that pack for the same reason.
100
+
101
+ **A host's own shapes still work.** That is a size decision too: a usable world at
67
102
  country resolution is several hundred kilobytes of path data, larger than the
68
103
  whole charts module, and it would be paid for by every page that draws a bar
69
104
  chart. A country map is drawn from shapes the host supplies, at whatever
@@ -93,6 +128,28 @@ reported as unplaced rather than rolled up.
93
128
 
94
129
  ## The projection
95
130
 
131
+ A map names its projection, and a pack declares the right one for itself, so
132
+ `shapes: pack` alone draws a sensible map. `projection` overrides it and
133
+ `projectionOptions` carries `parallels` and `centre` to the two that take
134
+ them. The drawn geometry is **fitted** to the panel, so a map of one country
135
+ fills its box rather than sitting inside the whole globe's.
136
+
137
+ | Name | What it is | Use it for |
138
+ |---|---|---|
139
+ | `equalEarth` | Equal-area (Šavrič, Patterson & Jenny 2018) | A world map — the default |
140
+ | `robinson` | Compromise, tabulated | A world map where the poles matter more than area |
141
+ | `mercator` | Conformal, cut at ±85.05° | Matching a web-map basemap |
142
+ | `albers` | Conic equal-area, two standard parallels | A country or continent in the mid-latitudes |
143
+ | `transverseMercator` | Conformal about a central meridian | A tall, narrow country: the UK through 2°W |
144
+ | `equirectangular` | Longitude and latitude onto x and y | Back-compatibility with maps drawn before 1.63 |
145
+
146
+ A ring that crosses the antimeridian — Russia, Fiji, the Chathams — is split
147
+ there before it is projected, so it draws as the parts it is rather than as a
148
+ band across the map. Antarctica is cropped until it carries a value, by the
149
+ country code `AQ` as well as the continent code `AN`.
150
+
151
+ ### Without a pack
152
+
96
153
  Equirectangular: longitude and latitude map linearly onto x and y. The least
97
154
  arithmetic and the most distortion at the poles, which is the right trade for
98
155
  shading regions by value: nothing here measures area, and a reader comparing
@@ -112,7 +169,89 @@ claims the mean is neutral.
112
169
 
113
170
  A region with no data is left in the empty colour rather than the ramp's low
114
171
  end: "no reading" and "the smallest reading" are different facts and must not
115
- share a shade.
172
+ share a shade. Over a fitted geometry pack the ramp also stays clear of its own
173
+ palest sixth, so the *lowest real reading* does not fall close enough to that
174
+ empty tone to read as the same thing — a heatmap's ramp is unaffected; its pale
175
+ end is the intended bottom of its own scale.
176
+
177
+ ## Rendering and interaction (1.63)
178
+
179
+ A region's stroke comes from `--lattice-chart-map-stroke` (falling back to the
180
+ grid-line token), drawn with `vector-effect: non-scaling-stroke` so zooming in
181
+ does not fatten it. `--lattice-chart-empty` is the fill a region with no
182
+ reading takes.
183
+
184
+ ```css
185
+ .my-app { --lattice-chart-map-stroke: #b0b8bf; }
186
+ ```
187
+
188
+ **`graticule`** draws a lon/lat reference grid under the regions, only over a
189
+ fitted geometry pack:
190
+
191
+ ```js
192
+ createChart({ grid, container: '#map', type: 'geomap', code: 'iso', y: 'sales',
193
+ shapes: pack, graticule: true }); // every 30°, or { step: 10 }
194
+ ```
195
+
196
+ **`labels: true`** names the regions that carry a reading — largest value
197
+ first, so on a crowded map the labels that survive a collision are the ones
198
+ worth having. Off by default, like every other chart's data labels, and only
199
+ over a fitted pack: the schematic continents already name themselves in the
200
+ legend and the tooltip.
201
+
202
+ **A pack's attribution** — the licence line `geo-europe-nuts` and `geo-uk`
203
+ require — is drawn under the map automatically; the three public-domain packs
204
+ draw none.
205
+
206
+ **The panel's height follows its width** when a host does not fix one: the
207
+ plot reserves an `aspect-ratio` from the geometry's own natural shape, so a
208
+ `width: 480px` container with no height gets a box shaped like the map rather
209
+ than however tall the surrounding layout happened to leave it.
210
+
211
+ **Pan, wheel-zoom and reset** work over a fitted pack, by pointer and by
212
+ keyboard: drag to pan, wheel to zoom, Shift+arrow to pan from the keyboard,
213
+ `+`/`-` to zoom, `0` to reset. Plain arrow keys are left alone — they move
214
+ keyboard focus between regions, the same roving-tabindex behaviour every
215
+ Lattice chart's marks have. Zoom is centred on the map's own fitted view
216
+ rather than on the pointer: panning and zooming need no projection's inverse
217
+ this way, and none of the six projections below has one written.
218
+
219
+ ### One example per pack
220
+
221
+ ```js
222
+ import { pack as world } from '@toclocoinc/lattice-grid/modules/geo-world-110m';
223
+ createChart({ grid, container: '#map', type: 'geomap', code: 'iso', y: 'sales',
224
+ shapes: world }); // Equal Earth, the pack's own default
225
+
226
+ import { pack as world50 } from '@toclocoinc/lattice-grid/modules/geo-world-50m';
227
+ createChart({ grid, container: '#map', type: 'geomap', code: 'iso', y: 'sales',
228
+ shapes: world50 }); // adds the micro-states 110m omits
229
+
230
+ import { pack as us } from '@toclocoinc/lattice-grid/modules/geo-us-states';
231
+ createChart({ grid, container: '#map', type: 'geomap', code: 'state', y: 'sales',
232
+ shapes: us }); // Albers; joins FIPS, USPS or ISO
233
+
234
+ import { pack as nuts } from '@toclocoinc/lattice-grid/modules/geo-europe-nuts';
235
+ createChart({ grid, container: '#map', type: 'geomap', code: 'region', y: 'sales',
236
+ shapes: nuts }); // NUTS 0-2 in one pack; joins by NUTS id
237
+
238
+ import { pack as uk } from '@toclocoinc/lattice-grid/modules/geo-uk';
239
+ createChart({ grid, container: '#map', type: 'geomap', code: 'authority', y: 'sales',
240
+ shapes: uk, layer: 'local-authorities' }); // regions | local-authorities | constituencies
241
+ ```
242
+
243
+ ### Against a bubble map or a hexbin map
244
+
245
+ `chart-bubblemap` and `chart-hexmap` take `shapes` too (1.63): the same pack, the
246
+ same projection, the outlines drawn faintly underneath for context. Without
247
+ `shapes`, both are unchanged — the points' own bounding box, scaled to the plot.
248
+
249
+ ```js
250
+ import '@toclocoinc/lattice-grid/modules/chart-bubblemap';
251
+ import { pack as us } from '@toclocoinc/lattice-grid/modules/geo-us-states';
252
+ createChart({ grid, container: '#map', type: 'bubblemap', lon: 'lng', lat: 'lat',
253
+ size: 'sales', shapes: us });
254
+ ```
116
255
 
117
256
  ---
118
257
 
@@ -437,7 +437,7 @@
437
437
  <div class="shell">
438
438
  <aside class="rail">
439
439
  <p class="rail__brand">Lattice Grid</p>
440
- <p class="rail__sub">Developer guide · v1.62.1</p>
440
+ <p class="rail__sub">Developer guide · v1.63.1</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -554,7 +554,7 @@
554
554
  <a href="API.html">reference tables</a> are the shorter version for when you already know.
555
555
  </p>
556
556
  <p class="chips">
557
- <span class="chip">Version 1.62.1</span>
557
+ <span class="chip">Version 1.63.1</span>
558
558
  <span class="chip">Zero dependencies</span>
559
559
  <span class="chip">No build step</span>
560
560
  </p>
@@ -813,6 +813,96 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
813
813
  nothing leaks.</p>
814
814
  </div>
815
815
 
816
+ <div class="example">
817
+ <p class="example__label">React: every viewer, not only the grid (1.63)</p>
818
+ <pre><code><span class="kw">import</span> React <span class="kw">from</span> 'react';
819
+ <span class="kw">import</span> ReactDOM <span class="kw">from</span> 'react-dom';
820
+ <span class="kw">import</span> { createGrid } <span class="kw">from</span> '@toclocoinc/lattice-grid';
821
+ <span class="kw">import</span> { createKPI } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/kpi';
822
+ <span class="kw">import</span> { createChart } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/charts';
823
+ <span class="kw">import</span> { createTabs } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/tabs';
824
+ <span class="kw">import</span> { createDataRouter } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/data-router';
825
+ <span class="kw">import</span> { createLatticeReact } <span class="kw">from</span> '@toclocoinc/lattice-grid/modules/react';
826
+
827
+ <span class="cmt">// createLatticeReact builds whichever components the factories you pass support.</span>
828
+ <span class="cmt">// The individual factories are there too, if you want only one:</span>
829
+ <span class="cmt">// createLatticeGrid, createLatticeKPI, createLatticeChart, createLatticeKanban,</span>
830
+ <span class="cmt">// createLatticeGantt, createLatticeLayout, createLatticeTabs,</span>
831
+ <span class="cmt">// createLatticeGridContext, createLatticeRouter, createLatticeViewer.</span>
832
+ <span class="kw">const</span> L = createLatticeReact({
833
+ React, ReactDOM, createGrid, createKPI, createChart, createTabs, createDataRouter,
834
+ });
835
+
836
+ <span class="kw">function</span> Dashboard({ rows, updates }) {
837
+ <span class="kw">const</span> router = L.useLatticeRouter(ROUTER_CONFIG);
838
+ <span class="kw">const</span> [tab, setTab] = React.useState('all');
839
+ <span class="kw">return</span> (
840
+ &lt;L.LatticeRouterProvider router={router}&gt;
841
+ &lt;L.LatticeGridProvider&gt;
842
+ &lt;L.LatticeTabs
843
+ active={tab}
844
+ onTabChanged={(e) =&gt; setTab(e.id)}
845
+ tabs={[
846
+ { id: 'all', label: 'All',
847
+ content: &lt;L.LatticeGrid name="all" route="all" {...CONFIG} rowUpdates={updates} /&gt; },
848
+ { id: 'big', label: 'Significant',
849
+ content: () =&gt; &lt;L.LatticeGrid name="big" route="significant" {...CONFIG} /&gt; },
850
+ ]}
851
+ /&gt;
852
+ &lt;L.LatticeKPI gridName="all" tiles={TILES} columns={5} /&gt;
853
+ &lt;L.LatticeChart gridName="all" type="bar" x="region" y="count" onClick={pick} /&gt;
854
+ &lt;/L.LatticeGridProvider&gt;
855
+ &lt;/L.LatticeRouterProvider&gt;
856
+ );
857
+ }</code></pre>
858
+ </div>
859
+ <div class="why">
860
+ <p><strong>Every viewer is a component now.</strong> Before 1.63 the adapter wrapped
861
+ <code>createGrid</code> and nothing else, so a React application wrote its own
862
+ <code>useEffect</code> for the KPI panel, for each chart, for the board, for the Gantt — and
863
+ its own tab strip, because the tabs module builds its tabs' grids itself and a grid built that
864
+ way is not a component. <code>createLatticeKPI</code>, <code>createLatticeChart</code>,
865
+ <code>createLatticeKanban</code>, <code>createLatticeGantt</code>,
866
+ <code>createLatticeLayout</code> and <code>createLatticeTabs</code> each return one component;
867
+ <code>createLatticeViewer</code> is the generic behind them, for anything not yet named, and
868
+ <code>createViewerController</code> is the framework-free lifecycle underneath.</p>
869
+ <p><strong>A viewer finds its grid through context, not a ref.</strong> A KPI panel and a chart
870
+ are built <em>against</em> a grid instance, and that instance does not exist until after the
871
+ first render. Writing to a ref re-renders nobody, so a sibling never learns the grid arrived.
872
+ <code>createLatticeGridContext</code> returns
873
+ <code>{ LatticeGridProvider, useLatticeGrid }</code>: a grid publishes itself under its
874
+ <code>name</code>, a viewer takes it by <code>gridName</code>, and the viewer is built the
875
+ moment it appears. A page with one grid names nothing.</p>
876
+ <p><strong>A tab's content is yours.</strong> <code>createLatticeTabs</code> keeps the module's
877
+ tablist semantics, keyboard handling, lazy first mount and live badges, and renders each tab's
878
+ React <code>content</code> into the module's own panel element through a portal — so a tab's
879
+ grid is a real <code>&lt;LatticeGrid&gt;</code> with props, a ref and the surrounding context.
880
+ A tab with no <code>content</code> is left to the module, so both kinds mix on one strip.</p>
881
+ <p><strong>The router is owned by a hook.</strong> <code>createLatticeRouter</code> returns
882
+ <code>useLatticeRouter</code>, which creates the router in an effect and destroys it in that
883
+ effect's cleanup — never in a <code>useState</code> initialiser, which React calls more than
884
+ once by design and whose discarded copy is never destroyed. It returns <code>null</code> on
885
+ the first render and the router on the second. <code>&lt;LatticeGrid route="…"&gt;</code>
886
+ attaches to whichever router <code>LatticeRouterProvider</code> published, and detaches
887
+ <em>before</em> the grid is destroyed so the router never holds a dead grid.</p>
888
+ <p><strong>A live feed needs no ref.</strong> <code>rowUpdates</code> is a keyed diff handed to
889
+ <code>grid.rows.apply()</code> whenever the object's identity changes.
890
+ <code>predicates</code> maps <code>{ name: fn }</code> to
891
+ <code>grid.filters.where(name, fn)</code>, which composes with whatever filter the reader set
892
+ — unlike the <code>filters</code> prop, which replaces the whole tree.</p>
893
+ <p><strong>What is a live prop is declared, not guessed.</strong> The grid takes any changed
894
+ configuration key through one call; no other viewer does. So each viewer lists the props it
895
+ can take while running (<code>VIEWER_APPLY</code>) and everything else is mount-time
896
+ configuration. A mount-time prop that changes is neither ignored nor silently remounted: it is
897
+ named once, with the remedy — a <code>key</code> that changes when you want the rebuild, or
898
+ the ref. <code>VIEWER_EVENTS</code> and <code>viewerHandlerName</code> are the matching event
899
+ surface (<code>card:move</code> becomes <code>onCardMove</code>), and
900
+ <code>DEFAULT_GRID_NAME</code> is the name a grid publishes under when you choose none.</p>
901
+ <p><strong>React 18 and 19, client only.</strong> Nothing uses an API added in 19 or removed in
902
+ 19. Every component owns a real DOM element, so there is no SSR and no React Server Component
903
+ support.</p>
904
+ </div>
905
+
816
906
  <div class="example">
817
907
  <p class="example__label">Vue 3</p>
818
908
  <pre><code><span class="kw">import</span> * <span class="kw">as</span> vue <span class="kw">from</span> 'vue';
@@ -1279,7 +1369,7 @@ off(); <span class="cmt">// every subscrip
1279
1369
  </p>
1280
1370
  <div class="example">
1281
1371
  <p class="example__label">Which version am I running?</p>
1282
- <pre><code>grid.getVersion(); <span class="cmt">// '1.62.1'</span>
1372
+ <pre><code>grid.getVersion(); <span class="cmt">// '1.63.1'</span>
1283
1373
  LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
1284
1374
  </div>
1285
1375
  <p class="lead-in">
@@ -1704,8 +1794,23 @@ format: { <span class="cmt">// when the short
1704
1794
  negative: 'parentheses', <span class="cmt">// (£1,234.50)</span>
1705
1795
  negativeClass: 'is-loss',
1706
1796
  nullDisplay: ', ',
1707
- }</code></pre>
1797
+ }
1798
+
1799
+ format: { decimals: 0, signed: true } <span class="cmt">// +5, 0, -5</span></code></pre>
1708
1800
  </div>
1801
+ <p class="lead-in">
1802
+ <code>signed</code> puts a leading <code>+</code> on a positive number, currency or percent
1803
+ value. Zero gets no sign either way — it is neither positive nor negative — and a negative
1804
+ value is entirely unaffected: it still renders however <code>negative</code> says
1805
+ (<code>'minus'</code> by default). Off by default, so an existing column's negatives-only
1806
+ look never changes underneath it.
1807
+ </p>
1808
+ <p class="lead-in">
1809
+ A key in <code>format</code> that the resolved type does not read — a typo, or a key that
1810
+ belongs to a different <code>format</code> shape, such as <code>signed</code> written on a
1811
+ text column — is never silently dropped. It warns once, by column and key name, instead of
1812
+ doing nothing.
1813
+ </p>
1709
1814
 
1710
1815
  <h3>Lookups</h3>
1711
1816
  <p class="lead-in">
@@ -2256,6 +2361,19 @@ grid.rows.expandAll();
2256
2361
  grid.rows.collapse('EMEA');</code></pre>
2257
2362
  </div>
2258
2363
 
2364
+ <p class="lead-in">
2365
+ <strong>Expand all</strong> and <strong>Collapse all</strong> are on the menu too, once the
2366
+ grid is grouped: the data area's own right-click menu and every column's header menu (the
2367
+ 3-dot button and a right-click on the heading alike) carry both, driving
2368
+ <code>grid.rows.expandAll()</code>/<code>collapseAll()</code> — the same public API a host
2369
+ calls directly (BACKLOG-0001305). They are hidden, not disabled, on an ungrouped grid: there
2370
+ is nothing for either to act on. Both flow through the same <code>contextMenu</code>/
2371
+ <code>columnMenu</code> customisation chain as every other built-in item, so a host filtering
2372
+ or extending the menu sees them as ordinary items — matched, like every other built-in, by
2373
+ their translated <code>name</code> (catalogue keys <code>menu.expandAll</code> and
2374
+ <code>menu.collapseAll</code>, the same ones the generated group column's own header menu
2375
+ already used — see <a href="#custom-menu">your own menu items</a>).
2376
+ </p>
2259
2377
  <p class="lead-in">
2260
2378
  <code>groupPanel: true</code> adds a drag-and-drop strip above the column header — the
2261
2379
  row-group panel. A user drags a heading into it to group by that column; the active
@@ -3550,6 +3668,52 @@ createGrid(el, {
3550
3668
  with no server involved, including a time-window filter on its <code>TIMESTAMP</code> column.</p>
3551
3669
  </div>
3552
3670
 
3671
+ <h3 id="duckdb-range-reads-guide">Whether a Parquet file streams or downloads whole</h3>
3672
+ <p class="lead-in">
3673
+ <code>duckdbAdapter</code> writes SQL and hands it to the connection you built; it never opens
3674
+ the file itself, so it has no say in whether <code>read_parquet(...)</code> reads the whole
3675
+ thing or only the bytes a query needs. That is decided by DuckDB's own configuration and by the
3676
+ HTTP server the file is served from.
3677
+ </p>
3678
+ <div class="example">
3679
+ <p class="example__label">Enable range reads before the first query</p>
3680
+ <pre><code><span class="kw">const</span> db = <span class="kw">await</span> makeDuckDB(); <span class="cmt">// yours: @duckdb/duckdb-wasm</span>
3681
+ <span class="kw">const</span> conn = <span class="kw">await</span> db.connect();
3682
+
3683
+ <span class="cmt">// Before the first read_parquet(...) call, on DuckDB-Wasm 1.32 and later:</span>
3684
+ <span class="kw">await</span> conn.query(<span class="str">'LOAD httpfs;'</span>);
3685
+
3686
+ createGrid(el, {
3687
+ columns,
3688
+ source: createPushdownSource({
3689
+ adapter: duckdbAdapter({
3690
+ connection: conn,
3691
+ from: <span class="str">"read_parquet('https://example.com/readings.parquet')"</span>,
3692
+ }),
3693
+ }),
3694
+ });</code></pre>
3695
+ </div>
3696
+ <div class="why">
3697
+ <p>Measured against a 1.5&nbsp;MB Parquet file on GitHub Pages (BACKLOG-0001324): without
3698
+ <code>LOAD httpfs;</code>, DuckDB-Wasm 1.32.0's default HTTP path issued zero Range requests
3699
+ and read the whole file — <strong>100%</strong> — for every query, chip filters included. With
3700
+ it loaded on the connection first, the same query issued 25 Range requests and read
3701
+ <strong>30%</strong> of the file. DuckDB-Wasm 1.29.0 read 119% of the file on the same test
3702
+ (its ranges overlapped), so treat the exact percentages as a version's behaviour, not a fixed
3703
+ promise — re-measure against the DuckDB build you ship.</p>
3704
+ <p>The file host has to cooperate too: it must answer <code>HEAD</code>, advertise
3705
+ <code>Accept-Ranges: bytes</code>, and answer a range request with <code>206 Partial
3706
+ Content</code> and a <code>Content-Range</code> header (GitHub Pages does all three). Cross-
3707
+ origin, its CORS policy must also expose <code>Content-Range</code> and
3708
+ <code>Content-Length</code>, or the browser cannot read them back. Any of that missing, and
3709
+ DuckDB falls back to a full read with nothing said.</p>
3710
+ <p>A file registered with <code>db.registerFileBuffer(...)</code> is always downloaded whole,
3711
+ whatever version is loaded or however the host answers: a buffer has already been fetched in
3712
+ full before DuckDB ever opens it, so there is nothing left to range-read. There is no
3713
+ <code>force_download</code> option — it does not exist in DuckDB-Wasm; registering a buffer
3714
+ is the way to force a whole download.</p>
3715
+ </div>
3716
+
3553
3717
  <h3 id="fulldataset">Whole-dataset statistics over a remote source</h3>
3554
3718
  <p class="lead-in">
3555
3719
  A windowed source reduces a total or statistic over the rows it has loaded, not the whole
@@ -3910,6 +4074,31 @@ detail.destroy();
3910
4074
  } }</code> works today, and so do <code>median</code>, <code>stddev</code>, <code>gini</code>,
3911
4075
  <code>iqr</code>, <code>entropy</code>, <code>trimmedMean</code> and the rest.
3912
4076
  <code>statistics</code> is for what <code>select</code> structurally cannot reach.</p>
4077
+ <p id="statistics-coverage"><strong>Say whether the figure is approximate.</strong> Every
4078
+ statistics row carries <code>n</code>, the rows the figure was computed over &mdash; but
4079
+ <code>n</code> alone cannot tell you whether that was <em>all</em> of them. Over a windowed
4080
+ parent it quietly is not: a stream with <code>maxRows: 200</code> that has had 2,000 rows
4081
+ through it computes over the 200 it is holding, and the parent's
4082
+ <code>rows.count()</code>, <code>rows.matchCount()</code> and <code>rows.totalCount()</code>
4083
+ all report 200 as well, so nothing in those numbers says a thing has been left out. Ask the
4084
+ parent grid instead: <code>parent.rows.coverage()</code> returns
4085
+ <code>{ covered, total, windowed }</code>, and <strong><code>covered &lt; total</code>, or
4086
+ <code>total === null</code>, means the figure is approximate</strong>. For that stream it
4087
+ reads <code>{ covered: 200, total: 2000, windowed: true }</code> &mdash; the 1,800 rows that
4088
+ have aged out are visible there and nowhere else. <code>total</code> is <code>null</code>,
4089
+ never a guess, when the source genuinely cannot know: a stream still open that has evicted
4090
+ nothing has no idea how many rows are coming. A memory parent reads
4091
+ <code>{ covered: n, total: n, windowed: false }</code>, so the check costs nothing to leave
4092
+ in and stays quiet when there is nothing to disclose.</p>
4093
+ <pre data-run="js" data-expect="true" data-covers="method:rows"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4094
+ <span class="kw">const</span> grid = createHeadlessGrid({
4095
+ rowKey: <span class="str">'id'</span>,
4096
+ columns: [{ field: <span class="str">'id'</span> }, { field: <span class="str">'amount'</span>, type: <span class="str">'number'</span> }],
4097
+ rows: [{ id: 1, amount: 10 }, { id: 2, amount: 20 }],
4098
+ });
4099
+ <span class="kw">const</span> { covered, total, windowed } = grid.rows.coverage();
4100
+ <span class="cmt">// A memory grid is exact: the figure covers everything there is.</span>
4101
+ <span class="kw">return</span> covered === total &amp;&amp; windowed === <span class="kw">false</span>;</code></pre>
3913
4102
  <div class="example">
3914
4103
  <p class="example__label">Which columns move together</p>
3915
4104
  <pre><code>source: {
@@ -9188,7 +9377,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
9188
9377
 
9189
9378
  <footer>
9190
9379
  <p>
9191
- Lattice Grid 1.62.1 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9380
+ Lattice Grid 1.63.1 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9192
9381
  Written against the shipped source. Where this guide and the code disagree, the code wins,
9193
9382
  please <a href="https://www.latticegrid.dev">tell us</a>.
9194
9383
  </p>