@toclocoinc/lattice-grid 1.67.0 → 1.68.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 (163) hide show
  1. package/README.md +1 -1
  2. package/angular/package.json +1 -1
  3. package/docs/API.html +21 -16
  4. package/docs/api-detail.html +40 -5
  5. package/lattice-grid.d.ts +12 -2
  6. package/lattice-grid.esm.min.js +27 -7
  7. package/lattice-grid.min.cjs +27 -7
  8. package/lattice-grid.min.js +27 -7
  9. package/modules/ai.d.ts +1 -1
  10. package/modules/ai.esm.min.js +3 -3
  11. package/modules/ai.min.cjs +3 -3
  12. package/modules/ai.min.js +3 -3
  13. package/modules/angular.d.ts +1 -1
  14. package/modules/angular.esm.min.js +3 -3
  15. package/modules/angular.min.cjs +3 -3
  16. package/modules/angular.min.js +3 -3
  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 +1 -1
  27. package/modules/chart-bubblemap.min.cjs +1 -1
  28. package/modules/chart-bubblemap.min.js +1 -1
  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 +1 -1
  59. package/modules/chart-hexmap.min.cjs +1 -1
  60. package/modules/chart-hexmap.min.js +1 -1
  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-markermap.d.ts +1 -1
  66. package/modules/chart-markermap.esm.min.js +1 -1
  67. package/modules/chart-markermap.min.cjs +1 -1
  68. package/modules/chart-markermap.min.js +1 -1
  69. package/modules/chart-parallel.d.ts +1 -1
  70. package/modules/chart-parallel.esm.min.js +1 -1
  71. package/modules/chart-parallel.min.cjs +1 -1
  72. package/modules/chart-parallel.min.js +1 -1
  73. package/modules/chart-ridgeline.d.ts +1 -1
  74. package/modules/chart-ridgeline.esm.min.js +1 -1
  75. package/modules/chart-ridgeline.min.cjs +1 -1
  76. package/modules/chart-ridgeline.min.js +1 -1
  77. package/modules/chart-roc.d.ts +1 -1
  78. package/modules/chart-roc.esm.min.js +1 -1
  79. package/modules/chart-roc.min.cjs +1 -1
  80. package/modules/chart-roc.min.js +1 -1
  81. package/modules/chart-slope.d.ts +1 -1
  82. package/modules/chart-slope.esm.min.js +1 -1
  83. package/modules/chart-slope.min.cjs +1 -1
  84. package/modules/chart-slope.min.js +1 -1
  85. package/modules/chart-splom.d.ts +1 -1
  86. package/modules/chart-splom.esm.min.js +1 -1
  87. package/modules/chart-splom.min.cjs +1 -1
  88. package/modules/chart-splom.min.js +1 -1
  89. package/modules/chart-waffle.d.ts +1 -1
  90. package/modules/chart-waffle.esm.min.js +1 -1
  91. package/modules/chart-waffle.min.cjs +1 -1
  92. package/modules/chart-waffle.min.js +1 -1
  93. package/modules/charts.d.ts +1 -1
  94. package/modules/charts.esm.min.js +1376 -1249
  95. package/modules/charts.min.cjs +1376 -1249
  96. package/modules/charts.min.js +1376 -1249
  97. package/modules/data-router.d.ts +1 -1
  98. package/modules/data-router.esm.min.js +204 -8
  99. package/modules/data-router.min.cjs +204 -8
  100. package/modules/data-router.min.js +204 -8
  101. package/modules/devtools.d.ts +1 -1
  102. package/modules/devtools.esm.min.js +1 -1
  103. package/modules/devtools.min.cjs +1 -1
  104. package/modules/devtools.min.js +1 -1
  105. package/modules/dhtmlx-compat.d.ts +1 -1
  106. package/modules/dhtmlx-compat.esm.min.js +3 -3
  107. package/modules/dhtmlx-compat.min.cjs +3 -3
  108. package/modules/dhtmlx-compat.min.js +3 -3
  109. package/modules/gantt.d.ts +1 -1
  110. package/modules/gantt.esm.min.js +32 -7
  111. package/modules/gantt.min.cjs +32 -7
  112. package/modules/gantt.min.js +32 -7
  113. package/modules/geo-europe-nuts.d.ts +1 -1
  114. package/modules/geo-europe-nuts.esm.min.js +1 -1
  115. package/modules/geo-uk.d.ts +1 -1
  116. package/modules/geo-uk.esm.min.js +1 -1
  117. package/modules/geo-us-states.d.ts +1 -1
  118. package/modules/geo-us-states.esm.min.js +1 -1
  119. package/modules/geo-world-110m.d.ts +1 -1
  120. package/modules/geo-world-110m.esm.min.js +1 -1
  121. package/modules/geo-world-50m.d.ts +1 -1
  122. package/modules/geo-world-50m.esm.min.js +1 -1
  123. package/modules/htmx.d.ts +1 -1
  124. package/modules/htmx.esm.min.js +27 -7
  125. package/modules/htmx.min.cjs +27 -7
  126. package/modules/htmx.min.js +27 -7
  127. package/modules/kanban.d.ts +1 -1
  128. package/modules/kanban.esm.min.js +3 -3
  129. package/modules/kanban.min.cjs +3 -3
  130. package/modules/kanban.min.js +3 -3
  131. package/modules/kpi.d.ts +26 -8
  132. package/modules/kpi.esm.min.js +31 -6
  133. package/modules/kpi.min.cjs +31 -6
  134. package/modules/kpi.min.js +31 -6
  135. package/modules/layout.d.ts +1 -1
  136. package/modules/layout.esm.min.js +3 -3
  137. package/modules/layout.min.cjs +3 -3
  138. package/modules/layout.min.js +3 -3
  139. package/modules/mock-socket.d.ts +1 -1
  140. package/modules/mock-socket.esm.min.js +1 -1
  141. package/modules/mock-socket.min.cjs +1 -1
  142. package/modules/mock-socket.min.js +1 -1
  143. package/modules/react.d.ts +1 -1
  144. package/modules/react.esm.min.js +3 -3
  145. package/modules/react.min.cjs +3 -3
  146. package/modules/react.min.js +3 -3
  147. package/modules/svelte.d.ts +1 -1
  148. package/modules/svelte.esm.min.js +3 -3
  149. package/modules/svelte.min.cjs +3 -3
  150. package/modules/svelte.min.js +3 -3
  151. package/modules/tabs.d.ts +1 -1
  152. package/modules/tabs.esm.min.js +13 -14
  153. package/modules/tabs.min.cjs +13 -14
  154. package/modules/tabs.min.js +13 -14
  155. package/modules/vue.d.ts +1 -1
  156. package/modules/vue.esm.min.js +3 -3
  157. package/modules/vue.min.cjs +3 -3
  158. package/modules/vue.min.js +3 -3
  159. package/modules/webcomponent.d.ts +1 -1
  160. package/modules/webcomponent.esm.min.js +27 -7
  161. package/modules/webcomponent.min.cjs +27 -7
  162. package/modules/webcomponent.min.js +27 -7
  163. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ dependencies, no build step required. Optional adapters for React, Vue, Svelte
