@toclocoinc/lattice-grid 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  dependencies, no build step required. Optional adapters for React, Vue, Svelte
5
5
  and Web Components ship alongside it.
6
6
 
7
- Version 1.8.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.9.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
@@ -20,11 +20,22 @@ runtime, not a CDN, not a font, not an icon sprite.
20
20
  | `lattice-grid.esm.min.js` | The same, as an ES module. |
21
21
  | `lattice-grid.min.css` | The theme. Required. |
22
22
  | `lattice-grid.d.ts` | TypeScript declarations. |
23
- | `modules/react.esm.min.js` | React adapter. Vue, Svelte and Web Component builds sit beside it. |
24
- | `modules/devtools.esm.min.js` | The devtools panel, including the accessibility checks. |
25
23
  | `docs/API.html` | The complete API reference. |
26
24
  | `docs/api-detail.html` | The developer guide — what each part does, and why. |
27
25
 
26
+ Every module is optional and none of them is loaded unless you import it.
27
+
28
+ | Module | What it is |
29
+ |---|---|
30
+ | `modules/charts.esm.min.js` | Thirty chart types drawn from the grid's data. |
31
+ | `modules/react.esm.min.js` | React adapter. |
32
+ | `modules/vue.esm.min.js` | Vue adapter. |
33
+ | `modules/svelte.esm.min.js` | Svelte adapter. |
34
+ | `modules/webcomponent.esm.min.js` | `<lattice-grid>` as a custom element. |
35
+ | `modules/htmx.esm.min.js` | htmx integration — survives htmx's DOM swaps, hydrates from a server-rendered `<table>`, and drives sort, filter and infinite scroll over plain htmx requests. UMD and CJS builds sit beside it. |
36
+ | `modules/dhtmlx-compat.esm.min.js` | A compatibility wrapper for dhtmlx Grid, for moving an existing integration across without rewriting it. |
37
+ | `modules/devtools.esm.min.js` | The devtools panel, including the accessibility checks. |
38
+
28
39
  ---
29
40
 
30
41
  ## What it does
@@ -103,7 +114,7 @@ enough to know whether the grid covers what you need.
103
114
  - **Comments** threaded on cells, and an annotation layer for presenting.
104
115
  - **Full-screen mode**, print, and image capture.
105
116
 
106
- ### Quality of the thing itself
117
+ ### Built in, not bolted on
107
118
 
108
119
  - **Accessible.** Keyboard operable throughout, ARIA grid semantics, a live
109
120
  region for announcements, honours reduced motion, forced colours and large
@@ -182,6 +193,21 @@ renderer registered through one will not appear in the other.
182
193
 
183
194
  The full setup for each framework is in the developer guide.
184
195
 
196
+ ### htmx and dhtmlx
197
+
198
+ Two integrations that are not framework adapters.
199
+
200
+ **htmx.** `modules/htmx` lets a grid survive htmx's own DOM swaps, hydrate from
201
+ a server-rendered `<table>`, and drive sort, filter and infinite scroll over
202
+ plain htmx requests. It is a complete package rather than an add-on —
203
+ `createGrid`, `autoInit`, `hydrateTable`, `readTable`, `serialiseState` and
204
+ `restoreState` are re-exported alongside its own functions — so a page using it
205
+ imports this and never the base package as well.
206
+
207
+ **dhtmlx.** `modules/dhtmlx-compat` exposes a dhtmlx Grid-shaped API over
208
+ Lattice, for moving an existing integration across a piece at a time rather
209
+ than rewriting it in one go.
210
+
185
211
  ### TypeScript
186
212
 
187
213
  Declarations ship in the box and are wired up in `package.json`, so editors find
@@ -225,21 +251,69 @@ Seven built-in types — `text`, `number`, `boolean`, `date`, `dateString`,
225
251
  `object` and `lookup` — cover ordinary business data and are inferred from your
226
252
  rows automatically.
227
253
 
228
- Beyond those, Lattice Grid ships **20 technical field types** for the data that
254
+ Beyond those, Lattice Grid ships **88 technical field types** for the data that
229
255
  usually ends up in a text column because the grid had nowhere to put it. Each is
230
256
  a complete type, not a display format: it brings its own parser, comparator,
231
257
  editor, filter, alignment, clipboard behaviour and Excel mapping.
232
258
 
