@toclocoinc/lattice-grid 1.9.1 → 1.10.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.9.1 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.10.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
@@ -61,7 +61,7 @@ enough to know whether the grid covers what you need.
61
61
 
62
62
  ### Working with the data
63
63
 
64
- - **Editing.** Cell, row and form editing, with over twenty editors — text,
64
+ - **Editing.** Cell, row and form editing, with twenty-three editors — text,
65
65
  number, date, time, select, multi-select, colour, rating, slider, segmented,
66
66
  code, password, icon picker and more. Validation, async commits, optimistic
67
67
  updates with rollback, and a full undo history.
@@ -78,14 +78,14 @@ enough to know whether the grid covers what you need.
78
78
 
79
79
  ### Seeing the data
80
80
 
81
- - **Charts.** `modules/charts` draws thirty chart types from the grid's own
81
+ - **Charts.** `modules/charts` draws thirty-five chart types from the grid's own
82
82
  data — line, bar, area, scatter, pie, donut, sunburst, treemap, radar, gauge,
83
83
  funnel, heatmap, histogram, box plot, candlestick, combo, geomap, sankey,
84
84
  chord, network, stream, violin, gantt and more. They follow the grid's
85
85
  filters, and clicking a mark can filter it in turn.
86
86
  - **Statistics.** `grid.statistics` profiles a column in one pass — count,
87
87
  missing, distinct, five-number summary, standard deviation, outliers and a
88
- histogram — and answers correlations and weighted averages. Twenty-eight
88
+ histogram — and answers correlations and weighted averages. Thirty-eight
89
89
  reduction kernels are available to the totals row, and you can register your
90
90
  own.
91
91
  - **Shadow columns.** Values the grid maintains about itself: how many times a
@@ -119,7 +119,8 @@ enough to know whether the grid covers what you need.
119
119
  - **Accessible.** Keyboard operable throughout, ARIA grid semantics, a live
120
120
  region for announcements, honours reduced motion, forced colours and large
121
121
  target sizes. The devtools module runs the accessibility checks in place.
122
- - **Internationalised.** Eighteen complete locales, right-to-left layout, and
122
+ - **Internationalised.** Twenty-two complete locale catalogues across nineteen
123
+ languages, right-to-left layout, and
123
124
  locale-aware number, date and currency formatting throughout.
124
125
  - **Themeable** through CSS custom properties, with light and dark built in and
125
126
  a reset that keeps a host page's stylesheet out.
package/docs/API.html CHANGED
@@ -768,6 +768,8 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
768
768
  <tr><td class="name">density</td><td class="type">'compact' | 'standard' | 'comfortable' | 'spacious' | number</td><td class="dflt">'compact'</td><td class="desc">One scale that every geometry token derives from — row height, spacing, decoration sizes, and type at a damped rate. Row heights are 23.8 / 28 / 42 / 56px. A number scales 28px, so <code>1.4</code> gives 39.2px for anything between the presets. Virtualisation follows it; an explicit <code>rowHeight</code> overrides it.</td></tr>
769
769
  <tr><td class="name">rowHeight</td><td class="type">number | (row) =&gt; number</td><td class="dflt">28</td><td class="desc">A function enables variable-height rows.</td></tr>
770
770
  <tr><td class="name">headerHeight</td><td class="type">number</td><td class="dflt">32</td><td class="desc">Per header row.</td></tr>
771
+ <tr><td class="name">title</td><td class="type">string</td><td class="dflt">&mdash;</td><td class="desc">A caption drawn above the column headings. Inside the grid rather than an element placed above it, so it scrolls with the grid, sits in the region a screen reader announces, and is kept by image capture and print.</td></tr>
772
+ <tr><td class="name">showHeader</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">Draw the column headings at all. <code>false</code> removes the row, and removes it from the accessibility tree rather than only from view. Distinct from <code>showColumnFunctions</code>, which keeps the headings and drops only their sort, filter and menu controls.</td></tr>
771
773
  <tr><td class="name">overscan</td><td class="type">number</td><td class="dflt">4</td><td class="desc">Rows rendered beyond the viewport.</td></tr>
772
774
  <tr><td class="name">autoHeight</td><td class="type">boolean | 'visible'</td><td class="dflt">—</td><td class="desc">Size rows to their content: cells wrap instead of ellipsising, and each row takes the height its tallest cell needs. Only rendered rows are measured either way — the difference is that <code>true</code> gives up above 10,000 rows and returns to fixed heights, while <code>'visible'</code> keeps measuring at any size and accepts a scrollbar that shifts as rows are measured on the way past.</td></tr>