5
5
  and Web Components ship alongside it, and Angular has compiled components of its
6
6
  own at `@toclocoinc/lattice-grid/angular` — inside this package, not beside it.
7
7
 
8
- Version 1.67.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
+ Version 1.68.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
9
9
 
10
10
  ---
11
11
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.67.0",
2
+ "version": "1.68.0",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
5
  "module": "./fesm2022/toclocoinc-lattice-grid-angular.mjs",
package/docs/API.html CHANGED
@@ -363,7 +363,7 @@
363
363
  <div class="shell">
364
364
  <aside class="rail">
365
365
  <p class="rail__brand">Lattice Grid</p>
366
- <p class="rail__sub">API reference · v1.67.0</p>
366
+ <p class="rail__sub">API reference · v1.68.0</p>
367
367
  <nav>
368
368
  <div class="rail__group">
369
369
  <span class="rail__label">Start</span>
@@ -456,7 +456,7 @@
456
456
  </header>
457
457
 
458
458
  <p class="chips">
459
- <span class="chip">Version 1.67.0</span>
459
+ <span class="chip">Version 1.68.0</span>
460
460
  <span class="chip">Zero dependencies</span>
461
461
  <span class="chip"><a href="api-detail.html">Developer guide &rarr;</a></span>
462
462
  </p>
@@ -1960,7 +1960,7 @@ grid.destroy();
1960
1960
  <table>
1961
1961
  <thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
1962
1962
  <tbody>
1963
- <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.67.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1963
+ <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.68.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1964
1964
  <tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
1965
1965
  <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>
1966
1966
  <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>
@@ -7366,7 +7366,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
7366
7366
  ],
7367
7367
  onTileClick: ({ tile }) =&gt; drillInto(tile.id),
7368
7368
  });</code></pre>