233
- | Group | Types | Notes |
234
- |---|---|---|
235
- | **Network** | `ipv4`, `ipv6`, `cidr` | Sort in address order, not lexically `10.0.0.9` before `10.0.0.10`. |
236
- | **Time** | `time`, `datetime`, `duration` | Three things a single `date` type keeps being asked to be. |
237
- | **Radix** | `hex`, `hex8`, `hex16`, `hex32`, `binary`, `binary8`, `octal` | The stored value stays a plain number; the base is presentation and input only. |
238
- | **Units** | `bytes`, `megabytes`, `gigabytes`, `bitrate`, `gigabits` | Shows `10 GB`, accepts `10,000M` typed in, stores `10`. |
239
- | **Structured** | `json`, `secret` | `secret` is write-only: editable, never displayed, never exported. |
259
+ | Group | Types |
260
+ |---|---|
261
+ | **Network** | `ipv4`, `ipv6`, `cidr` sort in address order, not lexically, so `10.0.0.9` comes before `10.0.0.10`. |
262
+ | **Time** | `time`, `datetime`, `duration` three things a single `date` type keeps being asked to be. |
263
+ | **Radix** | `hex`, `hex8`, `hex16`, `hex32`, `binary`, `binary8`, `octal` the stored value stays a plain number; the base is presentation and input only. |
264
+ | **Data** | `bytes`, `megabytes`, `gigabytes`, `bitrate`, `gigabits` shows `10 GB`, accepts `10,000M` typed in, stores `10`. Decimal and binary ladders are both first class, because `MB` and `MiB` are different quantities. |
265
+ | **Mechanical** | length, mass, duration, speed, acceleration, area, volume, force, pressure, torque, density, energy, power, angle, `rpm` and angular velocity. |
266
+ | **Fluid and thermal** | volumetric flow, mass flow, dynamic and kinematic viscosity, thermal conductivity, specific heat, and temperature in `celsius`, `fahrenheit` and `kelvin`. |
267
+ | **Electrical and SI** | voltage, current, resistance, capacitance, inductance, charge, conductance, frequency, flux density, luminous flux, luminous intensity, illuminance and substance — all auto-prefixed across the SI range. |
268
+ | **Chemistry and radiation** | `molarity`, `ppm`, `ppb`, absorbed dose, equivalent dose, radioactivity and dose rate. |
269
+ | **Finance and ratios** | `basisPoints`, `ratio`, `percentRate`, and decibels — which are reduced logarithmically, not averaged. |
270
+ | **Structured** | `json`, `secret` — `secret` is write-only: editable, never displayed, never exported. |
271
+
272
+ ### Units of your own
273
+
274
+ Three things define a unit family, and all three are yours to set: the symbols,
275
+ where the symbol sits relative to the number, and how the rungs relate to each
276
+ other.
277
+
278
+ ```js
279
+ import { registerUnitSystem, defineUnit, createUnitType } from '@toclocoinc/lattice-grid';
280
+
281
+ // The ladder. `factor` is how many base quantities one of this unit is, so the
282
+ // relationship between the rungs is simply their arithmetic.
283
+ registerUnitSystem('yarn', [
284
+ defineUnit('tex', 1, ['tx']),
285
+ defineUnit('ktex', 1e3, [], { prefix: 'k' }),
286
+ defineUnit('den', 1 / 9, ['denier']),
287
+ ]);
288
+
289
+ createGrid(el, {
290
+ dataTypes: {
291
+ linearDensity: createUnitType({ system: 'yarn', unit: 'tex', display: 'auto' }),
292
+ },
293
+ columns: [{ field: 'count', type: 'linearDensity' }],
294
+ });
295
+ ```
296
+
297
+ `display: 'auto'` walks the ladder and picks the most readable rung, so 1,500
298
+ tex renders as `1.5 ktex`. A unit given `{ auto: false }` stays off that ladder
299
+ while remaining accepted on input and available as an explicit `display` — which
300
+ is how imperial units sit beside metric ones without an auto readout jumping
301
+ between the two.
302
+
303
+ `placement: 'prefix'` puts the symbol in front of the number, for currency and
304
+ the few notations that want it. It applies to input as well as display, and the
305
+ trailing form is still accepted, because a column that renders `$1,200` will be
306
+ pasted into from somewhere that writes it the other way round.
307
+
308
+ ```js
309
+ createUnitType({ system: 'money', unit: '$', placement: 'prefix', decimals: 2 })
310
+ // renders $1,200.00
311
+ // accepts $1,200 · 1200 $ · k$1.2 · 1.2 k$
312
+ ```
240
313
 
241
- Custom types are first-class too `createRadixType` and `createUnitType` build
242
- new ones, and any type can `extend` another.
314
+ Whatever the display, **the stored value is always a plain number in the
315
+ column's base unit** — so sorting, filtering, grouping, totals and the pivot all
316
+ work on the number and never on the text.
243
317
 
244
318
  ---
245
319
 
package/docs/API.html CHANGED
@@ -417,6 +417,8 @@
417
417
  <a href="#names">Built-in names</a>
418
418
  <a href="#rules">Conditional formatting</a>
419
419
  <a href="#quickfilter">Quick filter</a>
420
+ <a href="#units">Units of your own</a>
421
+ <a href="#chartsmodule">The charts module</a>
420
422
  <a href="#charts">In-cell charts</a>
421
423
  <a href="#formulas">Formulas</a>
422
424
  <a href="#custom-menu">Custom items</a>
@@ -795,6 +797,14 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
795
797
  <tr><td class="name">statusBar</td><td class="type">boolean | { panels }</td><td class="desc">Composable panels along the bottom. Default set: <code>rowCount</code>, <code>selectedCount</code>, <code>aggregation</code>, <code>comments</code>, <code>updates</code>, <code>progress</code>. Each is silent when it has nothing to report.</td></tr>
796
798
  <tr><td class="name">maximise</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc"><code>false</code> removes the rail button and <code>grid.maximise</code>, for an application with its own full-screen mode.</td></tr>
797
799
  <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code> — nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV.</td></tr>
800
+ <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">An IANA zone every date column formats and parses in, so a grid shows one zone whatever the viewer's machine says. Individual columns may override it.</td></tr>
801
+ <tr><td class="name">formulaFunctions</td><td class="type">object</td><td class="desc">Your own functions, added to the formula language by name. The built-in list is closed on purpose; this is the one way in, and a function you add is called exactly as a built-in is.</td></tr>
802
+ <tr><td class="name">formatting</td><td class="type">object</td><td class="desc">Conditional formatting rules to seed, keyed by column id or <code>'*'</code>. The same shape <code>grid.formatting.all()</code> returns, so a saved view can be handed straight back.</td></tr>
803
+ <tr><td class="name">facets</td><td class="type">boolean | object</td><td class="desc">Header histograms that double as a filter. <code>collapsed</code>, <code>height</code>, and per-column <code>strategy</code> and <code>buckets</code>.</td></tr>
804
+ <tr><td class="name">updates</td><td class="type">object</td><td class="desc">How a live feed behaves: batching, the queue that holds while paused, and the highlight a changed cell flashes.</td></tr>
805
+ <tr><td class="name">comments</td><td class="type">object</td><td class="desc">Threaded cell comments — storage, the current author, and whether the indicator shows on an unread thread.</td></tr>
806
+ <tr><td class="name">presence</td><td class="type">object</td><td class="desc">Live cursors, selections and edit locks. Carries intent and never values; see <code>grid.presence</code>.</td></tr>
807
+ <tr><td class="name">environment</td><td class="type">function</td><td class="desc">Extra fields for the diagnostics bundle — build number, tenant, region. Called when a bundle is taken, never on the render path.</td></tr>
798
808
  <tr><td class="name">contextMenu</td><td class="type">boolean | (p) =&gt; MenuItem[]</td><td class="desc">Right-click menu. The function form is <code>(params, defaults) =&gt; items</code> — see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it — what a read-only grid wants, since the default menu offers Paste, Clear and Fill down.</td></tr>