773
775
  <tr><td class="name">columnVirtualisationAbove</td><td class="type">number</td><td class="dflt">30</td><td class="desc">Column count above which columns virtualise too.</td></tr>
@@ -1680,6 +1682,62 @@ stats.destroy(); // yours to call: the element belongs to your page</code></pr
1680
1682
  </table>
1681
1683
  </div>
1682
1684
 
1685
+ <h3 id="derived-values">Values the grid maintains for you</h3>
1686
+ <p>
1687
+ Three of the ideas in this section are not standard grid vocabulary, so it is worth saying what
1688
+ they have in common before the detail. <strong>Each of them is a value the grid keeps up to date
1689
+ from data you already have</strong>, declared once rather than maintained by hand.
1690
+ </p>
1691
+ <p>
1692
+ The alternative, in every application that needs one of these, is a parallel structure in the
1693
+ host: a copy of what each row looked like a moment ago, a rank recomputed on every tick, a
1694
+ cumulative total that has to be redone whenever the sort changes, and a dashboard panel running
1695
+ its own query beside the table. That code works for a while and then produces a number nobody
1696
+ can account for, usually because one part of it noticed a filter and another did not.
1697
+ </p>
1698
+ <div class="table-wrap">
1699
+ <table>
1700
+ <thead><tr><th>Concept</th><th>What it is</th><th>Reach for it when</th></tr></thead>
1701
+ <tbody>
1702
+ <tr>
1703
+ <td class="name">Shadow column</td>
1704
+ <td class="desc">An extra column, declared against a real one, holding something the grid works out about it: how it has changed, or where it sits among the others.</td>
1705
+ <td class="desc">You want &ldquo;what was this an hour ago&rdquo;, &ldquo;how many places has it moved&rdquo;, or &ldquo;which decile is it in&rdquo; as a column you can sort and filter on.</td>
1706
+ </tr>
1707
+ <tr>
1708
+ <td class="name">Running column</td>
1709
+ <td class="desc">A cumulative value: the total, or the share of the total, <em>by the time you reach this row</em>.</td>
1710
+ <td class="desc">You want a running balance, a cumulative percentage, or a Pareto curve down the page.</td>
1711
+ </tr>
1712
+ <tr>
1713
+ <td class="name">Derived grid</td>
1714
+ <td class="desc">A whole second grid whose rows are built from the first: grouped, unnested, filtered, ranked or profiled.</td>
1715
+ <td class="desc">You want a top-five panel, a breakdown by region, an exceptions list or a statistics summary <em>beside</em> the table, and it must never disagree with it.</td>
1716
+ </tr>
1717
+ </tbody>
1718
+ </table>
1719
+ </div>
1720
+ <p>
1721
+ <strong>The line between a shadow and a running column is the sort order.</strong> A shadow is a
1722
+ function of the column: a row&rsquo;s own history, or where its value sits among the others.
1723
+ Sort the grid differently and a rank is still the same rank. A running total is the opposite
1724
+ &mdash; it answers &ldquo;how much by the time we reach this row&rdquo;, and <em>by the time</em>
1725
+ is the order the rows are in, so re-sorting changes every value in the column. That is why they
1726
+ are declared separately rather than as two kinds of one thing.
1727
+ </p>
1728
+ <p>
1729
+ <strong>A derived grid is a different scale of the same idea.</strong> A shadow adds a column to
1730
+ the rows you have; a derivation produces different rows altogether &mdash; one per sales person
1731
+ rather than one per sale. Because it is a <em>source</em> rather than a special kind of grid,
1732
+ the result sorts, filters, totals, themes and exports like any other, and can itself be the
1733
+ source of another.
1734
+ </p>
1735
+ <p class="section-note">
1736
+ All three read the rows the grid is currently showing, so a filter applied to the table moves
1737
+ the ranks, the running totals and every derived panel together. That is the property worth
1738
+ having: not that any one of them is clever, but that they cannot disagree.
1739
+ </p>
1740
+
1683
1741
  <h3>Shadow columns</h3>
1684
1742
  <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>