7369
- <p>Each tile names an <strong>aggregation</strong> (<code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code>, or a <code>custom</code> reducer <code>(rows, tile) =&gt; value</code>), the <strong>field</strong> it reads, and an optional <strong>filter</strong> predicate. Formatting (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>, with decimals, currency and locale), a <strong>baseline</strong> for a delta, and semantic <strong>threshold bands</strong> (two cut points with a direction, or an explicit <code>bands</code> list) are all per tile; the band is a semantic name (<code>good</code>/<code>warn</code>/<code>critical</code>), separate from any accent colour. An optional <strong>sparkline</strong> plots a <code>{ x, y }</code> series in sequence order. Each tile is a labelled <code>&lt;figure&gt;</code>, focusable and keyboard-activatable, its value announced; the sparkline transition respects <code>prefers-reduced-motion</code>.</p>
7369
+ <p>Each tile names an <strong>aggregation</strong> (<code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code>, or a <code>custom</code> reducer <code>(rows, tile) =&gt; value</code>), the <strong>field</strong> it reads, and an optional <strong>filter</strong> predicate. Formatting (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>, with decimals, currency and locale), a <strong>baseline</strong> for a delta, and semantic <strong>threshold bands</strong> (two cut points with a direction, or an explicit <code>bands</code> list) are all per tile; the band is a semantic name (<code>good</code>/<code>warn</code>/<code>critical</code>), separate from any accent colour. <code>delta: 'absolute' | 'relative' | 'both'</code> (default <code>'both'</code>) chooses what the movement line prints against the baseline (BACKLOG-0001673) &mdash; the difference alone, the percentage alone, or both together, because the percentage of a rate series (4.10 vs a 4.30 baseline) is meaningless and a host needs to keep the line without it. An optional <strong>sparkline</strong> plots a <code>{ x, y }</code> series in sequence order. Each tile is a labelled <code>&lt;figure&gt;</code>, focusable and keyboard-activatable, its value announced; the sparkline transition respects <code>prefers-reduced-motion</code>.</p>
7370
7370
  <p><strong>A panel with no data says so.</strong> A tile's status is <code>good</code>, <code>warn</code>, <code>critical</code>, <code>null</code> (no thresholds) &mdash; or <code>unknown</code>, which means the tile <em>measured nothing</em>. Two things cause that: the panel holds <em>no rows at all</em>, or the tile's <code>field</code> names no column on the bound grid, so it never read a cell to reduce over. That fourth status is decided from data presence <em>before</em> any threshold is consulted, because an aggregation over nothing returns the identity of its operation (<code>sum</code> and <code>count</code> return 0) and 0 is a number a threshold grades: under <code>lowerIsBetter</code> cut points it grades <code>good</code>, so an empty panel would otherwise read as a healthy one. An <code>unknown</code> tile reports a <code>null</code> value, renders the <code>nullText</code> placeholder rather than a zero, and is marked with a dashed edge and a visible &ldquo;No data&rdquo; caption that also forms part of its accessible name &mdash; the state is never carried by colour alone. Because <code>unknown</code> is an explicit status rather than a severity, anything rolling tiles up can surface &ldquo;not measured&rdquo; instead of inheriting a false green.</p>
7371
7371
  <p><strong>A measured zero is still a measurement.</strong> A tile whose <code>filter</code> matches none of the rows the panel <em>does</em> hold is a different thing: no open incidents is genuinely good, so it reads <code>0</code> and is graded on its thresholds exactly as before. Only an empty panel is <code>unknown</code>.</p>
7372
7372
  <p><strong>Staleness is a different question, and a time-bounded source answers it.</strong> <code>unknown</code> is about the absence of rows, not their staleness: a panel over an unbounded store keeps showing the last values it was given when its feed goes quiet, because those rows are still there. Give the underlying source a <a href="#sources">rolling time window</a> (<code>maxAge</code> with <code>ageBy</code>) and its rows age out while the feed is silent, so the panel drains and reports <code>unknown</code> the next time it reads them &mdash; a grid-bound panel as the grid announces the change (or when the host calls <code>refresh()</code>), a routed one as the removals reach <code>rows.apply</code>.</p>
@@ -7445,7 +7445,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
7445
7445
  kpi.destroy();
7446
7446
  <span class="kw">return</span> [shapes, shaped, ignoredAgg].join(' | ');</code></pre>
7447
7447
  <h3 id="kpi-tree">The KPI tree: top-level items that expand to the indicators beneath them</h3>