799
809
  <tr><td class="name">columnMenu</td><td class="type">boolean | (p) =&gt; MenuItem[]</td><td class="desc">The header's 3-dot menu, and a right-click on a column heading. The function form is <code>(params, defaults) =&gt; items</code>, with <code>params</code> carrying <code>colId</code>, <code>column</code> and <code>grid</code> — see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it.</td></tr>
800
810
  <tr><td class="name">shortcuts</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">The <kbd>?</kbd> keyboard shortcut overlay. <code>false</code> suppresses it, for a host that wants <kbd>?</kbd> for itself. See <a href="api-detail.html#keyboard">Keyboard</a>.</td></tr>
@@ -1032,7 +1042,8 @@ grid.overlay.hide();</code></pre>
1032
1042
  <tr><td class="sig">corner()</td><td class="type">{ row, colId } | null</td><td class="desc">Bottom-right cell of the newest range, where the fill handle sits.</td></tr>
1033
1043
  <tr><td class="sig">inRange(rowIndex, colId)</td><td class="type">boolean</td><td class="desc">Is a cell inside any selected range?</td></tr>
1034
1044
  <tr><td class="sig">cells()</td><td class="type">{ key, colId }[]</td><td class="desc">Every cell in the selected ranges.</td></tr>
1035
- <tr><td class="sig">summary()</td><td class="type">object</td><td class="desc">count, sum, min, max, avg over the range.</td></tr>
1045
+ <tr><td class="sig">statistics()</td><td class="type">object | null</td><td class="desc">Everything <code>summary()</code> reports plus median, quartiles, deviation, distinct and outliers — over the selected <em>cells</em>, so a rectangle spanning three columns is one set of numbers. Null with nothing selected.</td></tr>
1046
+ <tr><td class="sig">summary()</td><td class="type">object</td><td class="desc">count, sum, min, max, avg over the range.</td></tr>
1036
1047
  </tbody>
1037
1048
  </table>
1038
1049
  </div>
@@ -1600,10 +1611,74 @@ grid.statistics.reduce('margin', 'p95'); // any registered kernel
1600
1611
  grid.statistics.correlation('spend', 'revenue'); // Pearson's r, clamped to [-1, 1]
1601
1612
  grid.statistics.weightedAverage('price', 'qty');
1602
1613
 
1614
+ grid.statistics.covariance('spend', 'revenue');
1615
+ grid.statistics.regression('spend', 'revenue'); // { slope, intercept, r2, stdError, n }
1616
+ grid.statistics.spearman('spend', 'revenue'); // rank; one outlier cannot drag it
1617
+ grid.statistics.kendall('spend', 'revenue'); // tau-b, null past 5,000 rows
1618
+ grid.statistics.weightedQuantile('price', 'qty'); // the median by default
1619
+
1620
+ grid.statistics.series('price', { by: 'date', periodsPerYear: 252 });
1621
+ // { volatility, annualisedVolatility, growth, maxDrawdown, maxDrawdownFrom,
1622
+ // maxDrawdownTo, autocorrelation, upDays, downDays, … }
1623
+
1624
+ grid.statistics.capability('mm', { baseline: 20 });
1625
+ // { cp, cpk, pp, ppk, sigmaWithin, sigmaOverall, outOfSpec, defectRate,
1626
+ // limits: { centre, upper, lower, sigma }, violations: [{ index, rule }] }
1627
+
1603
1628
  grid.statistics.shadow('price', 'delta', 'R42'); // one row's shadow value
1604
1629
  grid.statistics.rebase('price'); // "mark all" — today's values become the baseline
1605
1630
  grid.statistics.tracking(); // { columns, rows, forgotten }</code></pre>
1606
1631
  <p>The <code>statistics</code> tool panel is the end-user half of <code>profile()</code>: a column picker, the twelve figures and a histogram of the column's shape, all following the filters. Add it with <code>toolPanel: { panels: ['columns', 'statistics'] }</code>.</p>
