@toclocoinc/lattice-grid 1.15.0 → 1.17.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 +1 -1
- package/docs/API.html +473 -14
- package/docs/api-detail.html +334 -5
- package/lattice-grid.d.ts +402 -6
- package/lattice-grid.esm.min.js +4080 -890
- package/lattice-grid.min.cjs +4080 -890
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +4080 -890
- package/modules/charts.esm.min.js +89 -4
- package/modules/devtools.esm.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +46 -4
- package/modules/htmx.esm.min.js +4080 -890
- package/modules/htmx.min.cjs +4080 -890
- package/modules/htmx.min.js +4080 -890
- package/modules/react.esm.min.js +2 -2
- package/modules/svelte.esm.min.js +2 -2
- package/modules/vue.esm.min.js +2 -2
- package/modules/webcomponent.esm.min.js +4080 -890
- package/package.json +1 -1
package/docs/api-detail.html
CHANGED
|
@@ -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.
|
|
440
|
+
<p class="rail__sub">Developer guide · v1.17.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -1533,6 +1533,43 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
|
|
|
1533
1533
|
property, not a search for hex codes.</p>
|
|
1534
1534
|
</div>
|
|
1535
1535
|
|
|
1536
|
+
<div class="example">
|
|
1537
|
+
<p class="example__label">Icon sets: a threshold glyph per value band</p>
|
|
1538
|
+
<pre><code><span class="cmt">// A built-in set — traffic lights, arrows, rating marks — driven by value.</span>
|
|
1539
|
+
cell: { decoration: { type: 'icon', iconSet: 'arrows' } }
|
|
1540
|
+
|
|
1541
|
+
<span class="cmt">// Or your own bands. The highest `min` a value clears wins; a band with no</span>
|
|
1542
|
+
<span class="cmt">// `min` is the catch-all. `label` is what a screen reader announces.</span>
|
|
1543
|
+
cell: { decoration: { type: 'icon', bands: [
|
|
1544
|
+
{ min: 0.9, icon: 'success', label: 'on target', variant: 'success' },
|
|
1545
|
+
{ min: 0.5, icon: 'warning', label: 'at risk', variant: 'warning' },
|
|
1546
|
+
{ icon: 'danger', label: 'off track', variant: 'danger' },
|
|
1547
|
+
] } }</code></pre>
|
|
1548
|
+
</div>
|
|
1549
|
+
<div class="why">
|
|
1550
|
+
<p>An icon set is a restatement of the value, not a replacement for it: the value still
|
|
1551
|
+
renders beside the glyph, and the band's <code>label</code> is set as the glyph's
|
|
1552
|
+
<code>aria-label</code>, so a screen-reader user hears "on target 92%" rather than a bare
|
|
1553
|
+
number with the status lost. The glyphs are the grid's own inline sprites (§16), so an icon
|
|
1554
|
+
set adds no dependency and makes no request. Built-in sets: <code>trafficLights</code>,
|
|
1555
|
+
<code>arrows</code>, <code>trafficArrows</code>, <code>ratings</code>.</p>
|
|
1556
|
+
</div>
|
|
1557
|
+
|
|
1558
|
+
<div class="example">
|
|
1559
|
+
<p class="example__label">Turning a decoration on at runtime</p>
|
|
1560
|
+
<pre><code><span class="cmt">// Set, change or clear a column's decoration after the grid is built.</span>
|
|
1561
|
+
grid.columns.decorate('score', { type: 'bar', min: 0, max: 100 });
|
|
1562
|
+
grid.columns.decorate('trend', { type: 'icon', iconSet: 'arrows' });
|
|
1563
|
+
grid.columns.decorate('score', <span class="kw">null</span>); <span class="cmt">// back to plain text</span></code></pre>
|
|
1564
|
+
</div>
|
|
1565
|
+
<div class="why">
|
|
1566
|
+
<p>A decoration is presentation, so <code>columns.decorate</code> is a live setter like
|
|
1567
|
+
<code>grid.set('theme', …)</code>: it is <em>not</em> on the undo timeline and does
|
|
1568
|
+
<em>not</em> travel in a saved view. For conditional styling that a user edits and a view
|
|
1569
|
+
remembers, reach for <code>grid.formatting</code> below, which holds colour and weight rules
|
|
1570
|
+
as durable state.</p>
|
|
1571
|
+
</div>
|
|
1572
|
+
|
|
1536
1573
|
<div class="example">
|
|
1537
1574
|
<p class="example__label">Your own renderer</p>
|
|
1538
1575
|
<pre><code>components: {
|
|
@@ -1875,6 +1912,41 @@ grid.rows.expandAll();
|
|
|
1875
1912
|
grid.rows.collapse('EMEA');</code></pre>
|
|
1876
1913
|
</div>
|
|
1877
1914
|
|
|
1915
|
+
<p class="lead-in">
|
|
1916
|
+
<code>groupPanel: true</code> adds a drag-and-drop strip above the column header — the
|
|
1917
|
+
row-group panel. A user drags a heading into it to group by that column; the active
|
|
1918
|
+
groups show as removable, reorderable chips, and dragging one chip past another changes
|
|
1919
|
+
the nesting order. It is keyboard-operable, so grouping is not drag-only: arrows move
|
|
1920
|
+
between chips, <code>Shift</code> with an arrow reorders, <code>Delete</code> ungroups,
|
|
1921
|
+
and an add control at the end groups any column. Every change is spoken through the live
|
|
1922
|
+
region. The strip drives <code>grid.columns.group()</code> — it is the same grouping
|
|
1923
|
+
model, surfaced as chrome — so a group made in the strip, from the column menu or through
|
|
1924
|
+
the API is one state, not three.
|
|
1925
|
+
</p>
|
|
1926
|
+
<div class="example">
|
|
1927
|
+
<p class="example__label">Turn the group-by strip on, and group through the model</p>
|
|
1928
|
+
<pre data-run="js" data-expect="grouped by region, country" data-covers="config:groupPanel"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
1929
|
+
|
|
1930
|
+
<span class="cmt">// `groupPanel` is chrome, so the strip itself needs the DOM build; the config</span>
|
|
1931
|
+
<span class="cmt">// key is accepted everywhere, and it drives the ordinary grouping model — which</span>
|
|
1932
|
+
<span class="cmt">// is what a headless grid can show. The strip renders the state below as chips.</span>
|
|
1933
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
1934
|
+
columns: [{ field: 'region' }, { field: 'country' }, { field: 'sales', type: 'number' }],
|
|
1935
|
+
rows: [
|
|
1936
|
+
{ region: 'EMEA', country: 'UK', sales: 10 },
|
|
1937
|
+
{ region: 'EMEA', country: 'DE', sales: 20 },
|
|
1938
|
+
{ region: 'AMER', country: 'US', sales: 30 },
|
|
1939
|
+
],
|
|
1940
|
+
groupPanel: <span class="kw">true</span>,
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
<span class="cmt">// Order is nesting order, outermost first — exactly the order the chips show.</span>
|
|
1944
|
+
grid.columns.group(['region', 'country']);
|
|
1945
|
+
<span class="kw">const</span> groups = grid.state.get().group;
|
|
1946
|
+
grid.destroy();
|
|
1947
|
+
<span class="kw">return</span> `grouped by ${groups.join(', ')}`;</code></pre>
|
|
1948
|
+
</div>
|
|
1949
|
+
|
|
1878
1950
|
<div class="example">
|
|
1879
1951
|
<p class="example__label">Totals</p>
|
|
1880
1952
|
<pre><code>{ field: 'capacity', type: 'number', total: 'sum' }
|
|
@@ -2423,6 +2495,58 @@ createGrid(right, { columns, rows,
|
|
|
2423
2495
|
at silently. Rows with no rate, or no weight, are left out rather than counted as zero.
|
|
2424
2496
|
</p>
|
|
2425
2497
|
|
|
2498
|
+
<h3 id="aggregate-chooser">Choosing an aggregate at runtime</h3>
|
|
2499
|
+
<p class="lead-in">
|
|
2500
|
+
With <code>aggregateChooser</code> on, the column menu's totalling entry becomes an
|
|
2501
|
+
<em>Aggregate</em> submenu. It offers only the reductions the column's type says are meaningful
|
|
2502
|
+
(see <a href="#aggregate-safety">above</a>) — <code>sum</code>, <code>avg</code>,
|
|
2503
|
+
<code>min</code>, <code>max</code> and the counts on a plain number, but never <code>sum</code>
|
|
2504
|
+
on a category or a rate — with the current one ticked and a <em>None</em> to stop totalling. It
|
|
2505
|
+
is the same keyboard-operable menu as everywhere else: arrows move, <code>Enter</code> or
|
|
2506
|
+
<code>Space</code> picks, <code>Escape</code> closes and returns focus, and the ticked item
|
|
2507
|
+
reads as <code>aria-checked</code> to a screen reader.
|
|
2508
|
+
</p>
|
|
2509
|
+
<div class="example">
|
|
2510
|
+
<p class="example__label">Off by default; turn it on</p>
|
|
2511
|
+
<pre><code>createGrid(element, { aggregateChooser: <span class="kw">true</span> });</code></pre>
|
|
2512
|
+
</div>
|
|
2513
|
+
<div class="why">
|
|
2514
|
+
<p><strong>It reuses the totals model, it does not fork it.</strong> Every choice drives
|
|
2515
|
+
<code>grid.columns.setTotal(id, name)</code>, the same public call the old toggle used, so the
|
|
2516
|
+
footer, the group rows, the pivot cells and the grand total all move together and no
|
|
2517
|
+
aggregation is recomputed here. <code>grid.columns.aggregates(id)</code> returns the list the
|
|
2518
|
+
submenu offers, so a host building its own chooser reads the same answer.</p>
|
|
2519
|
+
<p><strong>Safety holds on both routes.</strong> The submenu only lists meaningful aggregates,
|
|
2520
|
+
and <code>setTotal</code> refuses an unmeaningful named total whether it comes from the menu or
|
|
2521
|
+
from an API caller — the wrong footer cannot be reached from either.</p>
|
|
2522
|
+
<p><strong>Off by default and non-breaking.</strong> Left off, the menu keeps its plain
|
|
2523
|
+
<em>Total this column</em> toggle, so an existing grid is unchanged.</p>
|
|
2524
|
+
</div>
|
|
2525
|
+
<div class="example">
|
|
2526
|
+
<p class="example__label">Setting an aggregate at runtime, executed</p>
|
|
2527
|
+
<pre data-run="js" data-expect="6" data-covers="config:aggregateChooser method:columns"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
2528
|
+
|
|
2529
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
2530
|
+
aggregateChooser: <span class="kw">true</span>,
|
|
2531
|
+
columns: [{ field: 'amount', type: 'number', total: 'sum' }, { field: 'region' }],
|
|
2532
|
+
rows: [{ id: '1', amount: 2, region: 'N' }, { id: '2', amount: 4, region: 'S' }],
|
|
2533
|
+
rowKey: 'id',
|
|
2534
|
+
grandTotalRow: 'inline',
|
|
2535
|
+
});
|
|
2536
|
+
grid.rows.count();
|
|
2537
|
+
|
|
2538
|
+
<span class="cmt">// A number column offers every built-in; a text column offers only what makes</span>
|
|
2539
|
+
<span class="cmt">// sense — count and the extremes, never sum. This is the list the chooser shows.</span>
|
|
2540
|
+
<span class="kw">const</span> offered = grid.columns.aggregates('amount'); <span class="cmt">// ['sum','avg','min',...]</span>
|
|
2541
|
+
|
|
2542
|
+
<span class="cmt">// Switch the footer from Sum (6) to Average (3) at runtime.</span>
|
|
2543
|
+
grid.columns.setTotal('amount', 'avg');
|
|
2544
|
+
<span class="kw">const</span> total = grid.rows.get(grid.rows.count() - <span class="num">1</span>).totals.amount;
|
|
2545
|
+
|
|
2546
|
+
grid.destroy();
|
|
2547
|
+
<span class="kw">return</span> offered.includes('sum') ? <span class="num">6</span> : <span class="num">0</span>; <span class="cmt">// sum is offered on a number column</span></code></pre>
|
|
2548
|
+
</div>
|
|
2549
|
+
|
|
2426
2550
|
<h2 id="sticky-group-headings">Sticky group headings</h2>
|
|
2427
2551
|
<p class="lead-in">
|
|
2428
2552
|
Scrolling inside a group keeps that group's headings pinned above the rows, so the rows on
|
|
@@ -2430,9 +2554,9 @@ createGrid(right, { columns, rows,
|
|
|
2430
2554
|
</p>
|
|
2431
2555
|
|
|
2432
2556
|
<div class="example">
|
|
2433
|
-
<p class="example__label">
|
|
2434
|
-
<pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">
|
|
2435
|
-
createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">//
|
|
2557
|
+
<p class="example__label">Off by default; opt in or set the cap</p>
|
|
2558
|
+
<pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">true</span> }); <span class="cmt">// on, up to two</span>
|
|
2559
|
+
createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// on, up to three</span></code></pre>
|
|
2436
2560
|
</div>
|
|
2437
2561
|
|
|
2438
2562
|
<div class="why">
|
|
@@ -2474,6 +2598,41 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2474
2598
|
</tbody>
|
|
2475
2599
|
</table>
|
|
2476
2600
|
</div>
|
|
2601
|
+
<h3 id="ingest">Trimming the memory footprint</h3>
|
|
2602
|
+
<p>
|
|
2603
|
+
By default the store retains the row objects you hand it by reference, so
|
|
2604
|
+
<code>rows.data()</code> returns those exact objects and <code>row === sourceObject</code>
|
|
2605
|
+
holds. The <code>ingest</code> option controls that:
|
|
2606
|
+
</p>
|
|
2607
|
+
<div class="table-wrap">
|
|
2608
|
+
<table>
|
|
2609
|
+
<thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
|
|
2610
|
+
<tbody>
|
|
2611
|
+
<tr><td class="name">retainSource</td><td class="type">boolean</td><td class="desc">Default <code>true</code>. Set <code>false</code> to keep only the packed columns and reconstruct a plain row object from them on demand. <code>rows.data()</code> then returns freshly reconstructed objects — a new object each call — so <code>row === sourceObject</code> and a custom renderer reading <code>row.sourceObject</code> no longer hold, and equality becomes value-based. Cell values are identical either way, so <code>get()</code>, <code>byKey()</code>, <code>value()</code> and <code>values()</code> are unaffected. On its own this drops only the store's reference array, not the objects: the source layer still holds them.</td></tr>
|
|
2612
|
+
<tr><td class="name">dropSourceRows</td><td class="type">boolean</td><td class="desc">Default <code>false</code>. Set <code>true</code> to release the caller's row objects from the <em>source layer</em> and the grid config once the store is built, so the packed columns are the only resident copy. This is where the large reduction lives — roughly an order of magnitude at a million rows — because the caller's objects are the dominant term. Implies <code>retainSource: false</code> (the store must reconstruct), so it carries the same identity tradeoff. An impure computed column — a shadow or a rank/positional column — is never stored, so it cannot be served from the columns: it warns once and drops out rather than answering wrongly, and <code>rows.move()</code> is refused. Do not enable it on a grid that sorts, filters, groups or totals on such a column, or that reorders rows in place.</td></tr>
|
|
2613
|
+
</tbody>
|
|
2614
|
+
</table>
|
|
2615
|
+
</div>
|
|
2616
|
+
<div class="example">
|
|
2617
|
+
<p class="example__label">Dropping retained source objects</p>
|
|
2618
|
+
<pre><code>createGrid(el, {
|
|
2619
|
+
columns,
|
|
2620
|
+
rowKey: 'id',
|
|
2621
|
+
rows,
|
|
2622
|
+
<span class="cmt">// Release the caller's objects entirely; the packed columns are the sole copy.</span>
|
|
2623
|
+
ingest: { dropSourceRows: <span class="kw">true</span> },
|
|
2624
|
+
});</code></pre>
|
|
2625
|
+
</div>
|
|
2626
|
+
<div class="why">
|
|
2627
|
+
<p>With <code>retainSource: false</code> alone the saving is only the store's own copy of the
|
|
2628
|
+
object references, not the objects themselves: the source layer and the grid config still
|
|
2629
|
+
hold the array, so whoever handed the grid its rows keeps them alive. <code>dropSourceRows</code>
|
|
2630
|
+
releases those references too, so once the caller lets go the objects can be collected and the
|
|
2631
|
+
grid becomes the sole holder of the data. Reach for it on a large, read-mostly grid where you
|
|
2632
|
+
can let go of the source array and do not depend on caller identity through
|
|
2633
|
+
<code>rows.data()</code>.</p>
|
|
2634
|
+
</div>
|
|
2635
|
+
|
|
2477
2636
|
<div class="example">
|
|
2478
2637
|
<p class="example__label">A remote source</p>
|
|
2479
2638
|
<pre><code>source: {
|
|
@@ -2591,7 +2750,8 @@ createGrid(el, { columns, rowKey: 'id', source });
|
|
|
2591
2750
|
plan.pushed; <span class="cmt">// the query the adapter was given</span>
|
|
2592
2751
|
plan.residual; <span class="cmt">// { filters, sort, quick } the grid applied after</span>
|
|
2593
2752
|
plan.unpushed; <span class="cmt">// ['filter'], the parts that stayed behind</span>
|
|
2594
|
-
plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span
|
|
2753
|
+
plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span>
|
|
2754
|
+
plan.full; <span class="cmt">// true when fullDataset forced it, not just residual work</span></code></pre>
|
|
2595
2755
|
</div>
|
|
2596
2756
|
<p>
|
|
2597
2757
|
The grid also warns once, naming the predicate that could not be pushed, because the fix is
|
|
@@ -2633,6 +2793,78 @@ createGrid(el, {
|
|
|
2633
2793
|
with no server involved.</p>
|
|
2634
2794
|
</div>
|
|
2635
2795
|
|
|
2796
|
+
<h3 id="fulldataset">Whole-dataset statistics over a remote source</h3>
|
|
2797
|
+
<p class="lead-in">
|
|
2798
|
+
A windowed source reduces a total or statistic over the rows it has loaded, not the whole
|
|
2799
|
+
matching set — a footer median of the 200 rows on screen, which is wrong and looks right.
|
|
2800
|
+
<code>fullDataset</code> makes the source hold the entire matching set client-side so those
|
|
2801
|
+
figures are computed over everything.
|
|
2802
|
+
</p>
|
|
2803
|
+
<div class="example">
|
|
2804
|
+
<p class="example__label">Correct footer figures over a REST or DuckDB source</p>
|
|
2805
|
+
<pre><code><span class="kw">const</span> source = createPushdownSource({
|
|
2806
|
+
adapter: restAdapter({ url: '/api/trades' }),
|
|
2807
|
+
fullDataset: {
|
|
2808
|
+
enabled: <span class="kw">true</span>, <span class="cmt">// hold the whole matching set, once per query</span>
|
|
2809
|
+
maxRows: 1_000_000, <span class="cmt">// refuse (visible error) past this</span>
|
|
2810
|
+
maxBytesEstimate: 512 * 1024 * 1024,
|
|
2811
|
+
},
|
|
2812
|
+
});</code></pre>
|
|
2813
|
+
</div>
|
|
2814
|
+
<div class="why">
|
|
2815
|
+
<p>It is <strong>off by default</strong> and reuses the same whole-result path that residual
|
|
2816
|
+
work already takes: the flag ORs into <code>needsAll</code>, so once the set is held it is
|
|
2817
|
+
ordinary in-memory data and the grid's existing total and statistics kernels reduce over all of
|
|
2818
|
+
it, with no per-stat change. When it is active and within the limits, the whole matching set is
|
|
2819
|
+
covered, so the windowed-statistic warning (BACKLOG-0000731) stays silent — the figure is now
|
|
2820
|
+
honestly whole-dataset.</p>
|
|
2821
|
+
<p><strong>It is refused loudly, never truncated.</strong> A matching set past
|
|
2822
|
+
<code>maxRows</code> or <code>maxBytesEstimate</code> is thrown and surfaced as a
|
|
2823
|
+
<code>source:error</code> with no rows shown, rather than held as a fraction and presented as
|
|
2824
|
+
the whole. A fraction shown as the whole is exactly the silent wrong answer this feature
|
|
2825
|
+
exists to remove, so it is never how the feature fails. For a <code>restAdapter</code>, which
|
|
2826
|
+
cannot compute, this is the only route to a correct whole-dataset statistic.</p>
|
|
2827
|
+
</div>
|
|
2828
|
+
|
|
2829
|
+
<h3 id="aggregates">Pushing statistics down to the engine</h3>
|
|
2830
|
+
<p class="lead-in">
|
|
2831
|
+
A DuckDB-class engine computes a median or a standard deviation over the whole matching set far
|
|
2832
|
+
faster than pulling every row here to do it. The <code>aggregates</code> config decides, at
|
|
2833
|
+
grid setup, which statistics the engine computes and which the grid does — a design-time
|
|
2834
|
+
developer choice, fixed for the life of the grid, never a runtime toggle and never shown to an
|
|
2835
|
+
end user.
|
|
2836
|
+
</p>
|
|
2837
|
+
<div class="example">
|
|
2838
|
+
<p class="example__label">Push the verified-identical stats, keep the rest exact</p>
|
|
2839
|
+
<pre><code><span class="kw">const</span> source = createPushdownSource({
|
|
2840
|
+
adapter: duckdbAdapter({ connection: conn, from: <span class="str">'trades'</span> }),
|
|
2841
|
+
aggregates: {
|
|
2842
|
+
<span class="cmt">// 'engine' pushes everything expressible; 'engine-if-identical' pushes only</span>
|
|
2843
|
+
<span class="cmt">// the stats whose engine result is verified identical to the grid kernel;</span>
|
|
2844
|
+
<span class="cmt">// 'client' (the default when absent) computes everything here.</span>
|
|
2845
|
+
<span class="kw">default</span>: <span class="str">'engine-if-identical'</span>,
|
|
2846
|
+
overrides: { mode: <span class="str">'client'</span> }, <span class="cmt">// I want the grid's null-when-distinct mode</span>
|
|
2847
|
+
},
|
|
2848
|
+
});</code></pre>
|
|
2849
|
+
</div>
|
|
2850
|
+
<div class="why">
|
|
2851
|
+
<p>Every statistic is classified <strong>IDENTICAL</strong> (the engine's result equals the
|
|
2852
|
+
grid's own kernel, verified against it on the same data) or <strong>MAY-DIFFER</strong> (the
|
|
2853
|
+
engine computes it by a method that can differ from the grid's definition — <code>mode</code>
|
|
2854
|
+
returns a value where the grid returns null). The classification drives the docs and
|
|
2855
|
+
build-time provenance, <em>not</em> whether a stat is pushed: that is your choice.
|
|
2856
|
+
<code>weightedQuantile</code> is the one genuine fallback, always client-side, because the
|
|
2857
|
+
engine cannot express the grid's midpoint convention. The published table of every stat, its
|
|
2858
|
+
class and its SQL is generated from one map (<code>STAT_PUSHDOWN</code>) so it cannot drift.</p>
|
|
2859
|
+
<p><strong>No mixed provenance.</strong> An engine figure and a client figure never appear in
|
|
2860
|
+
one result set. Aggregates are pushed only when the filter is <em>fully</em> pushed; a residual
|
|
2861
|
+
filter the engine could not apply forces every aggregate client-side, because an engine number
|
|
2862
|
+
computed over a superset beside a client number over the real set would be wrong-but-plausible.
|
|
2863
|
+
<code>source.lastPlan().aggregates</code> reports, per stat, whether the engine or the grid
|
|
2864
|
+
computed it and the class it was assigned — inspection during the build, not a per-figure
|
|
2865
|
+
runtime marker.</p>
|
|
2866
|
+
</div>
|
|
2867
|
+
|
|
2636
2868
|
<h2 id="derived">Grids built from other grids</h2>
|
|
2637
2869
|
<p class="lead-in">
|
|
2638
2870
|
A derived grid takes its rows from another grid rather than from a load: grouped and
|
|
@@ -2827,6 +3059,27 @@ columns: [
|
|
|
2827
3059
|
scrolling past a rounded corner is cut by it rather than squaring it off.
|
|
2828
3060
|
</p>
|
|
2829
3061
|
|
|
3062
|
+
<div class="example">
|
|
3063
|
+
<p class="example__label">Zebra striping (opt-in)</p>
|
|
3064
|
+
<pre><code>createGrid(element, {
|
|
3065
|
+
columns, rows,
|
|
3066
|
+
stripedRows: <span class="kw">true</span>, <span class="cmt">// shade alternate data rows; off by default</span>
|
|
3067
|
+
});</code></pre>
|
|
3068
|
+
</div>
|
|
3069
|
+
|
|
3070
|
+
<p class="lead-in">
|
|
3071
|
+
<code>stripedRows</code> shades every other data row. It is strictly opt-in and off by default,
|
|
3072
|
+
so a grid that never mentions it looks exactly as it did on upgrade. Parity is decided by each
|
|
3073
|
+
row's <em>logical</em> index rather than its position in the DOM: rows are virtualised and
|
|
3074
|
+
recycled, so a <code>:nth-child</code> rule would repaint the stripe onto whichever row landed
|
|
3075
|
+
in an odd slot after a scroll, and a logical-index stripe keeps a row shaded consistently across
|
|
3076
|
+
a scroll and across the left-pinned, centre and right-pinned segments of the same row. Group
|
|
3077
|
+
headings, group footers and the grand total are structure rather than data, so they are never
|
|
3078
|
+
striped. The stripe uses the theme's <code>--lattice-surface-alt</code> token, which every
|
|
3079
|
+
palette defines, so dark, high-contrast and terminal are correct without any extra rule, and
|
|
3080
|
+
both selection and hover still win over it.
|
|
3081
|
+
</p>
|
|
3082
|
+
|
|
2830
3083
|
<h2 id="cards">Cards, lists and feeds</h2>
|
|
2831
3084
|
<p class="lead-in">
|
|
2832
3085
|
<code>rowTemplate</code> draws each row with a layout of your own instead of dividing it into
|
|
@@ -4490,6 +4743,40 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
|
|
|
4490
4743
|
people fight.</p>
|
|
4491
4744
|
</div>
|
|
4492
4745
|
|
|
4746
|
+
<h3 id="paste-preview">Previewing a bulk paste</h3>
|
|
4747
|
+
<p class="lead-in">
|
|
4748
|
+
A paste is the one clipboard gesture that can rewrite dozens of cells with nothing to inspect
|
|
4749
|
+
first: a payload that lands a column to the left of where it was aimed, or over a range the
|
|
4750
|
+
user forgot was selected, looks exactly like one that worked. Turn on
|
|
4751
|
+
<code>edit.pastePreview</code> and a paste into more than one cell opens a confirm/cancel dialog
|
|
4752
|
+
before anything commits.
|
|
4753
|
+
</p>
|
|
4754
|
+
<div class="example">
|
|
4755
|
+
<p class="example__label">Opt in</p>
|
|
4756
|
+
<pre><code>createGrid(host, {
|
|
4757
|
+
<span class="cmt">// Off by default: an unconfigured grid pastes straight away, as before.</span>
|
|
4758
|
+
edit: { enabled: <span class="kw">true</span>, pastePreview: <span class="kw">true</span> },
|
|
4759
|
+
});</code></pre>
|
|
4760
|
+
</div>
|
|
4761
|
+
<p>
|
|
4762
|
+
The dialog lists every cell that will change, old → new, and every cell a commit
|
|
4763
|
+
would reject — a read-only cell, a value the column's type or <code>edit.validate</code>
|
|
4764
|
+
refuses, a cell a permission policy forbids. <strong>Confirm</strong> commits precisely that set
|
|
4765
|
+
through the ordinary paste path; <strong>Cancel</strong> commits nothing. A single-cell paste
|
|
4766
|
+
skips the dialog — a preview for one cell is friction, not a safety net. The dialog is a
|
|
4767
|
+
modal <code>role="dialog"</code>: <kbd>Escape</kbd> cancels, <kbd>Tab</kbd> stays inside it,
|
|
4768
|
+
focus moves in on open and back on close, and its opening is announced through the grid's live
|
|
4769
|
+
region. The same diff is available without any UI from
|
|
4770
|
+
<code>grid.edit.previewPaste(anchor, text, extent?)</code>, which returns
|
|
4771
|
+
<code>{ changes, rejected }</code> and changes nothing.
|
|
4772
|
+
</p>
|
|
4773
|
+
<div class="why">
|
|
4774
|
+
<p><strong>Why a per-<code>edit</code> flag, off by default.</strong> Paste preview lives under
|
|
4775
|
+
<code>edit</code> because a paste is a bulk edit and its accept/reject decisions are the edit
|
|
4776
|
+
model's — the preview cannot disagree with the commit because it runs the same checks.
|
|
4777
|
+
Off by default keeps every existing grid's paste behaviour exactly as it was.</p>
|
|
4778
|
+
</div>
|
|
4779
|
+
|
|
4493
4780
|
<h2 id="keyboard">Keyboard</h2>
|
|
4494
4781
|
<p class="lead-in">
|
|
4495
4782
|
Press <kbd>?</kbd> in the grid to see this list in the product. The overlay is generated from
|
|
@@ -5002,6 +5289,45 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
5002
5289
|
version, and you do not have to maintain a lookup table keyed by column id alongside the
|
|
5003
5290
|
columns themselves.</p>
|
|
5004
5291
|
</div>
|
|
5292
|
+
|
|
5293
|
+
<h3 id="range-chart">Chart a selected range</h3>
|
|
5294
|
+
<p class="lead-in">
|
|
5295
|
+
<code>rangeChart</code> turns a selected cell range into a chart — the spreadsheet gesture. It
|
|
5296
|
+
is off by default; set it and the cell menu offers <strong>Chart selection</strong>, with
|
|
5297
|
+
<kbd>Alt</kbd>+<kbd>F1</kbd> as the keyboard route, whenever the selected range has a numeric
|
|
5298
|
+
column to plot. The leading text column becomes the categories and the numeric columns beside
|
|
5299
|
+
it become the measures; a hidden or unreadable column is never charted, and the chart is bound
|
|
5300
|
+
to the band of rows the rectangle covers.
|
|
5301
|
+
</p>
|
|
5302
|
+
<p>
|
|
5303
|
+
The DOM layer draws no charts — the charts module is optional and the page loads it — so
|
|
5304
|
+
<code>rangeChart</code> carries the handler that draws. A function, or an object with
|
|
5305
|
+
<code>onChart</code>, is called <code>(grid, range)</code>; it typically calls
|
|
5306
|
+
<code>chartRange</code> from <code>modules/charts</code>, which derives the chart from the
|
|
5307
|
+
range and returns the live <code>Chart</code>.
|
|
5308
|
+
</p>
|
|
5309
|
+
<div class="example">
|
|
5310
|
+
<p class="example__label">Wiring the gesture to the charts module</p>
|
|
5311
|
+
<pre><code>import { chartRange } from '@toclocoinc/lattice-grid/modules/charts';
|
|
5312
|
+
|
|
5313
|
+
createGrid(el, {
|
|
5314
|
+
columns, rows,
|
|
5315
|
+
rangeChart(grid, range) {
|
|
5316
|
+
<span class="cmt">// One numeric column → a bar; several → a grouped bar. Null when the</span>
|
|
5317
|
+
<span class="cmt">// range has nothing to measure, so guard before using it.</span>
|
|
5318
|
+
<span class="kw">const</span> chart = chartRange(grid, { container: '#chart', range });
|
|
5319
|
+
<span class="kw">if</span> (chart) chart.update({ scheme: 'colourblind' });
|
|
5320
|
+
},
|
|
5321
|
+
});</code></pre>
|
|
5322
|
+
</div>
|
|
5323
|
+
<div class="why">
|
|
5324
|
+
<p><strong>Why a handler rather than a flag that just draws.</strong> The charts module is
|
|
5325
|
+
optional by design — a page that never charts never loads it — so the DOM layer cannot draw a
|
|
5326
|
+
chart itself without pulling the whole drawing surface into every bundle. Handing the drawing
|
|
5327
|
+
back to the page keeps that promise, and it is the same seam <code>createChart</code> already
|
|
5328
|
+
uses: the grid is handed to the charts module, never imported by it.</p>
|
|
5329
|
+
</div>
|
|
5330
|
+
|
|
5005
5331
|
<div class="example">
|
|
5006
5332
|
<p class="example__label">A button of your own on the rail</p>
|
|
5007
5333
|
<pre><code>createGrid(el, {
|
|
@@ -5486,6 +5812,9 @@ grid.state.apply(savedView.state);
|
|
|
5486
5812
|
<thead><tr><th>Name</th><th>What it does</th></tr></thead>
|
|
5487
5813
|
<tbody>
|
|
5488
5814
|
<tr><td class="name">createChart</td><td class="desc">Draw one of the thirty-seven chart types from a grid’s own data. It follows the grid’s filters, and a mark can filter the grid back.</td></tr>
|
|
5815
|
+
<tr><td class="name">chartRange</td><td class="desc">Chart a selected cell range — the spreadsheet gesture. Derives the chart from the range’s shape (a leading text column is the categories, the numeric columns the measures), respects hidden and unreadable columns, and returns the live chart or null when there is nothing to measure.</td></tr>
|
|
5816
|
+
<tr><td class="name">canChartRange</td><td class="desc">Whether <code>chartRange</code> would draw something for the grid’s current selection — the question a menu asks before offering the item.</td></tr>
|
|
5817
|
+
<tr><td class="name">deriveRangeSpec</td><td class="desc">Decide what a chart of a range should be without drawing it: the type, the category column, the measures, and a spec ready for <code>createChart</code>.</td></tr>
|
|
5489
5818
|
<tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
|
|
5490
5819
|
<tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
|
|
5491
5820
|
<tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>
|