1685
1743
  <pre><code>columns: [
@@ -1720,7 +1778,12 @@ stats.destroy(); // yours to call: the element belongs to your page</code></pr
1720
1778
  </table>
1721
1779
  </div>
1722
1780
  <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>
1781
+ <p>
1782
+ A running column answers &ldquo;how much by the time we reach this row&rdquo;: a balance
1783
+ that accumulates down the page, or the share of the total accounted for so far. It is the
1784
+ column a Pareto chart is made of, and the one a finance report opens with.
1785
+ </p>
1786
+ <p>It is declared separately from a shadow column, and the sort order is the reason. 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
1787
  <pre><code>columns: [
1725
1788
  { field: 'amount', type: 'number' },
1726
1789
  { id: 'cum', title: 'Running', running: { of: 'amount', kind: 'total' } },
@@ -2028,10 +2091,91 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'tria
2028
2091
  <tr><td class="sig">paged</td><td class="type">fetch</td><td class="desc">A page at a time from a server that paginates.</td></tr>
2029
2092
  <tr><td class="sig">remote</td><td class="type">fetch</td><td class="desc">Blocks fetched as the viewport reaches them, with sort, filter and grouping pushed to the server.</td></tr>
2030
2093
  <tr><td class="sig">stream</td><td class="type">connect</td><td class="desc">Rows arriving over time. Promotes to memory once complete.</td></tr>
2094
+ <tr><td class="sig">derived</td><td class="type">from</td><td class="desc">Rows built from another grid: grouped, unnested, filtered, ranked or profiled. Read-only, and follows the source.</td></tr>
2031
2095
  </tbody>
2032
2096
  </table>
2033
2097
  </div>
2034
2098
 
2099
+ <h3 id="derived-source">Derived sources: a grid built from another grid</h3>
2100
+ <p class="section-note">
2101
+ Most dashboards put a summary panel beside a table &mdash; the top five sales people, the
2102
+ breakdown by region, the exceptions list. Built by hand, that panel runs its own query, and
2103
+ sooner or later somebody filters the table and the panel does not follow. Everyone who has
2104
+ shipped a dashboard has been in the meeting where two numbers on one screen disagree.
2105
+ </p>
2106
+ <p>
2107
+ A derived grid removes the possibility. It is a second grid whose rows are <em>built from the
2108
+ first</em> &mdash; grouped, unnested, filtered, ranked or profiled &mdash; so the panel is the
2109
+ table, one derivation later, and one filter moves both. It answers the questions a summary
2110
+ panel exists for: the top five sales people, the most-sold SKUs, a statistical profile of
2111
+ whatever the user has filtered to.
2112
+ </p>
2113
+ <p>
2114
+ It is a <em>source</em> rather than a new kind of grid, so everything downstream &mdash; its own sorting and filters,
2115
+ totals, shadow columns, formatting, export, themes &mdash; works on the result and knows
2116
+ nothing about where the rows came from. Charts bind to one as readily as to any grid.
2117
+ </p>
2118
+ <pre><code>source: {
2119
+ mode: 'derived',
2120
+ from: salesGrid,
2121
+ follow: 'filtered',
2122
+
2123
+ groupBy: 'rep',
2124
+ select: { revenue: { of: 'amount', fn: 'sum' }, deals: { fn: 'count' } },
2125
+ sort: [{ col: 'revenue', dir: 'desc' }],
2126
+ limit: 5,
2127
+ }</code></pre>
2128
+ <p class="section-note">
2129
+ The pipeline runs in one order, and the order is the contract:
2130
+ <code>unnest &rarr; where &rarr; bucket &rarr; group &rarr; reduce &rarr; sort &rarr; limit</code>.
2131
+ <code>where</code> sits before grouping deliberately: filtering afterwards is a different
2132
+ question &mdash; which <em>groups</em>, not which <em>rows</em> &mdash; and one key cannot mean both.
2133
+ </p>
2134
+ <div class="table-wrap">
2135
+ <table>
2136
+ <thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
2137
+ <tbody>
2138
+ <tr><td class="name">from</td><td class="type">Grid</td><td class="desc">Required. The grid to read.</td></tr>
2139
+ <tr><td class="name">follow</td><td class="type">'filtered' | 'all' | 'selected' | 'grouped'</td><td class="desc">Which of its rows to read. <code>filtered</code> by default. <code>grouped</code> re-aggregates by whatever dimension the user has grouped the source by, so a panel tracks the reader rather than a dimension fixed when the page was built; with the source ungrouped it falls back to <code>groupBy</code>.</td></tr>
2140
+ <tr><td class="name">unnest</td><td class="type">string</td><td class="desc">Expand an array property, one row per element, keeping the parent's fields. Address the element with a dotted path afterwards &mdash; <code>lines.sku</code> is the element, <code>region</code> is still the parent. A row whose property is absent or empty contributes nothing.</td></tr>
2141
+ <tr><td class="name">where</td><td class="type">(row) =&gt; boolean</td><td class="desc">A row predicate, applied before grouping. With no <code>groupBy</code> the rows pass through as themselves, which is how an exceptions list is built.</td></tr>
2142
+ <tr><td class="name">bucket</td><td class="type">{ of, by }</td><td class="desc">Round a date column down to the start of its period and group on that. <code>by</code> is <code>day</code>, <code>week</code>, <code>month</code>, <code>quarter</code> or <code>year</code>; weeks start on the ISO Monday.</td></tr>
2143
+ <tr><td class="name">groupBy</td><td class="type">string | string[]</td><td class="desc">The dimension, or dimensions, to group by. Omit to pass rows through.</td></tr>
2144
+ <tr><td class="name">select</td><td class="type">Record&lt;string, {of, fn}&gt;</td><td class="desc">The reduced columns, by output id. <code>fn</code> is any key of the totals-row kernels, so <code>median</code>, <code>p95</code>, <code>stddev</code> and <code>gini</code> are available as readily as <code>sum</code>. <code>count</code> needs no <code>of</code>.</td></tr>
2145
+ <tr><td class="name">sort</td><td class="type">{ col, dir }[]</td><td class="desc">Order the derived rows before limiting them. The grid's own user-facing sort is separate and unaffected.</td></tr>
2146
+ <tr><td class="name">limit</td><td class="type">number</td><td class="desc">Keep at most this many rows.</td></tr>
2147
+ <tr><td class="name">limitPer</td><td class="type">string</td><td class="desc">Apply <code>limit</code> within each distinct value of this column rather than overall &mdash; the best three SKUs in <em>each</em> region, which a global limit cannot express.</td></tr>
2148
+ <tr><td class="name">cumulative</td><td class="type">{ of, upTo }</td><td class="desc">Keep rows until their running share of the total reaches <code>upTo</code>, 0 to 1. The Pareto question. The row that crosses the cutoff is kept, because the set has to reach the share.</td></tr>
2149
+ <tr><td class="name">profile</td><td class="type">string | string[]</td><td class="desc">Replaces the pipeline with a transpose: one row per column, with count, present, missing, distinct, min, max, mean, median, quartiles, deviation and outlier count as its columns.</td></tr>
2150
+ <tr><td class="name">orient</td><td class="type">'columns' | 'metrics'</td><td class="desc">With <code>profile</code>, emit one row per statistic instead of one per column &mdash; the shape a dashboard tile wants.</td></tr>
2151
+ <tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">When to re-derive. <code>idle</code> by default, coalescing to a frame, because a hundred cell updates in one frame are one derivation. A number debounces by that many milliseconds.</td></tr>
2152
+ </tbody>
2153
+ </table>
2154
+ </div>
2155
+ <p class="section-note">
2156
+ <strong>Read-only.</strong> A derived row is an answer, not a record: there is no write-back for
2157
+ the sum of four hundred rows, so writes are refused with a reason rather than accepted and
2158
+ discarded on the next refresh.
2159
+ </p>
2160
+ <p class="section-note">
2161
+ <strong>They chain.</strong> A derived grid can be the source of another to any depth &mdash; a
2162
+ profile <em>of</em> the top five &mdash; and a change at the root travels the whole chain. A
2163
+ cycle is refused rather than recursed.
2164
+ </p>
2165
+ <p class="section-note">
2166
+ <strong>The key.</strong> <code>rowKey</code> defaults to the derived key and need not be set.
2167
+ It is the group value, which is what makes a live ranking readable: the row moves rather than
2168
+ the values under it changing.
2169
+ </p>
2170
+ <p class="section-note">
2171
+ <strong>Cost.</strong> Each refresh re-derives from the source in full, so the work is linear in
2172
+ the number of rows read and largely independent of what is reduced &mdash; roughly 650&nbsp;ms
2173
+ per 200,000 rows, whether the selection is one sum or four statistics. For a large source under
2174
+ a live feed, prefer the default <code>idle</code> or a debounce, and narrow with
2175
+ <code>follow: 'filtered'</code> so the derivation reads what the user is looking at rather than
2176
+ the whole table.
2177
+ </p>
2178
+
2035
2179
  <h3>The remote request</h3>
2036
2180
  <p class="section-note">Your <code>fetch</code> receives one object and returns <code>{ rows, total }</code>.</p>
2037
2181
  <div class="table-wrap">
@@ -2274,8 +2418,61 @@ createGrid(el, {
2274
2418
  <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
2419
  <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
2420
 
2421
+ <h2 id="stat">The statistic block</h2>
2422
+ <p>
2423
+ <code>createStat</code> draws the tile a dashboard opens with: a label, a value, its change
2424
+ against a baseline, and a line saying what the comparison was. Two things make it worth using
2425
+ rather than writing. <strong>It reads the grid</strong>, so it cannot disagree with the table
2426
+ beneath it &mdash; a tile saying &pound;4.2M above a table filtered to &pound;1.8M is worse
2427
+ than no tile, and that is what a hand-built tile does the first time somebody adds a filter.
2428
+ And <strong>it formats through the column's own type</strong>: a stat over a
2429
+ <code>seconds</code> column reads <code>42.1 ms</code>, over a money column with an auto ladder
2430
+ <code>&pound;1.2M</code>, with nothing declared.
2431
+ </p>
2432
+ <pre><code>import { createStat } from '@toclocoinc/lattice-grid';
2433
+
2434
+ createStat({
2435
+ grid, container: '#mrr',
2436
+ title: 'Monthly recurring revenue',
2437
+ of: 'mrr', fn: 'sum',
2438
+ baseline: lastMonth,
2439
+ footer: 'vs. last month',
2440
+ });</code></pre>
2441
+ <div class="table-wrap">
2442
+ <table>
2443
+ <thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
2444
+ <tbody>
2445
+ <tr><td class="name">grid</td><td class="type">Grid</td><td class="desc">The grid to read.</td></tr>
2446
+ <tr><td class="name">container</td><td class="type">Element | string</td><td class="desc">Required. An element, or a selector resolved against the grid's document.</td></tr>
2447
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc">The label above the value. Hidden when absent rather than left blank.</td></tr>
2448
+ <tr><td class="name">of</td><td class="type">string</td><td class="desc">The column to reduce. Omit for <code>count</code>.</td></tr>
2449
+ <tr><td class="name">fn</td><td class="type">TotalName</td><td class="desc">Any of the totals-row kernels: <code>sum</code>, <code>avg</code>, <code>median</code>, <code>p95</code>, <code>distinct</code>, <code>gini</code> and the rest. <code>sum</code> by default.</td></tr>
2450
+ <tr><td class="name">show</td><td class="type">string</td><td class="desc">Report this column from the row holding the extreme, rather than the extreme itself &mdash; <code>{ of: 'sales', fn: 'max', show: 'rep' }</code> is the <em>name</em> of the best rep. Needs <code>min</code> or <code>max</code>; no single row holds an average, so any other reduction is refused with a warning.</td></tr>
2451
+ <tr><td class="name">value</td><td class="type">unknown | fn</td><td class="desc">A literal value &mdash; numeric or otherwise &mdash; or a function of the grid, instead of a reduction.</td></tr>
2452
+ <tr><td class="name">footer</td><td class="type">string | fn</td><td class="desc">Text under the value, or a function of it.</td></tr>
2453
+ <tr><td class="name">baseline</td><td class="type">number | fn</td><td class="desc">What the value is compared against. A zero baseline reports the absolute change and no percentage, because &ldquo;up infinity per cent&rdquo; is not a reading anyone can act on.</td></tr>
2454
+ <tr><td class="name">goodWhen</td><td class="type">'up' | 'down' | 'neither'</td><td class="desc">Whether a rise is good news, which decides the colour. <code>up</code> by default. Revenue up is green and error rate up is red; a tile that paints every rise green is misleading on half a dashboard.</td></tr>
2455
+ <tr><td class="name">scope</td><td class="type">'filtered' | 'all' | 'selected'</td><td class="desc">Which rows feed the value. <code>filtered</code> by default; <code>all</code> for a tile that is deliberately a constant, such as the denominator a filtered number is a share of.</td></tr>
2456
+ <tr><td class="name">live</td><td class="type">boolean</td><td class="desc"><code>false</code> stops the tile following the grid. <code>refresh()</code> still works, so a caller can drive it.</td></tr>
2457
+ <tr><td class="name">format</td><td class="type">(value, grid) =&gt; string</td><td class="desc">Override the formatting the column's type would apply.</td></tr>
2458
+ <tr><td class="name">empty</td><td class="type">string</td><td class="desc">Shown when there is no value. An em dash by default.</td></tr>
2459
+ <tr><td class="name">decimals</td><td class="type">number</td><td class="desc">Fraction digits for a value whose reduction changed the unit. 2 by default.</td></tr>
2460
+ </tbody>
2461
+ </table>
2462
+ </div>
2463
+ <p class="section-note">
2464
+ The column's formatter is borrowed only where the reduction leaves the unit alone. A Gini
2465
+ coefficient over a money column is a ratio between 0 and 1, and rendering it as
2466
+ <code>$0.34</code> says it is thirty-four cents; counts, ratios and variances &mdash; which are
2467
+ in units <em>squared</em> &mdash; fall back to a plain number.
2468
+ </p>
2469
+ <p class="section-note">
2470
+ Returns <code>{ element, value, refresh, destroy }</code>. A misconfigured tile returns an inert
2471
+ handle rather than throwing, so a dashboard with one bad tile still renders the other eleven.
2472
+ </p>
2473
+
2277
2474
  <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>
2475
+ <p><code>modules/charts</code> draws thirty-five 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
2476
  <pre><code>import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
2280
2477
 
2281
2478
  const chart = createChart({
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.9.1 — type declarations
2
+ * Lattice Grid 1.10.0 — type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -393,8 +393,8 @@ export type RendererName =
393
393
  export type EditorName =
394
394
  | 'checkbox' | 'code' | 'colour' | 'date' | 'datetime' | 'duration' | 'iconPicker'
395
395
  | 'ipaddress' | 'multiSelect' | 'number' | 'objectPicker' | 'password' | 'radix'
396
- | 'rating' | 'segmented' | 'select' | 'slider' | 'text' | 'textarea' | 'time'
397
- | 'treeSelect' | 'unit' | (string & {});
396
+ | 'rating' | 'segmented' | 'select' | 'slider' | 'temperature' | 'text' | 'textarea'
397
+ | 'time' | 'treeSelect' | 'unit' | (string & {});
398
398
 
399
399
  export interface EditorParams extends CellParams {
400
400
  stop(cancel?: boolean): void;
@@ -757,8 +757,62 @@ export interface StreamSourceConfig {
757
757
  coalesceMs?: number;
758
758
  }
759
759
 
760
+ /** One reduced column of a derived grid. */
761
+ export interface DerivedSelect {
762
+ /** The column to reduce, as a field name or a dotted path. Omit for `count`. */
763
+ of?: string;
764
+ /** A key of `TOTAL_FNS` — `sum`, `avg`, `median`, `p95`, `distinct` and the rest. */
765
+ fn?: TotalName;
766
+ }
767
+
768
+ /**
769
+ * A grid whose rows are derived from another grid: aggregated, unnested,
770
+ * filtered, ranked or profiled. Read-only — write to the source instead.
771
+ */
772
+ export interface DerivedSourceConfig {
773
+ mode: 'derived';
774
+ /**
775
+ * A derived grid keys on `__key`, which the source writes onto every row it
776
+ * produces — the group value, the profiled column, or the source row's own
777
+ * key when nothing is grouped. `config.rowKey` defaults to it, so it need not
778
+ * be set; an explicit `rowKey` still wins.
779
+ */
780
+ /** The grid to read. */
781
+ from: Grid;
782
+ /** Which of its rows to read. `filtered` by default. */
783
+ follow?: 'filtered' | 'all' | 'selected' | 'grouped';
784
+
785
+ /** An array property to expand, one row per element, before anything else. */
786
+ unnest?: string;
787
+ /** A row predicate, applied before grouping. */
788
+ where?: (row: unknown) => boolean;
789
+ /** Round a date column down to a period, and group on that. */
790
+ bucket?: { of: string; by: 'day' | 'week' | 'month' | 'quarter' | 'year' };
791
+ /** The dimension, or dimensions, to group by. Omit to pass rows through. */
792
+ groupBy?: string | string[];
793
+ /** The reduced columns, by output id. */
794
+ select?: Record<string, DerivedSelect>;
795
+ /** How to order the derived rows before limiting them. */
796
+ sort?: { col: string; dir?: 'asc' | 'desc' }[];
797
+ /** Keep at most this many rows. */
798
+ limit?: number;
799
+ /** Apply `limit` within each distinct value of this column, not overall. */
800
+ limitPer?: string;
801
+ /** Keep rows until their running share of the total reaches `upTo`, 0 to 1. */
802
+ cumulative?: { of: string; upTo: number };
803
+
804
+ /** One row per column, with the statistics as columns. Replaces the pipeline. */
805
+ profile?: string | string[];
806
+ /** With `profile`, emit one row per statistic instead of one per column. */
807
+ orient?: 'columns' | 'metrics';
808
+
809
+ /** When to re-derive. `idle` by default — coalesced to a frame. */
810
+ refresh?: 'live' | 'idle' | 'manual' | number;
811
+ }
812
+
760
813
  export type SourceConfig =
761
- | MemorySourceConfig | PagedSourceConfig | RemoteSourceConfig | StreamSourceConfig;
814
+ | MemorySourceConfig | PagedSourceConfig | RemoteSourceConfig | StreamSourceConfig
815
+ | DerivedSourceConfig;
762
816
 
763
817
  // ---------------------------------------------------------------------------
764
818
  // Grid configuration (spec 18.1)
@@ -1033,6 +1087,26 @@ export interface GridConfig {
1033
1087
  */
1034
1088
  showColumnFunctions?: boolean;
1035
1089
  rowHeight?: number | ((row: Row) => number);
1090
+ /**
1091
+ * A caption for the grid, drawn above the column headings.
1092
+ *
1093
+ * Inside the grid rather than an element the host places above it: a title
1094
+ * outside does not scroll with the grid, is not in the region a screen reader
1095
+ * announces, and is left behind by image capture and print.
1096
+ */
1097
+ title?: string;
1098
+ /**
1099
+ * Draw the column headings at all.
1100
+ *
1101
+ * `true` by default. `false` removes the row, and removes it from the
1102
+ * accessibility tree rather than only from view — a heading a screen reader
1103
+ * still announces is invisible, not hidden. What a small dashboard tile
1104
+ * wants when its `title` already says what the panel is.
1105
+ *
1106
+ * Distinct from `showColumnFunctions`, which keeps the headings and drops
1107
+ * only the sort, filter and menu controls inside them.
1108
+ */
1109
+ showHeader?: boolean;
1036
1110
  headerHeight?: number;
1037
1111
  overscan?: number;
1038
1112
  /**
@@ -2796,10 +2870,138 @@ export function parseUnit(text: string | number, opts?: UnitConfig): number | nu
2796
2870
  export function formatUnit(value: number | null | undefined, opts?: UnitConfig): string;
2797
2871
  export const UNIT_SYSTEMS: Record<string, readonly UnitDescriptor[]>;
2798
2872
 
2873
+ /** How a statistic block finds the number it reports. */
2874
+ export interface StatValueSpec {
2875
+ /** The column to reduce, as a field name or a dotted path. Omit for `count`. */
2876
+ of?: string;
2877
+ /** A key of `TOTAL_FNS` — `sum`, `avg`, `median`, `p95`, `gini` and the rest. */
2878
+ fn?: TotalName;
2879
+ /**
2880
+ * Report this column from the row holding the extreme, rather than the
2881
+ * extreme itself: `{ of: 'sales', fn: 'max', show: 'rep' }` is the *name* of
2882
+ * the best rep. Needs `min` or `max` — no single row holds an average.
2883
+ */
2884
+ show?: string;
2885
+ }
2886
+
2887
+ /**
2888
+ * A statistic block: a label, a value, its change, and what it is compared with.
2889
+ *
2890
+ * Reads the grid, so it cannot disagree with the table beneath it, and formats
2891
+ * through the column's own type, so the tile and the table cannot drift.
2892
+ */
2893
+ export interface StatConfig extends StatValueSpec {
2894
+ grid?: Grid;
2895
+ /** An element, or a CSS selector resolved against the grid's document. */
2896
+ container: HTMLElement | string;
2897
+ title?: string;
2898
+ /** A literal value, a spec to reduce, or a function of the grid. */
2899
+ value?: unknown | StatValueSpec | ((grid: Grid) => unknown);
2900
+ /** Text under the value, or a function of it. */
2901
+ footer?: string | ((value: unknown, grid: Grid) => string);
2902
+ /** What the value is compared against, for the change indicator. */
2903
+ baseline?: number | ((grid: Grid) => number);
2904
+ /** Whether a rise is good news. `up` by default. */
2905
+ goodWhen?: 'up' | 'down' | 'neither';
2906
+ /** Which rows feed the value. `filtered` by default. */
2907
+ scope?: 'filtered' | 'all' | 'selected';
2908
+ /** `false` stops the tile following the grid; `refresh()` still works. */
2909
+ live?: boolean;
2910
+ /** Override the formatting the column's type would apply. */
2911
+ format?: (value: unknown, grid: Grid) => string;
2912
+ /** Shown when there is no value. `—` by default. */
2913
+ empty?: string;
2914
+ /** Fraction digits for a value whose reduction changed the unit. 2 by default. */
2915
+ decimals?: number;
2916
+ /** Extra class names for the tile's root. */
2917
+ class?: string;
2918
+ }
2919
+
2920
+ /** The handle `createStat` returns. */
2921
+ export interface Stat {
2922
+ element(): HTMLElement | null;
2923
+ value(): unknown;
2924
+ refresh(): void;
2925
+ destroy(): void;
2926
+ }
2927
+
2928
+ export function createStat(config: StatConfig): Stat;
2929
+ export function deltaOf(value: number | null, baseline: number | null):
2930
+ { direction: 'up' | 'down' | 'flat'; change: number | null; percent: number | null };
2931
+ export function toneOf(direction: string, goodWhen: string): 'good' | 'bad' | 'flat';
2932
+
2799
2933
  export function createGrid(element: HTMLElement, config?: GridConfig): Grid;
2800
2934
  export function createHeadlessGrid(config?: GridConfig): Grid;
2801
2935
  export function registerModules(modules: GridModule[], opts?: { licence?: string }): void;
2802
2936
  export function setLicence(licence: string): LicenceInfo;
2937
+
2938
+ /**
2939
+ * American spellings of the licence functions, exported alongside the British
2940
+ * ones because a host that writes `license` everywhere else should not have to
2941
+ * remember which spelling this one API uses.
2942
+ */
2943
+ export const setLicense: typeof setLicence;
2944
+ export function licenceInfo(): LicenceInfo;
2945
+ export const licenseInfo: typeof licenceInfo;
2946
+ export function licenceState(): LicenceInfo;
2947
+ export const licenseState: typeof licenceState;
2948
+
2949
+ /**
2950
+ * Compile a formatting rule list into a style function.
2951
+ *
2952
+ * `stats` supplies the column summary the distribution operators need — the
2953
+ * top decile, the outliers, two deviations from the mean. Without it those
2954
+ * rules cannot be answered and are skipped.
2955
+ */
2956
+ export function compileRules(
2957
+ rules: FormattingRule[],
2958
+ stats?: object | null,
2959
+ ): (p: CellParams) => CellStyle | null;
2960
+
2961
+ /**
2962
+ * Browser-storage backing for saved views.
2963
+ *
2964
+ * Returns null where no usable storage exists — a private window, or a browser
2965
+ * with site data blocked — so a caller can fall back rather than throw.
2966
+ */
2967
+ export function createLocalViewStorage(opts?: {
2968
+ key?: string;
2969
+ storage?: { getItem: Function; setItem: Function };
2970
+ }): { read(): object[] | null; write(views: object[]): void } | null;
2971
+
2972
+ /** Build a data type for hexadecimal, binary or octal values. */
2973
+ export function createRadixType(config?: object | string): DataType;
2974
+
2975
+ /** Build a column store from row objects, off the main thread where available. */
2976
+ export function ingest(
2977
+ rows: unknown[],
2978
+ plan?: object,
2979
+ opts?: object,
2980
+ ): Promise<{ store: object; schema: object[]; decisions: object[] }>;
2981
+ /** The synchronous form of {@link ingest}. */
2982
+ export function ingestSync(
2983
+ rows: unknown[],
2984
+ plan?: object,
2985
+ opts?: object,
2986
+ ): { store: object; schema: object[]; decisions: object[] };
2987
+
2988
+ /** Mount one tool panel into an element of your own, outside the grid's rail. */
2989
+ export function mountPanel(opts: {
2990
+ grid: Grid;
2991
+ panel: string | Function;
2992
+ container: Element;
2993
+ }): { element: HTMLElement; refresh(): void; destroy(): void };
2994
+
2995
+ /** The column names a compiled formula reads, deduplicated. */
2996
+ export function referencesOf(node: object): string[];
2997
+
2998
+ /** The right-click menu, for a host that drives it directly. */
2999
+ export class ContextMenu {
3000
+ constructor(opts?: object);
3001
+ open(p: object): void;
3002
+ close(): void;
3003
+ destroy(): void;
3004
+ }
2803
3005
  export function version(): string;
2804
3006
 
2805
3007
  export const LatticeGrid: {