1632
+ <p>It shows the twelve one-pass figures, then <strong>Shape</strong> (skewness, kurtosis, Jarque–Bera), <strong>Robust</strong> (trimmed and winsorized means, MAD, robust outliers), <strong>Concentration</strong> (Gini, HHI, entropy, evenness, top-3 share) and <strong>Capability</strong> where the column declares a <code>spec</code>. A section whose reductions all return null is left out rather than shown as a column of dashes.</p>
1633
+ <p>Or put it in your own page. <code>mountPanel</code> takes no dock and does not create one — <code>toolPanel</code> may be off entirely — so a statistics readout can sit beside a chart, in your own sidebar, or in a settings dialog, at whatever size you give it. It repaints on the same events the rail does, so it stays in step with filters, edits and saved views without you subscribing to anything.</p>
1634
+ <pre><code>import { mountPanel } from '@toclocoinc/lattice-grid';
1635
+
1636
+ const stats = mountPanel({ grid, panel: 'statistics', container: sidebar });
1637
+ stats.refresh(); // for a change the grid does not announce
1638
+ stats.destroy(); // yours to call: the element belongs to your page</code></pre>
1639
+ <p>Any built-in panel works — <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code> — as does a constructor of your own.</p>
1640
+
1641
+
1642
+ <h3>The reductions</h3>
1643
+ <p>Forty-one, all available to a totals row, to <code>reduce()</code> and to the profiling panel. Names are the same everywhere and the labels come from the message catalogue, so a grid in Polish reads in Polish.</p>
1644
+ <div class="table-wrap">
1645
+ <table>
1646
+ <thead><tr><th>Group</th><th>Names</th></tr></thead>
1647
+ <tbody>
1648
+ <tr><td class="sig">Basic</td><td><code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countValues</code>, <code>first</code>, <code>last</code>, <code>distinct</code>, <code>mode</code>, <code>range</code></td></tr>
1649
+ <tr><td class="sig">Spread</td><td><code>variance</code>, <code>varianceP</code>, <code>stddev</code>, <code>stddevP</code>, <code>iqr</code>, <code>mad</code>, <code>sumSquares</code></td></tr>
1650
+ <tr><td class="sig">Quantiles</td><td><code>median</code>, <code>p25</code>, <code>p75</code>, <code>p90</code>, <code>p95</code>, <code>p99</code></td></tr>
1651
+ <tr><td class="sig">Shape</td><td><code>skewness</code>, <code>kurtosis</code>, <code>jarqueBera</code> — above 5.99 the column is not plausibly normal</td></tr>
1652
+ <tr><td class="sig">Means</td><td><code>geomean</code>, <code>harmean</code>, <code>weightedAvg</code>, <code>trimmedMean</code>, <code>winsorizedMean</code></td></tr>
1653
+ <tr><td class="sig">Outliers</td><td><code>robustOutliers</code> — by the modified z-score, which an outlier cannot hide inside the way it inflates an ordinary one</td></tr>
1654
+ <tr><td class="sig">Concentration</td><td><code>hhi</code>, <code>entropy</code>, <code>evenness</code>, <code>top3Share</code>, <code>top10Share</code>, <code>gini</code> — the only group that reads a <em>text</em> column, because "how concentrated is this" is a question about categories</td></tr>
1655
+ <tr><td class="sig">Positional</td><td><code>argmin</code>, <code>argmax</code></td></tr>
1656
+ </tbody>
1657
+ </table>
1658
+ </div>
1659
+
1660
+ <h3>Process capability</h3>
1661
+ <p>Declare the customer's tolerance on the column, and the capability figures, a control chart and any rule marking an out-of-tolerance cell all read the same limits.</p>
1662
+ <pre><code>columns: [{ field: 'mm', type: 'number', spec: { lower: 9.5, upper: 10.8, target: 10 } }]</code></pre>
1663
+ <p><strong>Cp</strong> and <strong>Cpk</strong> use short-term variation, estimated from the moving range; <strong>Pp</strong> and <strong>Ppk</strong> use the overall standard deviation. The gap between them is the point: Cpk well above Ppk means the process drifted. Cp above Cpk means it is precise and aimed wrong, which needs a different fix from being too variable.</p>
1664
+ <div class="note"><p><code>baseline</code> fixes the control limits over the first N readings. Without it the limits are computed over everything — including whatever the process did wrong — so a step change pulls the centre line between the two levels and <em>both</em> halves land outside three sigma. Technically true, and useless for finding when it moved.</p></div>
1665
+
1666
+ <h3>Seeing it</h3>
1667
+ <p>The statistics have chart types to match, in <code>modules/charts</code>. Each takes its numbers from this namespace rather than recomputing, so a coefficient in a matrix and the same one from the API cannot drift apart.</p>
1668
+ <div class="table-wrap">
1669
+ <table>
1670
+ <thead><tr><th>Type</th><th>What it shows</th></tr></thead>
1671
+ <tbody>
1672
+ <tr><td class="sig">correlogram</td><td>Every numeric column against every other, on a ramp centred at zero so the sign reads first. <code>method: 'spearman'</code> ranks instead; where the two disagree, the pair is related but not linearly.</td></tr>
1673
+ <tr><td class="sig">qq</td><td>Sample quantiles against normal ones. Jarque–Bera says a column is not normal; this shows <em>how</em> — a heavy tail bends the ends, a skew bows the whole line. The reference runs through the quartiles, as R's <code>qqline</code> does, because a fitted line is dragged by the very tails you are inspecting.</td></tr>
1674
+ <tr><td class="sig">ecdf</td><td>The share at or below each value, as a step. No bins, so its shape is not partly a choice, and two overlay cleanly where two histograms fight.</td></tr>
1675
+ <tr><td class="sig">lorenz</td><td>The curve a Gini is read off, against the diagonal a perfectly even column would trace.</td></tr>
1676
+ <tr><td class="sig">control</td><td>An individuals chart: control limits from the moving range, the specification, and points breaking a Western Electric rule. The control limits are the process talking and the specification is the customer talking — conflating them is the classic error, so they are drawn differently.</td></tr>
1677
+ <tr><td class="sig">histogram</td><td><code>curve: true</code> overlays a kernel density estimate, which has no bin edges and so separates what is in the data from what is in the binning.</td></tr>
1678
+ <tr><td class="sig">scatter</td><td><code>fit: true</code> draws least squares per series with R² beside it.</td></tr>
1679
+ </tbody>
1680
+ </table>
1681
+ </div>
1607
1682
 
1608
1683
  <h3>Shadow columns</h3>
1609
1684
  <p>A shadow column is declared against another column and maintained by the grid. It has no field in the data and it is not a pure computed column either, because its value depends on what happened <em>before</em>. It is a real column throughout: sortable, filterable, totalled, grouped, exported, saved into a view — which is what makes "show me every circuit repriced more than twice this session, most-changed first" one gesture rather than a report.</p>