7448
- <p>Set <code>tree</code> and the panel becomes a <strong>rail</strong> instead of a grid of tiles: a small number of top-level items, each expanding to the indicators underneath it, with the parent telling you at a glance whether anything below needs attention. <code>Compute</code> expands to <code>psi</code> and <code>cpu</code>; collapsed, it still shows you that one of them is in breach.</p>
7448
+ <p>Set <code>tree</code> and the panel becomes a <strong>rail</strong> instead of a grid of tiles: a small number of top-level items, each expanding to the indicators underneath it, with the parent telling you at a glance whether anything below needs attention. <code>Compute</code> expands to <code>psi</code> and <code>cpu</code>; collapsed, it still shows you that one of them is in breach. <code>tree</code> is the opt-in (BACKLOG-0001673): a dotted tile id is not a hierarchy on its own &mdash; with <code>tree</code> unset, the ids you see below would stay plain, unsplit tile ids and the panel would render every tile flat, warning once that setting <code>tree</code> is how the hierarchy is reached.</p>
7449
7449
  <pre><code>const kpi = createKPI(document.querySelector('#rail'), {
7450
7450
  rows, rowKey: 'id',
7451
7451
  tiles: [
@@ -7461,7 +7461,7 @@ kpi.destroy();
7461
7461
 
7462
7462
  kpi.nodes()[0].rollup; <span class="cmt">// 'critical' — even with the branch shut</span>
7463
7463
  kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code></pre>
7464
- <p><strong>Where the shape comes from &mdash; two sources, in this order.</strong> <em>Declared:</em> <code>tree: { path }</code> or <code>tree: { parentKey }</code> over the <em>tile specs</em>, the same two shapes the grid's <a href="#tree-data">tree data</a> and the tree-select editor already take, so a hierarchy you have configured once needs no second vocabulary. A <code>path</code> is the tile's <em>own</em> place, its own segment last &mdash; <code>['System','Compute','cpu']</code>, exactly as <code>['EMEA','UK','Colchester']</code> is Colchester's path and not its parent's &mdash; and levels no tile represents are synthesised, so <code>System</code> and <code>Compute</code> appear without a tile of their own. <em>Derived:</em> with neither declared, the tile ids are split on <code>separator</code> (default <code>.</code>), so <code>system.compute.cpu</code> files itself. A panel whose ids carry no separator is flat and renders exactly as it always did; <code>tree: false</code> keeps it flat whatever the ids look like. A tile's <code>field</code> is never a source &mdash; a dot there already means a nested object property, and overloading it would make <code>field: 'cpu.util'</code> ambiguous.</p>
7464
+ <p><strong>Where the shape comes from &mdash; two sources, in this order, once <code>tree</code> is set.</strong> <em>Declared:</em> <code>tree: { path }</code> or <code>tree: { parentKey }</code> over the <em>tile specs</em>, the same two shapes the grid's <a href="#tree-data">tree data</a> and the tree-select editor already take, so a hierarchy you have configured once needs no second vocabulary. A <code>path</code> is the tile's <em>own</em> place, its own segment last &mdash; <code>['System','Compute','cpu']</code>, exactly as <code>['EMEA','UK','Colchester']</code> is Colchester's path and not its parent's &mdash; and levels no tile represents are synthesised, so <code>System</code> and <code>Compute</code> appear without a tile of their own. <em>Derived:</em> with neither declared, the tile ids are split on <code>separator</code> (default <code>.</code>), so <code>system.compute.cpu</code> files itself &mdash; but only once <code>tree</code> is given an object (<code>{}</code> included); unset, a dot in an id means nothing and the panel warns once rather than building a hierarchy you never asked for. A panel whose ids carry no separator is flat and renders exactly as it always did even with <code>tree</code> set; <code>tree: false</code>, same as leaving it unset, keeps it flat whatever the ids look like, silently. A tile's <code>field</code> is never a source &mdash; a dot there already means a nested object property, and overloading it would make <code>field: 'cpu.util'</code> ambiguous.</p>
7465
7465
  <p><strong>No value rolls up; severity does.</strong> A parent shows no aggregated number. That is not a simplification: the running accumulators expose <code>add</code>/<code>remove</code>/<code>value</code> and no merge, so <code>avg</code>, <code>countDistinct</code> and a <code>custom</code> reducer cannot be composed from their children without rescanning, and a per-aggregation exception list would be a number that is right for a sum and wrong for an average. A parent that has a tile of its own still shows <em>that tile's</em> reading. What does roll up is the status: <code>rollup</code> is the worst severity at or below the node, across as many levels as you have, and it is what a collapsed branch reports.</p>
7466
7466
  <p><strong>Nothing measured is not good news, and it does not win the roll-up either.</strong> A leaf that measured nothing is <code>unknown</code> (see above), and <code>unknown</code> is deliberately excluded from <code>rollup</code>: ranking &ldquo;not measured&rdquo; as the worst thing beneath a parent would hide a real warning under it. It is surfaced separately instead &mdash; <code>node.unknown</code> counts the descendants that measured nothing, and the node renders it in words (&ldquo;2 unknown&rdquo;) and puts it in its accessible name. So neither way of being wrong is available: silence cannot read as green, and it cannot bury an amber.</p>
7467
7467
  <p><strong>Accessible by construction.</strong> The rail is a real <code>role="tree"</code> with the APG keyboard model &mdash; <kbd>&rarr;</kbd> opens a closed branch and otherwise steps into it, <kbd>&larr;</kbd> closes an open one and otherwise steps out to its parent, <kbd>&uarr;</kbd>/<kbd>&darr;</kbd> walk what is visible, <kbd>Home</kbd>/<kbd>End</kbd> jump to its ends, <kbd>Enter</kbd>/<kbd>Space</kbd> activate &mdash; with a roving tabindex, and <code>aria-level</code>, <code>aria-posinset</code> and <code>aria-setsize</code> on every node, because a reader cannot count what a collapsed branch has left out of the DOM. Status carries a <strong>shape</strong> as well as a colour (a filled circle, a triangle, a square, a hollow circle), not one dot in three colours, and a parent's rolled-up status is <em>in its accessible name</em>: &ldquo;Compute, 6 items, worst status critical&rdquo;, announced as one string. Every phrase is a catalogue key: pass <code>messages</code> (any <code>{ t(key, params) }</code>, including a grid's own) to translate the panel, and a key your catalogue lacks falls back to English rather than printing the key.</p>
@@ -7471,8 +7471,10 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
7471
7471
  <p><strong>A collapsed branch costs data, not paint.</strong> Every node's status is computed whether or not you can see it &mdash; that is what makes the rail worth having &mdash; while a collapsed branch contributes no DOM at all. It is the same division the grid's grouping already makes between its totals walk and its display walk. Expansion is patched in place, keyed on the node, so a live routed feed does not throw a keyboard user off the node they are standing on.</p>
7472
7472
  <h3 id="kpi-tree-example">A collapsed parent reports the red leaf underneath it, executed</h3>
7473
7473
  <p class="section-note">Two subsystems from dotted tile ids, one of them in breach, with nothing expanded; then the same
7474
- rail with no data at all. Run headless on every build.</p>
7475
- <pre data-run="js" data-expect="compute false critical | cpu critical | null 2" data-covers="export:createKPI"><code><span class="kw">const</span> { createKPI } = <span class="kw">await</span> import('../packages/modules/kpi/index.js');
7474
+ rail with no data at all. <code>tree</code> is the opt-in (BACKLOG-0001673) &mdash; a dotted id alone does
7475
+ not build a hierarchy, so this example sets <code>tree: {}</code> deliberately, the way any host must. Run
7476
+ headless on every build.</p>
7477
+ <pre data-run="js" data-expect="compute false critical | cpu critical | null 2" data-covers="export:createKPI config:tree"><code><span class="kw">const</span> { createKPI } = <span class="kw">await</span> import('../packages/modules/kpi/index.js');
7476
7478
 
7477
7479
  <span class="kw">const</span> fewerIsBetter = { warn: 70, critical: 90, direction: 'lowerIsBetter' };
7478
7480
  <span class="kw">const</span> tiles = [
@@ -7482,8 +7484,9 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
7482
7484
  thresholds: { warn: 100, critical: 300, direction: 'lowerIsBetter' } },
7483
7485
  ];
7484
7486
 
7485
- <span class="cmt">// No `tree` block: the dots in the tile ids are the hierarchy.</span>
7486
- <span class="kw">const</span> kpi = createKPI(null, { rows: [{ id: 'h1', cpu: 94, mem: 40, latency: 12 }], rowKey: 'id', tiles });
7487
+ <span class="cmt">// `tree: {}` is the opt-in: without it, these dotted ids would stay flat tiles</span>
7488
+ <span class="cmt">// and the panel would warn once instead of building a hierarchy.</span>
7489
+ <span class="kw">const</span> kpi = createKPI(null, { rows: [{ id: 'h1', cpu: 94, mem: 40, latency: 12 }], rowKey: 'id', tree: {}, tiles });
7487
7490
 
7488
7491
  <span class="kw">const</span> compute = kpi.nodes()[0];
7489
7492
  <span class="cmt">// Nobody has opened it, and it reports the breach anyway.</span>
@@ -7491,7 +7494,7 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
7491
7494
  <span class="kw">const</span> leaf = [compute.children[0].label, compute.children[0].status].join(' '); <span class="cmt">// cpu critical</span>
7492
7495
 
7493
7496
  <span class="cmt">// Nothing delivered: the parent reports the silence rather than a false green.</span>
7494
- <span class="kw">const</span> quiet = createKPI(null, { rows: [], rowKey: 'id', tiles });
7497
+ <span class="kw">const</span> quiet = createKPI(null, { rows: [], rowKey: 'id', tree: {}, tiles });
7495
7498
  <span class="kw">const</span> silent = String(quiet.nodes()[0].rollup) + ' ' + quiet.nodes()[0].unknown; <span class="cmt">// null 2</span>
7496
7499
 
7497
7500
  <span class="kw">return</span> [shut, leaf, silent].join(' | ');</code></pre>
@@ -9719,7 +9722,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9719
9722
  </table>
9720
9723
  </div>
9721
9724
  <h3 id="type-ChartAnnotation">ChartAnnotation</h3>
9722
- <p class="section-note">One declarative annotation (BACKLOG-0000744, extended by BACKLOG-0000953). A reference or target line, a shaded band, a callout, or an `event` marker. Its value is a constant `value` (or `from`/`to` for a band), or a `compute` reduction of the data it annotates — `mean`, `median`, `min`, `max`, or `p95` for a percentile — so it follows the data as the grid is filtered. A band with `orient: 'vertical'` shades an x-range instead — an event window, a maintenance period — and an `event` marker is a labelled vertical rule with a flag at a position on the x axis. Every annotation names the axis it reads, which on a dual-axis chart is what stops it being placed against the wrong scale, and is written into the accessible table as a sentence — a vertical marker and an event stating the position they sit at, because a screen-reader user needs where and when, not only that a marker exists.</p>
9725
+ <p class="section-note">One declarative annotation (BACKLOG-0000744, extended by BACKLOG-0000953). A reference or target line, a shaded band, a callout, or an `event` marker. Its value is a constant `value` (or `from`/`to` for a band), or a `compute` reduction of the data it annotates — `mean`, `median`, `min`, `max`, or `p95` for a percentile — so it follows the data as the grid is filtered. A band with `orient: 'vertical'` shades an x-range instead — an event window, a maintenance period — and an `event` marker is a labelled vertical rule with a flag at a position on the x axis. Every annotation names the axis it reads, which on a dual-axis chart is what stops it being placed against the wrong scale, and is written into the accessible table as a sentence — a vertical marker and an event stating the position they sit at, because a screen-reader user needs where and when, not only that a marker exists. An annotation the chart cannot place — an x value it cannot resolve on the axis, or a band whose edges are off-scale — is dropped with one console warning naming the annotation's kind, the axis and the reason, rather than vanishing without a trace (BACKLOG-0001672).</p>
9723
9726
  <div class="table-wrap">
9724
9727
  <table>
9725
9728
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
@@ -9785,7 +9788,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9785
9788
  <tr><td class="name">fn</td><td class="type">TotalName</td><td class="desc">A reduction name, as the totals row uses. <small>(optional)</small></td></tr>
9786
9789
  <tr><td class="name">type</td><td class="type">'bar' | 'line' | 'area'</td><td class="desc">The mark this measure draws with, on a combo chart. <small>(optional)</small></td></tr>
9787
9790
  <tr><td class="name">axis</td><td class="type">'left' | 'right'</td><td class="desc">Which axis it belongs to, on a combo chart. <small>(optional)</small></td></tr>
9788
- <tr><td class="name">title</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
9791
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc">The series label a combo's legend and axis titles use (BACKLOG-0001672). `label` is read too, as an undeclared alias, for a caller already using it; `title` wins when both are given. Falls back to the measure column's own `title`, then to `col`, when neither is set. <small>(optional)</small></td></tr>
9789
9792
  </tbody>
9790
9793
  </table>
9791
9794
  </div>
@@ -12539,7 +12542,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12539
12542
  <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12540
12543
  <tr><td class="name">nullText</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12541
12544
  <tr><td class="name">locale</td><td class="type">string</td><td class="desc">The default locale a clock tile formats in when the tile itself declares none (BACKLOG-0001640); falls back to the browser's default. No effect on a stat tile, which takes its own `format.locale`. <small>(optional)</small></td></tr>
12542
- <tr><td class="name">tree</td><td class="type">KPITreeConfig | false</td><td class="desc">Arrange the tiles as a hierarchy; `false` keeps the panel flat. <small>(optional)</small></td></tr>
12545
+ <tr><td class="name">tree</td><td class="type">KPITreeConfig | false</td><td class="desc">Arrange the tiles as a hierarchy; unset or `false` keeps the panel flat. Opt-in (BACKLOG-0001673): a dotted tile id is not a hierarchy until `tree` is set, and warns once while it is not. <small>(optional)</small></td></tr>
12543
12546
  <tr><td class="name">messages</td><td class="type">{ t(key: string, params?: Record&lt;string, unknown&gt;): string }</td><td class="desc">The catalogue the panel's own text is read from. A panel routinely has no grid to borrow one off — two of its three input modes have none — so this is the first-class way to translate it. A grid's own `messages` satisfies the shape; a key it does not carry falls back to English. <small>(optional)</small></td></tr>
12544
12547
  <tr><td class="name">onTileClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
12545
12548
  <tr><td class="name">onTileDblClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
@@ -12625,6 +12628,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12625
12628
  <tr><td class="name">format</td><td class="type">KPIFormat</td><td class="desc">Value formatting. <small>(optional)</small></td></tr>
12626
12629
  <tr><td class="name">target</td><td class="type">number</td><td class="desc">A comparison target rendered alongside the value. <small>(optional)</small></td></tr>
12627
12630
  <tr><td class="name">baseline</td><td class="type">number</td><td class="desc">A baseline the tile's delta is measured against. <small>(optional)</small></td></tr>
12631
+ <tr><td class="name">delta</td><td class="type">'absolute' | 'relative' | 'both'</td><td class="desc">What the movement line prints against `baseline` (BACKLOG-0001673, F-FRED-G): `'absolute'` the difference alone, `'relative'` the percentage alone, `'both'` (the default, unchanged) both together. A rate series (4.10 vs 4.30) makes the percentage a percent-of-a-percent and meaningless, so `'absolute'` is how a host keeps the line without it. The arrow and its colour follow the sign of the difference either way. <small>(optional)</small></td></tr>
12628
12632
  <tr><td class="name">thresholds</td><td class="type">KPIThresholds</td><td class="desc">Threshold bands, either two cut points or an explicit band list. <small>(optional)</small></td></tr>
12629
12633
  <tr><td class="name">bands</td><td class="type">KPIBand[]</td><td class="desc">Explicit status bands (an alternative to `thresholds`). <small>(optional)</small></td></tr>
12630
12634
  <tr><td class="name">sparkline</td><td class="type">KPISparkline | string</td><td class="desc">A trend sparkline series. <small>(optional)</small></td></tr>
@@ -12663,13 +12667,14 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12663
12667
  <tr><td class="name">delta</td><td class="type">number | null</td><td class="desc"></td></tr>
12664
12668
  <tr><td class="name">deltaPercent</td><td class="type">number | null</td><td class="desc"></td></tr>
12665
12669
  <tr><td class="name">deltaFormatted</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12670
+ <tr><td class="name">deltaMode</td><td class="type">'absolute' | 'relative' | 'both'</td><td class="desc">What the movement line prints; see `KPIStatTile.delta`. Always present once `baseline` is. <small>(optional)</small></td></tr>
12666
12671
  <tr><td class="name">count</td><td class="type">number</td><td class="desc"></td></tr>
12667
12672
  <tr><td class="name">sparkline</td><td class="type">number[] | null</td><td class="desc"></td></tr>
12668
12673
  </tbody>
12669
12674
  </table>
12670
12675
  </div>
12671
12676
  <h3 id="type-KPITreeConfig">KPITreeConfig</h3>
12672
- <p class="section-note">The hierarchy a KPI panel arranges its tiles into (BACKLOG-0001059): a rail of top-level items that expand to the indicators beneath them, each parent highlighted with the worst status below it. The shape is declared with `path` or `parentKey` — the same two shapes the grid's tree data and the tree-select editor take — over the **tile specs**, not the rows. With neither declared, one is derived by splitting the tile ids on `separator`, so `system.compute.cpu` files itself under Compute under System. A panel whose ids carry no separator stays flat, and `false` keeps it flat whatever they look like. A tile's `field` is never a source: a dot there already means a nested object property.</p>
12677
+ <p class="section-note">The hierarchy a KPI panel arranges its tiles into (BACKLOG-0001059): a rail of top-level items that expand to the indicators beneath them, each parent highlighted with the worst status below it. `tree` is an opt-in (BACKLOG-0001673): omitted or `false`, the panel is flat whatever its tile ids look like, and a dotted id seen with `tree` unset is reported once rather than silently turned into a hierarchy. Given any object (`{}` included), the shape is declared with `path` or `parentKey` — the same two shapes the grid's tree data and the tree-select editor take — over the **tile specs**, not the rows. With neither declared, one is derived by splitting the tile ids on `separator`, so `system.compute.cpu` files itself under Compute under System. A panel whose ids carry no separator stays flat even with `tree` set. A tile's `field` is never a source: a dot there already means a nested object property.</p>
12673
12678
  <div class="table-wrap">
12674
12679
  <table>
12675
12680
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
@@ -14901,7 +14906,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
14901
14906
  <!-- END GENERATED TYPE REFERENCE -->
14902
14907
 
14903
14908
  <footer>
14904
- Lattice Grid 1.67.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
14909
+ Lattice Grid 1.68.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
14905
14910
  This document describes the behaviour of the shipped library. Where this guide and the code
14906
14911
  disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
14907
14912
  </footer>
@@ -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.67.0</p>
440
+ <p class="rail__sub">Developer guide · v1.68.0</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.67.0</span>
557
+ <span class="chip">Version 1.68.0</span>
558
558
  <span class="chip">Zero dependencies</span>
559
559
  <span class="chip">No build step</span>
560
560
  </p>
@@ -1492,7 +1492,7 @@ off(); <span class="cmt">// every subscrip
1492
1492
  </p>
1493
1493
  <div class="example">
1494
1494
  <p class="example__label">Which version am I running?</p>
1495
- <pre><code>grid.getVersion(); <span class="cmt">// '1.67.0'</span>
1495
+ <pre><code>grid.getVersion(); <span class="cmt">// '1.68.0'</span>
1496
1496
  LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
1497
1497
  </div>
1498
1498
  <p class="lead-in">
@@ -9035,7 +9035,7 @@ return (`proposals ${proposals.join(',')} | diff ${result.diff.map((d) =&gt; `ol
9035
9035
 
9036
9036
  <h3 id="kpi-config-examples">KPI config keys, executed</h3>
9037
9037
  <p class="lead-in">
9038
- Three examples covering the eleven KPI config keys not already demonstrated elsewhere in this
9038
+ Four examples covering the twelve KPI config keys not already demonstrated elsewhere in this
9039
9039
  guide.
9040
9040
  </p>
9041
9041
 
@@ -9145,6 +9145,41 @@ return (`${el.querySelectorAll('.lat-kpi__node')[0].getAttribute('aria-label')}
9145
9145
  </p>
9146
9146
  </div>
9147
9147
 
9148
+ <div class="example">
9149
+ <p class="example__label">What the movement line prints against a baseline: delta, executed</p>
9150
+ <pre data-run="js" data-expect="both:▼ -0.2 (-4.7%) | absolute:▼ -0.2 | relative:▼ -4.7%" data-covers="config:delta"><code>const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
9151
+ const { createKPI } = await import('../packages/modules/kpi/index.js');
9152
+
9153
+ const dom = createTestDom({});
9154
+ const el = dom.document.createElement('div');
9155
+ // A rate series (4.10 vs a 4.30 baseline a year earlier): the percentage of a
9156
+ // rate is meaningless, so a host names 'absolute' to keep the line without it.
9157
+ const kpi = createKPI(el, {
9158
+ rows: [{ id: 1, rate: 4.10 }], rowKey: 'id',
9159
+ tiles: [
9160
+ { id: 'both', label: 'Default', aggregation: 'max', field: 'rate',
9161
+ baseline: 4.30, format: { decimals: 1 } },
9162
+ { id: 'absolute', label: 'Difference alone', aggregation: 'max', field: 'rate',
9163
+ baseline: 4.30, format: { decimals: 1 }, delta: 'absolute' },
9164
+ { id: 'relative', label: 'Percentage alone', aggregation: 'max', field: 'rate',
9165
+ baseline: 4.30, format: { decimals: 1 }, delta: 'relative' },
9166
+ ],
9167
+ });
9168
+ const lines = ['both', 'absolute', 'relative'].map((id) =&gt; {
9169
+ const fig = [...el.querySelectorAll('.lat-kpi__tile')].find((f) =&gt; f.getAttribute('aria-label').startsWith(
9170
+ id === 'both' ? 'Default' : (id === 'absolute' ? 'Difference' : 'Percentage')));
9171
+ return `${id}:${fig.querySelector('.lat-kpi__delta').textContent}`;
9172
+ });
9173
+ return lines.join(' | ');
9174
+ </code></pre>
9175
+ <p>
9176
+ <code>delta: 'absolute' | 'relative' | 'both'</code> (default <code>'both'</code>, unchanged)
9177
+ chooses what a tile's movement line prints against its <code>baseline</code> &mdash; the
9178
+ difference alone, the percentage alone, or both together, exactly as before this option
9179
+ existed. The arrow and its colour follow the sign of the difference in every mode.
9180
+ </p>
9181
+ </div>
9182
+
9148
9183
  <h3 id="route-options-examples">Data Router route options, executed</h3>
9149
9184
  <div class="example">
9150
9185
  <p class="example__label">A per-route filter and transform, executed</p>
@@ -9651,7 +9686,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
9651
9686
 
9652
9687
  <footer>
9653
9688
  <p>
9654
- Lattice Grid 1.67.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9689
+ Lattice Grid 1.68.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9655
9690
  Written against the shipped source. Where this guide and the code disagree, the code wins,
9656
9691
  please <a href="https://www.latticegrid.dev">tell us</a>.
9657
9692
  </p>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.67.0, type declarations
2
+ * Lattice Grid 1.68.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -7558,6 +7558,12 @@ export interface ChartMeasure {
7558
7558
  type?: 'bar' | 'line' | 'area';
7559
7559
  /** Which axis it belongs to, on a combo chart. */
7560
7560
  axis?: 'left' | 'right';
7561
+ /**
7562
+ * The series label a combo's legend and axis titles use.
7563
+ * `label` is read too, as an undeclared alias, for a caller already using
7564
+ * it; `title` wins when both are given. Falls back to the measure column's
7565
+ * own `title`, then to `col`, when neither is set.
7566
+ */
7561
7567
  title?: string;
7562
7568
  }
7563
7569
 
@@ -7670,7 +7676,11 @@ export interface ChartTrend {
7670
7676
  * which on a dual-axis chart is what stops it being placed against the wrong
7671
7677
  * scale, and is written into the accessible table as a sentence — a vertical
7672
7678
  * marker and an event stating the position they sit at, because a screen-reader
7673
- * user needs where and when, not only that a marker exists.
7679
+ * user needs where and when, not only that a marker exists. An annotation the
7680
+ * chart cannot place — an x value it cannot resolve on the axis, or a band
7681
+ * whose edges are off-scale — is dropped with one console warning naming the
7682
+ * annotation's kind, the axis and the reason, rather than vanishing without a
7683
+ * trace.
7674
7684
  */
7675
7685
  export interface ChartAnnotation {
7676
7686
  /**