@toclocoinc/lattice-grid 1.16.0 → 1.18.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 +323 -9
- package/docs/api-detail.html +205 -22
- package/lattice-grid.d.ts +277 -8
- package/lattice-grid.esm.min.js +1888 -561
- package/lattice-grid.min.cjs +1888 -561
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +1888 -561
- package/modules/charts.esm.min.js +2 -2
- package/modules/devtools.esm.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +46 -4
- package/modules/htmx.esm.min.js +1888 -561
- package/modules/htmx.min.cjs +1888 -561
- package/modules/htmx.min.js +1888 -561
- 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 +1888 -561
- 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.18.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -1368,6 +1368,17 @@ dataTypes: {
|
|
|
1368
1368
|
is <code>1.00 MB</code> rather than <code>1,000 kB</code>.
|
|
1369
1369
|
</p>
|
|
1370
1370
|
|
|
1371
|
+
<p class="lead-in">
|
|
1372
|
+
<code>compound: ['ft', 'in']</code> renders one stored number across an ordered subset of the
|
|
1373
|
+
system's units — a length as <code>5 ft 11 in</code>, a duration as <code>1 h 23 m</code>.
|
|
1374
|
+
It is display and parse only: the stored value stays a single base-unit number, so sorting,
|
|
1375
|
+
filtering, grouping and totals are the same arithmetic as any other unit column. The units are
|
|
1376
|
+
sorted largest to smallest, the smallest carries the remainder, and parsing sums the parts, so
|
|
1377
|
+
the display round-trips through a paste. The mid-value <em>editor</em> — keystroke
|
|
1378
|
+
roll-over between feet and inches, caret behaviour at a rung boundary — is a separate,
|
|
1379
|
+
later piece of work; this is the read-and-paste half.
|
|
1380
|
+
</p>
|
|
1381
|
+
|
|
1371
1382
|
<div class="why">
|
|
1372
1383
|
<p><strong>Angles wrap, so their mean is replaced.</strong> The average of 359° and 1° is 0°,
|
|
1373
1384
|
and the arithmetic answer (180°) is a confident, plausible number pointing in exactly the
|
|
@@ -1961,12 +1972,14 @@ grid.destroy();
|
|
|
1961
1972
|
</p>
|
|
1962
1973
|
|
|
1963
1974
|
<p class="lead-in">
|
|
1964
|
-
On a memory source
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
<code>sum</code>, <code>avg</code>, <code>countValues</code>,
|
|
1968
|
-
<code>max</code> on numeric columns.
|
|
1969
|
-
|
|
1975
|
+
On a memory source totals are <strong>maintained incrementally</strong>: a cell update moves
|
|
1976
|
+
the running value by the difference rather than re-reducing the column, so a totals row costs
|
|
1977
|
+
the same on a million rows as on a thousand. This applies to both the grand total and each
|
|
1978
|
+
group subtotal, for <code>sum</code>, <code>avg</code>, <code>countValues</code>,
|
|
1979
|
+
<code>min</code> and <code>max</code> on numeric columns. A cell edit that moves a row between
|
|
1980
|
+
groups is subtracted from its old group and added to its new one; only the affected groups are
|
|
1981
|
+
touched. Everything else re-reduces, and so does the incremental path itself whenever it cannot
|
|
1982
|
+
reach the right answer:
|
|
1970
1983
|
</p>
|
|
1971
1984
|
<div class="table-wrap">
|
|
1972
1985
|
<table>
|
|
@@ -1979,7 +1992,7 @@ grid.destroy();
|
|
|
1979
1992
|
<tr><td class="name">Adding or removing rows</td><td class="desc">Re-reduced once, then incremental again.</td></tr>
|
|
1980
1993
|
<tr><td class="name">Filtering, sorting or grouping</td><td class="desc">Re-reduced once, because the rows contributing to the total have changed.</td></tr>
|
|
1981
1994
|
<tr><td class="name">Totals above ~1e15</td><td class="desc">Re-reduced. Past that magnitude a small change no longer moves a 64-bit float, and a running total would silently stop tracking the data.</td></tr>
|
|
1982
|
-
<tr><td class="name">Group
|
|
1995
|
+
<tr><td class="name">Group subtotals</td><td class="desc">Maintained incrementally the same way the grand total is: an in-group edit applies the difference, and a cross-group move subtracts from the old group and adds to the new. A <code>min</code>/<code>max</code> move that leaves a group's current extreme reseeds only that group. A custom group total, or a statistical reduction, keeps the full per-group pass.</td></tr>
|
|
1983
1996
|
</tbody>
|
|
1984
1997
|
</table>
|
|
1985
1998
|
</div>
|
|
@@ -2073,10 +2086,12 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
2073
2086
|
<pre><code>totalOnlyChangedColumns: <span class="kw">true</span></code></pre>
|
|
2074
2087
|
</div>
|
|
2075
2088
|
<p class="lead-in">
|
|
2076
|
-
|
|
2077
|
-
did not touch. Switching this on
|
|
2078
|
-
an update that rewrites a field with the value it already held reduces nothing at all,
|
|
2079
|
-
which is what a feed resending unchanged fields looks like.
|
|
2089
|
+
By default the total stage considers every totalled column on every change, even columns the
|
|
2090
|
+
change did not touch. Switching this on considers only the columns whose values actually moved,
|
|
2091
|
+
and an update that rewrites a field with the value it already held reduces nothing at all,
|
|
2092
|
+
which is what a feed resending unchanged fields looks like. This narrows <em>which</em> columns
|
|
2093
|
+
are looked at; the incremental grand total and group subtotals described above narrow
|
|
2094
|
+
<em>how</em> each one is brought up to date, and the two compound.
|
|
2080
2095
|
</p>
|
|
2081
2096
|
<p class="lead-in">
|
|
2082
2097
|
On a million rows in seven groups, a single-cell update with four totalled columns:
|
|
@@ -2547,6 +2562,49 @@ grid.destroy();
|
|
|
2547
2562
|
<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
2563
|
</div>
|
|
2549
2564
|
|
|
2565
|
+
<div class="why">
|
|
2566
|
+
<p><strong>The group subtotals and the grand total can reduce differently.</strong> By default
|
|
2567
|
+
one <code>total</code> drives both, and that is unchanged. When a column needs, say, an
|
|
2568
|
+
<em>average per group</em> under a <em>sum of everything</em>, set the two independently with
|
|
2569
|
+
the <code>scope</code> option: <code>setTotal(id, 'avg', { scope: 'group' })</code> and
|
|
2570
|
+
<code>setTotal(id, 'sum', { scope: 'grand' })</code>. A scope with no override falls back to
|
|
2571
|
+
<code>total</code>, and passing no <code>scope</code> sets the shared <code>total</code> and
|
|
2572
|
+
clears both overrides — so the one-property behaviour is exactly what it was. The same split is
|
|
2573
|
+
declarable on a column as <code>groupTotal</code> / <code>grandTotal</code>, and it persists in
|
|
2574
|
+
saved views alongside <code>total</code>.</p>
|
|
2575
|
+
</div>
|
|
2576
|
+
<div class="example">
|
|
2577
|
+
<p class="example__label">Group average under a grand sum, executed</p>
|
|
2578
|
+
<pre data-run="js" data-expect="avg groups under sum grand" data-covers="method:columns config:columns"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
2579
|
+
|
|
2580
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
2581
|
+
columns: [{ field: 'amount', type: 'number', total: 'sum' }, { field: 'region' }],
|
|
2582
|
+
rows: [
|
|
2583
|
+
{ id: '1', amount: 2, region: 'N' }, { id: '2', amount: 6, region: 'N' },
|
|
2584
|
+
{ id: '3', amount: 4, region: 'S' }, { id: '4', amount: 8, region: 'S' },
|
|
2585
|
+
],
|
|
2586
|
+
rowKey: 'id',
|
|
2587
|
+
grandTotalRow: <span class="kw">true</span>,
|
|
2588
|
+
});
|
|
2589
|
+
grid.columns.group(['region']);
|
|
2590
|
+
|
|
2591
|
+
<span class="cmt">// Group subtotals average within each region; the grand total sums everything.</span>
|
|
2592
|
+
grid.columns.setTotal('amount', 'avg', { scope: 'group' });
|
|
2593
|
+
grid.columns.setTotal('amount', 'sum', { scope: 'grand' });
|
|
2594
|
+
|
|
2595
|
+
<span class="cmt">// The first group subtotal (region N: (2+6)/2 = 4) and the grand row (sum = 20).</span>
|
|
2596
|
+
<span class="kw">let</span> groupSubtotal = <span class="kw">null</span>;
|
|
2597
|
+
<span class="kw">for</span> (<span class="kw">let</span> i = <span class="num">0</span>; i < grid.rows.count(); i++) {
|
|
2598
|
+
<span class="kw">const</span> r = grid.rows.get(i);
|
|
2599
|
+
<span class="kw">if</span> (r && r.group && !r.grandTotal && groupSubtotal === <span class="kw">null</span>) groupSubtotal = r.totals.amount;
|
|
2600
|
+
}
|
|
2601
|
+
<span class="kw">const</span> grand = grid.rows.get(grid.rows.count() - <span class="num">1</span>).totals.amount;
|
|
2602
|
+
|
|
2603
|
+
grid.destroy();
|
|
2604
|
+
<span class="kw">return</span> (groupSubtotal === <span class="num">4</span> && grand === <span class="num">20</span>)
|
|
2605
|
+
? 'avg groups under sum grand' : `group ${groupSubtotal}, grand ${grand}`;</code></pre>
|
|
2606
|
+
</div>
|
|
2607
|
+
|
|
2550
2608
|
<h2 id="sticky-group-headings">Sticky group headings</h2>
|
|
2551
2609
|
<p class="lead-in">
|
|
2552
2610
|
Scrolling inside a group keeps that group's headings pinned above the rows, so the rows on
|
|
@@ -2554,9 +2612,9 @@ grid.destroy();
|
|
|
2554
2612
|
</p>
|
|
2555
2613
|
|
|
2556
2614
|
<div class="example">
|
|
2557
|
-
<p class="example__label">
|
|
2558
|
-
<pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">
|
|
2559
|
-
createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">//
|
|
2615
|
+
<p class="example__label">Off by default; opt in or set the cap</p>
|
|
2616
|
+
<pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">true</span> }); <span class="cmt">// on, up to two</span>
|
|
2617
|
+
createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// on, up to three</span></code></pre>
|
|
2560
2618
|
</div>
|
|
2561
2619
|
|
|
2562
2620
|
<div class="why">
|
|
@@ -2608,7 +2666,8 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2608
2666
|
<table>
|
|
2609
2667
|
<thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
|
|
2610
2668
|
<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.</td></tr>
|
|
2669
|
+
<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>
|
|
2670
|
+
<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>
|
|
2612
2671
|
</tbody>
|
|
2613
2672
|
</table>
|
|
2614
2673
|
</div>
|
|
@@ -2618,14 +2677,17 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2618
2677
|
columns,
|
|
2619
2678
|
rowKey: 'id',
|
|
2620
2679
|
rows,
|
|
2621
|
-
|
|
2680
|
+
<span class="cmt">// Release the caller's objects entirely; the packed columns are the sole copy.</span>
|
|
2681
|
+
ingest: { dropSourceRows: <span class="kw">true</span> },
|
|
2622
2682
|
});</code></pre>
|
|
2623
2683
|
</div>
|
|
2624
2684
|
<div class="why">
|
|
2625
|
-
<p>
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2685
|
+
<p>With <code>retainSource: false</code> alone the saving is only the store's own copy of the
|
|
2686
|
+
object references, not the objects themselves: the source layer and the grid config still
|
|
2687
|
+
hold the array, so whoever handed the grid its rows keeps them alive. <code>dropSourceRows</code>
|
|
2688
|
+
releases those references too, so once the caller lets go the objects can be collected and the
|
|
2689
|
+
grid becomes the sole holder of the data. Reach for it on a large, read-mostly grid where you
|
|
2690
|
+
can let go of the source array and do not depend on caller identity through
|
|
2629
2691
|
<code>rows.data()</code>.</p>
|
|
2630
2692
|
</div>
|
|
2631
2693
|
|
|
@@ -2746,7 +2808,8 @@ createGrid(el, { columns, rowKey: 'id', source });
|
|
|
2746
2808
|
plan.pushed; <span class="cmt">// the query the adapter was given</span>
|
|
2747
2809
|
plan.residual; <span class="cmt">// { filters, sort, quick } the grid applied after</span>
|
|
2748
2810
|
plan.unpushed; <span class="cmt">// ['filter'], the parts that stayed behind</span>
|
|
2749
|
-
plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span
|
|
2811
|
+
plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span>
|
|
2812
|
+
plan.full; <span class="cmt">// true when fullDataset forced it, not just residual work</span></code></pre>
|
|
2750
2813
|
</div>
|
|
2751
2814
|
<p>
|
|
2752
2815
|
The grid also warns once, naming the predicate that could not be pushed, because the fix is
|
|
@@ -2788,6 +2851,105 @@ createGrid(el, {
|
|
|
2788
2851
|
with no server involved.</p>
|
|
2789
2852
|
</div>
|
|
2790
2853
|
|
|
2854
|
+
<h3 id="fulldataset">Whole-dataset statistics over a remote source</h3>
|
|
2855
|
+
<p class="lead-in">
|
|
2856
|
+
A windowed source reduces a total or statistic over the rows it has loaded, not the whole
|
|
2857
|
+
matching set — a footer median of the 200 rows on screen, which is wrong and looks right.
|
|
2858
|
+
<code>fullDataset</code> makes the source hold the entire matching set client-side so those
|
|
2859
|
+
figures are computed over everything.
|
|
2860
|
+
</p>
|
|
2861
|
+
<div class="example">
|
|
2862
|
+
<p class="example__label">Correct footer figures over a REST or DuckDB source</p>
|
|
2863
|
+
<pre><code><span class="kw">const</span> source = createPushdownSource({
|
|
2864
|
+
adapter: restAdapter({ url: '/api/trades' }),
|
|
2865
|
+
fullDataset: {
|
|
2866
|
+
enabled: <span class="kw">true</span>, <span class="cmt">// hold the whole matching set, once per query</span>
|
|
2867
|
+
maxRows: 1_000_000, <span class="cmt">// refuse (visible error) past this</span>
|
|
2868
|
+
maxBytesEstimate: 512 * 1024 * 1024,
|
|
2869
|
+
},
|
|
2870
|
+
});</code></pre>
|
|
2871
|
+
</div>
|
|
2872
|
+
<div class="why">
|
|
2873
|
+
<p>It is <strong>off by default</strong> and reuses the same whole-result path that residual
|
|
2874
|
+
work already takes: the flag ORs into <code>needsAll</code>, so once the set is held it is
|
|
2875
|
+
ordinary in-memory data and the grid's existing total and statistics kernels reduce over all of
|
|
2876
|
+
it, with no per-stat change. When it is active and within the limits, the whole matching set is
|
|
2877
|
+
covered, so the windowed-statistic warning (BACKLOG-0000731) stays silent — the figure is now
|
|
2878
|
+
honestly whole-dataset.</p>
|
|
2879
|
+
<p><strong>It is refused loudly, never truncated.</strong> A matching set past
|
|
2880
|
+
<code>maxRows</code> or <code>maxBytesEstimate</code> is thrown and surfaced as a
|
|
2881
|
+
<code>source:error</code> with no rows shown, rather than held as a fraction and presented as
|
|
2882
|
+
the whole. A fraction shown as the whole is exactly the silent wrong answer this feature
|
|
2883
|
+
exists to remove, so it is never how the feature fails. For a <code>restAdapter</code>, which
|
|
2884
|
+
cannot compute, this is the only route to a correct whole-dataset statistic.</p>
|
|
2885
|
+
</div>
|
|
2886
|
+
|
|
2887
|
+
<h3 id="allowpartialresults">Refusing a partial result over residual work</h3>
|
|
2888
|
+
<p class="lead-in">
|
|
2889
|
+
When residual work has to run in the browser, the source asks the adapter for the whole matching
|
|
2890
|
+
set and pages from what it holds. An adapter that answers with a <em>page</em> of that result —
|
|
2891
|
+
it paged when told not to — leaves the client-side filter or sort running over the wrong rows:
|
|
2892
|
+
the ones that belong on page one may be in the fraction never fetched. A page presented as the
|
|
2893
|
+
full filtered set is a wrong answer, not a slow one.
|
|
2894
|
+
</p>
|
|
2895
|
+
<div class="example">
|
|
2896
|
+
<p class="example__label">Refused by default; opt in only when you knowingly accept it</p>
|
|
2897
|
+
<pre><code><span class="cmt">// Default: a shortfall under residual work throws, surfaced as source:error.</span>
|
|
2898
|
+
<span class="kw">const</span> strict = createPushdownSource({ adapter });
|
|
2899
|
+
|
|
2900
|
+
<span class="cmt">// Knowing escape hatch: keep the old warn-once-and-proceed behaviour.</span>
|
|
2901
|
+
<span class="kw">const</span> lax = createPushdownSource({ adapter, allowPartialResults: <span class="kw">true</span> });</code></pre>
|
|
2902
|
+
</div>
|
|
2903
|
+
<div class="why">
|
|
2904
|
+
<p><strong>Off by default, refused loudly.</strong> Filtering or sorting a fraction of the result
|
|
2905
|
+
does not lose rows quietly, it returns the <em>wrong</em> rows, so the default throws rather than
|
|
2906
|
+
warns — <code>nothing fails silently</code> (§8) means a wrong answer is never preferred to a
|
|
2907
|
+
visible failure. The fix is an adapter that follows the engine's own paging, or holding the data
|
|
2908
|
+
in memory. <code>allowPartialResults: true</code> is the deliberate opt-out — a caller who
|
|
2909
|
+
accepts the permissive behaviour keeps the warn-once path — and it is never the default. It does
|
|
2910
|
+
not touch the <code>fullDataset</code> memory guard or the no-residual short-return warning, both
|
|
2911
|
+
of which stand regardless.</p>
|
|
2912
|
+
</div>
|
|
2913
|
+
|
|
2914
|
+
<h3 id="aggregates">Pushing statistics down to the engine</h3>
|
|
2915
|
+
<p class="lead-in">
|
|
2916
|
+
A DuckDB-class engine computes a median or a standard deviation over the whole matching set far
|
|
2917
|
+
faster than pulling every row here to do it. The <code>aggregates</code> config decides, at
|
|
2918
|
+
grid setup, which statistics the engine computes and which the grid does — a design-time
|
|
2919
|
+
developer choice, fixed for the life of the grid, never a runtime toggle and never shown to an
|
|
2920
|
+
end user.
|
|
2921
|
+
</p>
|
|
2922
|
+
<div class="example">
|
|
2923
|
+
<p class="example__label">Push the verified-identical stats, keep the rest exact</p>
|
|
2924
|
+
<pre><code><span class="kw">const</span> source = createPushdownSource({
|
|
2925
|
+
adapter: duckdbAdapter({ connection: conn, from: <span class="str">'trades'</span> }),
|
|
2926
|
+
aggregates: {
|
|
2927
|
+
<span class="cmt">// 'engine' pushes everything expressible; 'engine-if-identical' pushes only</span>
|
|
2928
|
+
<span class="cmt">// the stats whose engine result is verified identical to the grid kernel;</span>
|
|
2929
|
+
<span class="cmt">// 'client' (the default when absent) computes everything here.</span>
|
|
2930
|
+
<span class="kw">default</span>: <span class="str">'engine-if-identical'</span>,
|
|
2931
|
+
overrides: { mode: <span class="str">'client'</span> }, <span class="cmt">// I want the grid's null-when-distinct mode</span>
|
|
2932
|
+
},
|
|
2933
|
+
});</code></pre>
|
|
2934
|
+
</div>
|
|
2935
|
+
<div class="why">
|
|
2936
|
+
<p>Every statistic is classified <strong>IDENTICAL</strong> (the engine's result equals the
|
|
2937
|
+
grid's own kernel, verified against it on the same data) or <strong>MAY-DIFFER</strong> (the
|
|
2938
|
+
engine computes it by a method that can differ from the grid's definition — <code>mode</code>
|
|
2939
|
+
returns a value where the grid returns null). The classification drives the docs and
|
|
2940
|
+
build-time provenance, <em>not</em> whether a stat is pushed: that is your choice.
|
|
2941
|
+
<code>weightedQuantile</code> is the one genuine fallback, always client-side, because the
|
|
2942
|
+
engine cannot express the grid's midpoint convention. The published table of every stat, its
|
|
2943
|
+
class and its SQL is generated from one map (<code>STAT_PUSHDOWN</code>) so it cannot drift.</p>
|
|
2944
|
+
<p><strong>No mixed provenance.</strong> An engine figure and a client figure never appear in
|
|
2945
|
+
one result set. Aggregates are pushed only when the filter is <em>fully</em> pushed; a residual
|
|
2946
|
+
filter the engine could not apply forces every aggregate client-side, because an engine number
|
|
2947
|
+
computed over a superset beside a client number over the real set would be wrong-but-plausible.
|
|
2948
|
+
<code>source.lastPlan().aggregates</code> reports, per stat, whether the engine or the grid
|
|
2949
|
+
computed it and the class it was assigned — inspection during the build, not a per-figure
|
|
2950
|
+
runtime marker.</p>
|
|
2951
|
+
</div>
|
|
2952
|
+
|
|
2791
2953
|
<h2 id="derived">Grids built from other grids</h2>
|
|
2792
2954
|
<p class="lead-in">
|
|
2793
2955
|
A derived grid takes its rows from another grid rather than from a load: grouped and
|
|
@@ -2982,6 +3144,27 @@ columns: [
|
|
|
2982
3144
|
scrolling past a rounded corner is cut by it rather than squaring it off.
|
|
2983
3145
|
</p>
|
|
2984
3146
|
|
|
3147
|
+
<div class="example">
|
|
3148
|
+
<p class="example__label">Zebra striping (opt-in)</p>
|
|
3149
|
+
<pre><code>createGrid(element, {
|
|
3150
|
+
columns, rows,
|
|
3151
|
+
stripedRows: <span class="kw">true</span>, <span class="cmt">// shade alternate data rows; off by default</span>
|
|
3152
|
+
});</code></pre>
|
|
3153
|
+
</div>
|
|
3154
|
+
|
|
3155
|
+
<p class="lead-in">
|
|
3156
|
+
<code>stripedRows</code> shades every other data row. It is strictly opt-in and off by default,
|
|
3157
|
+
so a grid that never mentions it looks exactly as it did on upgrade. Parity is decided by each
|
|
3158
|
+
row's <em>logical</em> index rather than its position in the DOM: rows are virtualised and
|
|
3159
|
+
recycled, so a <code>:nth-child</code> rule would repaint the stripe onto whichever row landed
|
|
3160
|
+
in an odd slot after a scroll, and a logical-index stripe keeps a row shaded consistently across
|
|
3161
|
+
a scroll and across the left-pinned, centre and right-pinned segments of the same row. Group
|
|
3162
|
+
headings, group footers and the grand total are structure rather than data, so they are never
|
|
3163
|
+
striped. The stripe uses the theme's <code>--lattice-surface-alt</code> token, which every
|
|
3164
|
+
palette defines, so dark, high-contrast and terminal are correct without any extra rule, and
|
|
3165
|
+
both selection and hover still win over it.
|
|
3166
|
+
</p>
|
|
3167
|
+
|
|
2985
3168
|
<h2 id="cards">Cards, lists and feeds</h2>
|
|
2986
3169
|
<p class="lead-in">
|
|
2987
3170
|
<code>rowTemplate</code> draws each row with a layout of your own instead of dividing it into
|