@@ -1644,7 +1719,16 @@ grid.statistics.tracking(); // { columns, rows, forgotten
1644
1719
  </tbody>
1645
1720
  </table>
1646
1721
  </div>
1647
- <div class="note"><p>Positional kinds rank over every <em>tracked</em> row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. For the filtered answer, use <code>grid.statistics.profile()</code> or a distribution formatting rule.</p></div>
1722
+ <h3>Running totals</h3>
1723
+ <p>A running total is <strong>not</strong> a shadow kind, and the distinction is the design. Every shadow is a function of the column — of a row's own history, or of where its value sits among the others — so it reads the same however the rows are arranged. A running total does not: sort the grid differently and every value changes, because the question is "how much by the time we reach this row", and <em>by the time</em> is the sort order.</p>
1724
+ <pre><code>columns: [
1725
+ { field: 'amount', type: 'number' },
1726
+ { id: 'cum', title: 'Running', running: { of: 'amount', kind: 'total' } },
1727
+ { id: 'share', title: 'Cumulative %', running: { of: 'amount', kind: 'percent' } },
1728
+ ]</code></pre>
1729
+ <p>Computed in one pass over the display rows and cached against that ordering, so a hundred thousand rows are walked once per sort rather than once per cell. Group headings and totals rows are skipped — a running total that counted a subtotal would double everything below it — and a row with no value carries the figure forward unchanged rather than resetting it.</p>
1730
+
1731
+ <div class="note"><p>Positional kinds rank over every <em>tracked</em> row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. Pass <code>scope: 'filtered'</code> on the shadow spec to rank within what the filters left instead — both answers are legitimate, which is why it is a choice rather than a default.</p></div>
1648
1732
  <div class="note"><p>Shadow state is keyed by row key, never by index — after any sort an index-keyed history would report one row's past against another row's present, and the wrong number would be <em>sortable</em>. Memory is capped at 200,000 tracked rows per column; past that the oldest are dropped and <code>tracking().forgotten</code> says how many, rather than a smaller number being reported as though it were the truth.</p></div>
1649
1733
 
1650
1734
  <h2 id="highlight">grid.highlight</h2>
@@ -2048,23 +2132,59 @@ off(); <span class="cmt">// on() returns i
2048
2132
  <tr><td class="name">permissions:changed</td><td class="type">{ levels }</td><td class="desc">The context moved and every column re-resolved.</td></tr>
2049
2133
  <tr><td class="name">diff:changed</td><td class="type">{ summary }</td><td class="desc">A snapshot was set or cleared.</td></tr>
2050
2134
  <tr><td class="name">licence:changed</td><td class="type">{ info, state }</td><td class="desc">A key was installed, and again when verification settles.</td></tr>
2051
- </tbody>
2052
- </table>
2053
- </div>
2054
-
2055
- <div class="note">
2056
- <p><strong>Every event listed above is emitted</strong>, and these beyond them:
2057
- <code>config:changed</code>, <code>clipboard:copy</code>, <code>column:menu:open</code>,
2058
- <code>column:filter:open</code>, <code>export:progress</code>, <code>toolpanel:focus</code>,
2059
- <code>history:changed</code>, <code>state:reset</code>, <code>highlight:changed</code>,
2060
- <code>render:done</code>, <code>views:changed</code>, the four <code>view:*</code> events,
2061
- <code>permissions:changed</code> and <code>diff:changed</code>.</p>
2062
- <p>Two carry a condition worth knowing. <code>row:edit:start</code> and
2063
- <code>row:edit:end</code> replace the <code>cell:edit:*</code> pair only when
2064
- <code>edit.mode</code> is <code>'row'</code> you get one pair or the other, never both.
2065
- And <code>page:changed</code> needs <code>pagination</code> configured, or a
2066
- <code>grid.pagination.set()</code> call; without a page size there are no pages to change.</p>
2067
- </div>
2135
+ <tr><td class="name">columns:changed</td><td class="desc">The column set was replaced or reordered wholesale.</td></tr>
2136
+ <tr><td class="name">columns:tagged</td><td class="desc">A column's tags changed.</td></tr>
2137
+ <tr><td class="name">detail:toggled</td><td class="desc">A master-detail row opened or closed.</td></tr>
2138
+ <tr><td class="name">formatting:changed</td><td class="desc">A conditional formatting rule was added, edited, reordered or restated.</td></tr>
2139
+ <tr><td class="name">redaction:changed</td><td class="desc">A redaction rule changed.</td></tr>
2140
+ <tr><td class="name">diff:swapped</td><td class="desc">The baseline and the current rows were exchanged.</td></tr>
2141
+ <tr><td class="name">facet:computed</td><td class="desc">A header histogram finished counting. Carries the column and the buckets.</td></tr>
2142
+ <tr><td class="name">facet:filtered</td><td class="desc">A bucket or a dragged range was applied as a filter.</td></tr>
2143
+ <tr><td class="name">facet:expanded</td><td class="desc">The facet band was opened or collapsed.</td></tr>
2144
+ <tr><td class="name">facet:failed</td><td class="desc">A distribution could not be computed. Carries the reason.</td></tr>
2145
+ <tr><td class="name">form:opened</td><td class="desc">The row form opened.</td></tr>
2146
+ <tr><td class="name">form:closed</td><td class="desc">The row form closed without saving.</td></tr>
2147
+ <tr><td class="name">form:saved</td><td class="desc">The row form committed.</td></tr>
2148
+ <tr><td class="name">form:error</td><td class="desc">A commit from the form failed validation or was rejected.</td></tr>
2149
+ <tr><td class="name">tree:loading</td><td class="desc">Children are being fetched for a node.</td></tr>
2150
+ <tr><td class="name">tree:loaded</td><td class="desc">Children arrived. Carries the key and the count.</td></tr>
2151
+ <tr><td class="name">tree:loadFailed</td><td class="desc">A child fetch failed.</td></tr>
2152
+ <tr><td class="name">tree:loadAborted</td><td class="desc">A child fetch was cancelled, usually because the node collapsed.</td></tr>
2153
+ <tr><td class="name">rows:paused</td><td class="desc">A live feed was paused; updates queue from here.</td></tr>
2154
+ <tr><td class="name">rows:resumed</td><td class="desc">The feed resumed and the queue drained.</td></tr>
2155
+ <tr><td class="name">rows:deferred</td><td class="desc">Updates were held rather than applied, because an edit is in flight.</td></tr>
2156
+ <tr><td class="name">row:received</td><td class="desc">A row arrived from a source.</td></tr>
2157
+ <tr><td class="name">row:sent</td><td class="desc">A row was written back to a source.</td></tr>
2158
+ <tr><td class="name">row:copied</td><td class="desc">A row was duplicated.</td></tr>
2159
+ <tr><td class="name">row:moved</td><td class="desc">A row was dragged to a new position.</td></tr>
2160
+ <tr><td class="name">stream:evicted</td><td class="desc">A streaming source dropped rows to stay within its cap.</td></tr>
2161
+ <tr><td class="name">header:contextmenu</td><td class="desc">A heading was right-clicked.</td></tr>
2162
+ <tr><td class="name">timeline:attached</td><td class="desc">A time brush was connected to the grid.</td></tr>
2163
+ <tr><td class="name">timeline:detached</td><td class="desc">The brush was removed.</td></tr>
2164
+ <tr><td class="name">timeline:seek</td><td class="desc">The brush settled on a range.</td></tr>
2165
+ <tr><td class="name">timeline:seeking</td><td class="desc">The brush is being dragged. Throttled.</td></tr>
2166
+ <tr><td class="name">presentation:changed</td><td class="desc">Presentation mode started, or its options changed.</td></tr>
2167
+ <tr><td class="name">presentation:ended</td><td class="desc">Presentation mode ended. Annotations are cleared here.</td></tr>
2168
+ <tr><td class="name">presentation:view</td><td class="desc">The presentation advanced to another saved view.</td></tr>
2169
+ <tr><td class="name">presentation:scale</td><td class="desc">The presentation zoom changed.</td></tr>
2170
+ <tr><td class="name">presentation:spotlight</td><td class="desc">A region was spotlit or released.</td></tr>
2171
+ <tr><td class="name">presentation:captured</td><td class="desc">A PNG was taken.</td></tr>
2172
+ <tr><td class="name">comment:added</td><td class="desc">A comment was posted.</td></tr>
2173
+ <tr><td class="name">comment:edited</td><td class="desc">A comment was changed.</td></tr>
2174
+ <tr><td class="name">comment:deleted</td><td class="desc">A comment was removed.</td></tr>
2175
+ <tr><td class="name">comment:failed</td><td class="desc">A comment could not be saved. Carries the reason.</td></tr>
2176
+ <tr><td class="name">comment:threadOpened</td><td class="desc">A thread was opened in the panel.</td></tr>
2177
+ <tr><td class="name">comment:threadClosed</td><td class="desc">A thread was closed or resolved.</td></tr>
2178
+ <tr><td class="name">comment:indexLoaded</td><td class="desc">The comment index finished loading, so indicators can paint.</td></tr>
2179
+ <tr><td class="name">presence:published</td><td class="desc">This client's cursor or selection was broadcast.</td></tr>
2180
+ <tr><td class="name">presence:left</td><td class="desc">A peer disconnected.</td></tr>
2181
+ <tr><td class="name">presence:failed</td><td class="desc">A presence transport error. Presence is lossy by design; this is informational.</td></tr>
2182
+ <tr><td class="name">presence:lockRefused</td><td class="desc">An edit was refused because a peer holds the cell.</td></tr>
2183
+ </tbody>
2184
+ </table>
2185
+ </div>
2186
+
2187
+ <div class="note"><p>This list is complete, and stays complete: <code>tools/check.js</code> compares every <code>emit()</code> in the grid against the declared event names and fails the build on a mismatch. A chart raises its own events, which belong to the charts module rather than to this bus.</p></div>
2068
2188
 
2069
2189
  <h2 id="rules">Conditional formatting</h2>
2070
2190
  <p>Rules compile into the function <code>cell.style</code> already takes, so a compiled rule set
@@ -2119,6 +2239,128 @@ grid.filters.quickState(); <span class="cmt">// { text: 'acme london', mode: '
2119
2239
  grid does not blank on every open bracket. <code>fuzzy</code> does not reorder rows: ranking
2120
2240
  results would fight the sort the user chose.</p></div>
2121
2241
 
2242
+ <h2 id="units">Units of your own</h2>
2243
+ <p>Twenty-six unit systems ship &mdash; length, mass, pressure, data, bitrate, angle, temperature, flow and the rest. A family of your own takes three things, and all three are yours to set: the symbols, where the symbol sits relative to the number, and how the rungs relate to each other.</p>
2244
+ <pre><code>import { registerUnitSystem, defineUnit, createUnitType } from '@toclocoinc/lattice-grid';
2245
+
2246
+ <span class="cmt">// `factor` is how many base units one of these is. Exactly one must be 1.</span>
2247
+ registerUnitSystem('distance', [
2248
+ defineUnit('mm', 0.001, ['millimetre', 'millimetres']),
2249
+ defineUnit('cm', 0.01, ['centimetre', 'centimetres']),
2250
+ defineUnit('m', 1, ['metre', 'metres']),
2251
+ defineUnit('km', 1000, ['kilometre', 'kilometres']),
2252
+ ]);
2253
+
2254
+ createGrid(el, {
2255
+ dataTypes: {
2256
+ distance: createUnitType({ system: 'distance', unit: 'm', display: 'auto' }),
2257
+ },
2258
+ columns: [{ field: 'span', type: 'distance' }],
2259
+ });</code></pre>
2260
+ <p>The <code>factor</code> values <em>are</em> the relationship between the rungs &mdash; there is no separate ladder to declare, and no ordering to get right, because the ladder is sorted by factor at load. A hand-ordered list of thirty units is one transposition away from an auto display that walks backwards, and that mistake is invisible in review.</p>
2261
+ <div class="table-wrap">
2262
+ <table>
2263
+ <thead><tr><th>Option</th><th>Description</th></tr></thead>
2264
+ <tbody>
2265
+ <tr><td class="name">system</td><td class="desc">A built-in system, or one registered with <code>registerUnitSystem</code>.</td></tr>
2266
+ <tr><td class="name">unit</td><td class="desc">What the column <strong>stores</strong>. It need not be the system's base: the same ladder with <code>unit: 'km'</code> stores kilometres, and <code>50mm</code> typed in becomes 0.00005.</td></tr>
2267
+ <tr><td class="name">display</td><td class="desc"><code>'auto'</code> walks the ladder for the most readable rung, or name a symbol to fix it.</td></tr>
2268
+ <tr><td class="name">placement</td><td class="desc"><code>'prefix'</code> puts the symbol in front &mdash; <code>$1,200</code> &mdash; and applies to input as well as display. Defaults to a suffix.</td></tr>
2269
+ <tr><td class="name">decimals</td><td class="desc">Fixed fraction digits; or <code>minDecimals</code> / <code>maxDecimals</code>, or <code>significantFigures</code>.</td></tr>
2270
+ <tr><td class="name">locale</td><td class="desc">Separators and grouping. Follows the grid's locale when unset.</td></tr>
2271
+ </tbody>
2272
+ </table>
2273
+ </div>
2274
+ <p>A unit given <code>{ auto: false }</code> stays off the <code>display: 'auto'</code> ladder while remaining accepted on input and available as an explicit <code>display</code>. That is how imperial units sit beside metric ones without an auto readout jumping between the two.</p>
2275
+ <div class="note"><p><strong>The stored value is always a plain number in the column's own unit.</strong> Sorting, filtering, grouping, totals and the pivot all read that number and never the text, which is why <code>250mm</code> sorts below <code>1.5 cm</code> correctly rather than <code>1</code> sorting before <code>9</code>. <code>registerUnitSystem</code> is global and throws on a duplicate name, so register each system once at startup rather than inside a component that may mount twice.</p></div>
2276
+
2277
+ <h2 id="chartsmodule">The charts module</h2>
2278
+ <p><code>modules/charts</code> draws thirty chart types from the grid's own data. It is optional and imports nothing from the grid &mdash; the grid is handed in &mdash; so the bundle carries the drawing and none of the grid, and a page that never charts never loads it.</p>
2279
+ <pre><code>import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
2280
+
2281
+ const chart = createChart({
2282
+ grid, <span class="cmt">// the grid to read</span>
2283
+ container: '#revenue', <span class="cmt">// an element or a selector</span>
2284
+ type: 'bar',
2285
+ x: 'region', <span class="cmt">// the category column</span>
2286
+ y: 'revenue', <span class="cmt">// the measure column</span>
2287
+ });</code></pre>
2288
+ <p><strong>A chart reads the grid's filtered rows.</strong> Filter, sort or edit the grid and every chart bound to it redraws on the next frame &mdash; there is nothing to subscribe to and nothing to keep in step. A chart of rows the user cannot see would be describing a different data set.</p>
2289
+
2290
+ <h3>The types</h3>
2291
+ <div class="table-wrap">
2292
+ <table>
2293
+ <thead><tr><th>Family</th><th>Types</th><th>Takes</th></tr></thead>
2294
+ <tbody>
2295
+ <tr><td class="sig">Cartesian</td><td><code>line</code>, <code>step</code>, <code>area</code>, <code>rangeArea</code>, <code>bar</code>, <code>horizontalBar</code>, <code>waterfall</code>, <code>scatter</code>, <code>bubble</code></td><td><code>x</code>, <code>y</code>, optional <code>series</code></td></tr>
2296
+ <tr><td class="sig">Two axes</td><td><code>combo</code>, <code>pareto</code></td><td><code>x</code>, <code>measures</code></td></tr>
2297
+ <tr><td class="sig">Distribution</td><td><code>histogram</code>, <code>boxplot</code></td><td><code>y</code> alone</td></tr>
2298
+ <tr><td class="sig">Matrix</td><td><code>heatmap</code></td><td><code>x</code>, <code>y</code>, <code>series</code></td></tr>
2299
+ <tr><td class="sig">Part to whole</td><td><code>pie</code>, <code>donut</code>, <code>sunburst</code>, <code>treemap</code></td><td><code>x</code>, <code>y</code></td></tr>
2300
+ <tr><td class="sig">Specialist</td><td><code>radar</code>, <code>gauge</code>, <code>funnel</code>, <code>candlestick</code></td><td>varies; candlestick takes four <code>measures</code> in open, high, low, close order</td></tr>
2301
+ <tr><td class="sig">Geographic</td><td><code>geomap</code></td><td><code>x</code> as an ISO code, <code>y</code> as the value</td></tr>
2302
+ <tr><td class="sig">Flow</td><td><code>sankey</code>, <code>chord</code>, <code>network</code></td><td><code>source</code>, <code>target</code>, <code>y</code></td></tr>
2303
+ <tr><td class="sig">Over time</td><td><code>stream</code>, <code>marimekko</code>, <code>violin</code>, <code>gantt</code></td><td>varies; gantt takes <code>label</code>, <code>start</code>, <code>end</code></td></tr>
2304
+ </tbody>
2305
+ </table>
2306
+ </div>
2307
+ <p>A chart given data it cannot draw &mdash; a candlestick with three measures rather than four &mdash; says so on the chart rather than drawing nothing, because a chart that silently draws nothing is indistinguishable from one that is broken.</p>
2308
+
2309
+ <h3>The spec</h3>
2310
+ <div class="table-wrap">
2311
+ <table>
2312
+ <thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
2313
+ <tbody>
2314
+ <tr><td class="name">grid</td><td class="type">Grid</td><td class="desc">Required. The grid to read.</td></tr>
2315
+ <tr><td class="name">container</td><td class="type">Element | string</td><td class="desc">Required. Where to draw.</td></tr>
2316
+ <tr><td class="name">type</td><td class="type">string</td><td class="desc">One of the thirty above.</td></tr>
2317
+ <tr><td class="name">x / y</td><td class="type">string</td><td class="desc">Category and measure columns.</td></tr>
2318
+ <tr><td class="name">series</td><td class="type">string</td><td class="desc">Splits the measure into one series per distinct value.</td></tr>
2319
+ <tr><td class="name">measures</td><td class="type">object[]</td><td class="desc"><code>{col, fn, type, axis}</code> &mdash; several measures at once, each reduced by any totals kernel.</td></tr>
2320
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc">Drawn above the plot.</td></tr>
2321
+ <tr><td class="name">scheme</td><td class="type">string | string[]</td><td class="desc">A named scheme or your own colours. <code>schemeNames()</code> lists the built-in ones, including a colour-blind-safe palette.</td></tr>
2322
+ <tr><td class="name">legend</td><td class="type">boolean | object</td><td class="desc"><code>position</code>, and <code>isolate</code> so a click shows only that series &mdash; which is what a reader with eight series wants, and what plain toggling makes them do in seven clicks.</td></tr>
2323
+ <tr><td class="name">labels</td><td class="type">boolean | object</td><td class="desc">Values beside each mark. <code>position</code>, <code>format</code>, <code>minGap</code>. A label that would overlap one already placed is dropped rather than drawn over it.</td></tr>
2324
+ <tr><td class="name">axis</td><td class="type">object</td><td class="desc">Titles, tick density and formatting per axis.</td></tr>
2325
+ <tr><td class="name">reference</td><td class="type">object[]</td><td class="desc">Horizontal lines: <code>{value, label}</code>.</td></tr>
2326
+ <tr><td class="name">multiples</td><td class="type">string</td><td class="desc">One chart per distinct value of this column, on a shared scale.</td></tr>
2327
+ <tr><td class="name">buckets</td><td class="type">number</td><td class="desc">Histogram bins. Twelve by default.</td></tr>
2328
+ <tr><td class="name">canvas</td><td class="type">boolean | number</td><td class="desc">Draw to canvas past this many points, for a dense scatter.</td></tr>
2329
+ </tbody>
2330
+ </table>
2331
+ </div>
2332
+
2333
+ <h3>The chart</h3>
2334
+ <div class="table-wrap">
2335
+ <table>
2336
+ <thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
2337
+ <tbody>
2338
+ <tr><td class="sig">update(spec)</td><td class="type">void</td><td class="desc">Change any part of the spec and redraw. Keys you omit keep their values.</td></tr>
2339
+ <tr><td class="sig">draw()</td><td class="type">void</td><td class="desc">Redraw now, for a change the grid does not announce.</td></tr>
2340
+ <tr><td class="sig">data()</td><td class="type">object</td><td class="desc">What the chart last bound &mdash; series, categories and the rows behind them.</td></tr>
2341
+ <tr><td class="sig">on(event, fn)</td><td class="type">function</td><td class="desc">Returns its own unsubscribe.</td></tr>
2342
+ <tr><td class="sig">ascend(levels?)</td><td class="type">void</td><td class="desc">Up one level on a drillable hierarchy.</td></tr>
2343
+ <tr><td class="sig">toSVG(opts?)</td><td class="type">string</td><td class="desc">The chart as markup.</td></tr>
2344
+ <tr><td class="sig">toPNG(opts?)</td><td class="type">Promise&lt;Blob&gt;</td><td class="desc"><code>scale: 2</code> for a retina still.</td></tr>
2345
+ <tr><td class="sig">toCSV()</td><td class="type">string</td><td class="desc">The bound data, for a reader who wants the numbers.</td></tr>
2346
+ <tr><td class="sig">destroy()</td><td class="type">void</td><td class="desc">Yours to call: the element is in your page, not the grid's.</td></tr>
2347
+ <tr><td class="sig">element</td><td class="type">SVGElement</td><td class="desc">The chart's own root.</td></tr>
2348
+ </tbody>
2349
+ </table>
2350
+ </div>
2351
+
2352
+ <h3>Clicking a chart</h3>
2353
+ <p>A chart emits <code>point:click</code>, <code>point:hover</code> and <code>series:toggle</code>. The common use is filtering the grid from a mark, which makes the pair two views of one selection rather than a chart beside a table.</p>
2354
+ <pre><code>chart.on('point:click', ({ point }) =&gt; {
2355
+ grid.filters.set({ col: 'region', op: 'eq', value: point.x });
2356
+ });</code></pre>
2357
+
2358
+ <h3>Maps</h3>
2359
+ <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>
2360
+ <p>Codes that match nothing are counted and reported on the chart rather than dropped &mdash; 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>
2361
+
2362
+ <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 &mdash; and it is why the grid must be created first, and why a chart cannot outlive it.</p></div>
2363
+
2122
2364
  <h2 id="charts">In-cell charts</h2>
2123
2365
  <p>Seven chart renderers for a cell. Each is a single SVG whose path data is the only thing a
2124
2366
  repaint writes, so they cost the same as any other cell as rows recycle.</p>