@toclocoinc/lattice-grid 1.12.2 → 1.13.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 +56 -13
- package/docs/API.html +221 -10
- package/docs/CHART-CODES.md +1 -1
- package/docs/api-detail.html +488 -40
- package/lattice-grid.d.ts +185 -7
- package/lattice-grid.esm.min.js +684 -46
- package/lattice-grid.min.cjs +667 -46
- package/lattice-grid.min.js +667 -46
- package/modules/charts.esm.min.js +1 -1
- package/modules/devtools.esm.min.js +1 -1
- package/modules/dhtmlx-compat.esm.min.js +667 -46
- package/modules/htmx.esm.min.js +650 -46
- package/modules/htmx.min.cjs +650 -46
- package/modules/htmx.min.js +650 -46
- package/modules/react.esm.min.js +1 -1
- package/modules/svelte.esm.min.js +1 -1
- package/modules/vue.esm.min.js +1 -1
- package/modules/webcomponent.esm.min.js +667 -46
- 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.13.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -468,6 +468,13 @@
|
|
|
468
468
|
<a href="#row-transfer">Rows between grids</a>
|
|
469
469
|
<a href="#column-tags">Column tags</a>
|
|
470
470
|
<a href="#aligned-grids">Aligned grids</a>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="rail__group">
|
|
473
|
+
<span class="rail__label">Connected grids</span>
|
|
474
|
+
<a href="#pushdown">Querying an engine</a>
|
|
475
|
+
<a href="#derived">Derived grids</a>
|
|
476
|
+
<a href="#cross-filter">Cross-filtering</a>
|
|
477
|
+
<a href="#joins">Joining two grids</a>
|
|
471
478
|
<a href="#aggregate-safety">Aggregate safety</a>
|
|
472
479
|
<a href="#sticky-group-headings">Sticky group headings</a>
|
|
473
480
|
<a href="#sources">Large data</a>
|
|
@@ -500,6 +507,13 @@
|
|
|
500
507
|
<a href="#formatting-guide">Formatting a user can change</a>
|
|
501
508
|
<a href="#quickfilter-guide">Quick filter</a>
|
|
502
509
|
<a href="#charts-guide">In-cell charts</a>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="rail__group">
|
|
512
|
+
<span class="rail__label">Statistics</span>
|
|
513
|
+
<a href="#statistics">Profiling</a>
|
|
514
|
+
<a href="#spc">Process control</a>
|
|
515
|
+
<a href="#intervals">Confidence intervals</a>
|
|
516
|
+
<a href="#stat-tiles">Statistic tiles</a>
|
|
503
517
|
<a href="#formulas-guide">Formulas</a>
|
|
504
518
|
<a href="#custom-menu">Custom items</a>
|
|
505
519
|
<a href="#rail">The left rail</a>
|
|
@@ -884,7 +898,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
884
898
|
});
|
|
885
899
|
|
|
886
900
|
grid.events.on('cellClick', (row, column, event) => {
|
|
887
|
-
<span class="cmt">// row and column carry .id, row's own fields sit alongside it
|
|
901
|
+
<span class="cmt">// row and column carry .id, row's own fields sit alongside it,</span>
|
|
888
902
|
<span class="cmt">// the same shape dhtmlx's own IRow/ICol declare.</span>
|
|
889
903
|
});</code></pre>
|
|
890
904
|
</div>
|
|
@@ -915,7 +929,7 @@ grid.events.on('cellClick', (row, column, event) => {
|
|
|
915
929
|
handler with dhtmlx's own positional arguments: <code>(row, column, event)</code>,
|
|
916
930
|
not Lattice's own event object, because that is dhtmlx's own documented signature for
|
|
917
931
|
them. <code>afterRowDrop</code> calls your handler with <code>(data, event)</code>, firing
|
|
918
|
-
from either a same-grid reorder settling or a row landing here from another grid
|
|
932
|
+
from either a same-grid reorder settling or a row landing here from another grid,
|
|
919
933
|
dhtmlx has one event name for what Lattice models as two. Every other mapped event calls your
|
|
920
934
|
handler with Lattice's own event object, under Lattice's own field names, since a wrong guess
|
|
921
935
|
at a fabricated positional shape is worse than an honest one.</p>
|
|
@@ -1076,7 +1090,7 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
|
|
|
1076
1090
|
read at call time, so loading this module never requires htmx to already be on the
|
|
1077
1091
|
page, only to be present by the time a driven request actually fires. It ships as
|
|
1078
1092
|
ESM and as a plain <code><script src></code> build with no bundler required,
|
|
1079
|
-
with zero runtime dependencies beyond the grid itself and, at call time, htmx
|
|
1093
|
+
with zero runtime dependencies beyond the grid itself and, at call time, htmx,
|
|
1080
1094
|
but because it references the grid's own internals directly rather than the copy
|
|
1081
1095
|
already on the page, the bundle carries a full copy of the grid core alongside its
|
|
1082
1096
|
own code, the same trade-off the web component and dhtmlx wrappers already make.</p>
|
|
@@ -1658,7 +1672,7 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
|
|
|
1658
1672
|
lands on every one of them.</p>
|
|
1659
1673
|
<p>The reset uses no <code>!important</code>. It is specificity (0,1,1) and every rule that
|
|
1660
1674
|
dresses a grid element is (0,2,0) or higher, so the grid's own styling always wins and the
|
|
1661
|
-
reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class
|
|
1675
|
+
reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class,
|
|
1662
1676
|
<code>.lattice .lat-cell { … }</code>: outranks it, so overriding the grid deliberately works
|
|
1663
1677
|
exactly as before. Only bare-tag rules are shut out.</p>
|
|
1664
1678
|
<p>It touches box model and decoration only. Nothing in it sets <code>display</code>,
|
|
@@ -1727,7 +1741,7 @@ grid.overlay.hide();</code></pre>
|
|
|
1727
1741
|
<p>This runs the minimum pipeline. An update touching no sorted, filtered or grouped column
|
|
1728
1742
|
skips those stages entirely and only the totals and the affected cells refresh. Adds and
|
|
1729
1743
|
removals are structural and re-run everything.</p>
|
|
1730
|
-
<p>Removals tombstone in place rather than compacting, so every existing index stays valid
|
|
1744
|
+
<p>Removals tombstone in place rather than compacting, so every existing index stays valid,
|
|
1731
1745
|
which is what lets selection, expansion state and cached permutations survive a delete.</p>
|
|
1732
1746
|
</div>
|
|
1733
1747
|
|
|
@@ -1890,7 +1904,7 @@ grid.rows.collapse('EMEA');</code></pre>
|
|
|
1890
1904
|
</table>
|
|
1891
1905
|
</div>
|
|
1892
1906
|
<p class="lead-in">
|
|
1893
|
-
Nothing has to be configured for this, and the reported number is the same either way
|
|
1907
|
+
Nothing has to be configured for this, and the reported number is the same either way,
|
|
1894
1908
|
where a running value cannot be trusted, the column falls back to a full pass rather than
|
|
1895
1909
|
reporting a value it is unsure of.
|
|
1896
1910
|
</p>
|
|
@@ -1936,7 +1950,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1936
1950
|
the near edge, beside the row headings; <code>'after'</code> puts it at the far edge, which is
|
|
1937
1951
|
where a spreadsheet puts a grand total.</p>
|
|
1938
1952
|
<p><strong>It costs a column, not a pass.</strong> A pivoted group row still carries its
|
|
1939
|
-
reduction over every one of its leaves, which is exactly the total across all pivot values
|
|
1953
|
+
reduction over every one of its leaves, which is exactly the total across all pivot values,
|
|
1940
1954
|
so these columns read a number that has already been computed. They also count towards
|
|
1941
1955
|
<code>maxColumns</code>, since they are columns like any other.</p>
|
|
1942
1956
|
<p><strong>Opt in.</strong> Omitted, a pivot has the columns it has always had, so the option
|
|
@@ -1981,7 +1995,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1981
1995
|
<p class="lead-in">
|
|
1982
1996
|
Group totals re-reduce every totalled column on every change, including columns the change
|
|
1983
1997
|
did not touch. Switching this on reduces only the columns whose values actually moved, and
|
|
1984
|
-
an update that rewrites a field with the value it already held reduces nothing at all
|
|
1998
|
+
an update that rewrites a field with the value it already held reduces nothing at all,
|
|
1985
1999
|
which is what a feed resending unchanged fields looks like.
|
|
1986
2000
|
</p>
|
|
1987
2001
|
<p class="lead-in">
|
|
@@ -2078,7 +2092,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
|
|
|
2078
2092
|
<h2 id="full-width-rows">Full-width rows</h2>
|
|
2079
2093
|
<p class="lead-in">
|
|
2080
2094
|
A row drawn as a single band across every column instead of being divided into them: a
|
|
2081
|
-
section banner, an explanatory note, an empty-group message, a “load more” affordance
|
|
2095
|
+
section banner, an explanatory note, an empty-group message, a “load more” affordance,
|
|
2082
2096
|
anything that belongs between rows and is not itself divided by the columns.
|
|
2083
2097
|
</p>
|
|
2084
2098
|
|
|
@@ -2481,6 +2495,275 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2481
2495
|
and the same shape you would have written by hand.</p>
|
|
2482
2496
|
</div>
|
|
2483
2497
|
|
|
2498
|
+
<h2 id="pushdown">Querying an engine directly</h2>
|
|
2499
|
+
<p class="lead-in">
|
|
2500
|
+
A remote source hands you the whole request and leaves the translation to you. A pushdown
|
|
2501
|
+
adapter inverts that: you declare what your engine can answer, and the grid works out what to
|
|
2502
|
+
send and finishes the rest itself.
|
|
2503
|
+
</p>
|
|
2504
|
+
<div class="example">
|
|
2505
|
+
<p class="example__label">An OData endpoint, with nothing to write</p>
|
|
2506
|
+
<pre><code><span class="kw">import</span> { createPushdownSource, odataAdapter } <span class="kw">from</span> 'lattice-grid';
|
|
2507
|
+
|
|
2508
|
+
createGrid(el, {
|
|
2509
|
+
columns,
|
|
2510
|
+
rowKey: 'id',
|
|
2511
|
+
source: createPushdownSource({
|
|
2512
|
+
adapter: odataAdapter({ url: 'https://services.odata.org/V4/Northwind/Northwind.svc/Orders' }),
|
|
2513
|
+
pageSize: 100,
|
|
2514
|
+
}),
|
|
2515
|
+
});</code></pre>
|
|
2516
|
+
</div>
|
|
2517
|
+
<p>
|
|
2518
|
+
Four adapters ship. <code>odataAdapter</code> writes <code>$filter</code>,
|
|
2519
|
+
<code>$orderby</code>, <code>$top</code> and <code>$skip</code>, and follows
|
|
2520
|
+
<code>@odata.nextLink</code> when the server pages on its own terms.
|
|
2521
|
+
<code>restAdapter</code> covers an ordinary JSON endpoint whose parameter names are yours to
|
|
2522
|
+
give. <code>dfqlAdapter</code> speaks DemandFlow's query API. <code>duckdbAdapter</code>
|
|
2523
|
+
takes a live DuckDB connection.
|
|
2524
|
+
</p>
|
|
2525
|
+
|
|
2526
|
+
<h3>Declaring what an engine can do</h3>
|
|
2527
|
+
<p class="lead-in">
|
|
2528
|
+
No real engine answers the whole query. An adapter says what it can take, and everything
|
|
2529
|
+
undeclared stays with the grid.
|
|
2530
|
+
</p>
|
|
2531
|
+
<div class="example">
|
|
2532
|
+
<p class="example__label">An endpoint that pages and sorts, but does not filter</p>
|
|
2533
|
+
<pre><code>restAdapter({
|
|
2534
|
+
url: '/api/readings',
|
|
2535
|
+
params: { offset: 'from', limit: 'size', sort: 'orderBy', order: 'dir' },
|
|
2536
|
+
capabilities: { sort: 'single', range: <span class="kw">true</span>, total: <span class="kw">true</span> },
|
|
2537
|
+
})</code></pre>
|
|
2538
|
+
</div>
|
|
2539
|
+
<div class="table-wrap">
|
|
2540
|
+
<table>
|
|
2541
|
+
<thead><tr><th>Capability</th><th>Values</th><th>Means</th></tr></thead>
|
|
2542
|
+
<tbody>
|
|
2543
|
+
<tr><td class="name">filter</td><td class="type">false | 'term' | 'flat' | 'tree'</td><td class="desc">Nothing, one field and term, a flat conjunction, or a full condition tree.</td></tr>
|
|
2544
|
+
<tr><td class="name">operators</td><td class="type">string[]</td><td class="desc">Which comparisons the engine genuinely applies. Declare only those it does.</td></tr>
|
|
2545
|
+
<tr><td class="name">sort</td><td class="type">false | 'single' | 'multi'</td><td class="desc">No ordering, one column, or several.</td></tr>
|
|
2546
|
+
<tr><td class="name">quick</td><td class="type">boolean</td><td class="desc">Whether a free-text search across columns can be pushed.</td></tr>
|
|
2547
|
+
<tr><td class="name">range</td><td class="type">boolean</td><td class="desc">Whether the engine can return a window rather than the whole result.</td></tr>
|
|
2548
|
+
<tr><td class="name">total</td><td class="type">boolean</td><td class="desc">Whether it can report how many rows matched.</td></tr>
|
|
2549
|
+
<tr><td class="name">group</td><td class="type">boolean</td><td class="desc">Whether it can group and aggregate.</td></tr>
|
|
2550
|
+
</tbody>
|
|
2551
|
+
</table>
|
|
2552
|
+
</div>
|
|
2553
|
+
<div class="why">
|
|
2554
|
+
<p><strong>Everything is off unless declared.</strong> An adapter that declares nothing still
|
|
2555
|
+
works: the grid fetches and does all the work itself. That is the safe direction to be wrong
|
|
2556
|
+
in. Declaring an operator the engine does not really apply is the unsafe one, because the
|
|
2557
|
+
grid will trust it and stop checking.</p>
|
|
2558
|
+
<p><strong>Splitting a filter is not symmetric.</strong> An <code>and</code> group narrows
|
|
2559
|
+
with each condition, so the supported conjuncts go to the engine and the rest stay behind:
|
|
2560
|
+
the engine returns a superset and the grid narrows it. An <code>or</code> group widens with
|
|
2561
|
+
each branch, so pushing only the supported branches would return fewer rows than the filter
|
|
2562
|
+
allows, and the grid cannot recover rows that were never fetched. A disjunction that is not
|
|
2563
|
+
fully supported therefore stays whole on the client. The same asymmetry governs column
|
|
2564
|
+
pruning in the facet path.</p>
|
|
2565
|
+
<p><strong>Residual work needs the whole result.</strong> When anything is left over, the
|
|
2566
|
+
source stops asking for windows and asks for everything, applies the remainder, and pages
|
|
2567
|
+
from what it holds. Filtering a window on the client is not a slower route to the right
|
|
2568
|
+
answer, it is a fast route to a wrong one: the rows that belong on page one may sit on page
|
|
2569
|
+
nine, and the total is whatever the engine happened to count.</p>
|
|
2570
|
+
</div>
|
|
2571
|
+
|
|
2572
|
+
<h3>Seeing what was pushed</h3>
|
|
2573
|
+
<p class="lead-in">
|
|
2574
|
+
The split is reported rather than hidden, which is the difference between a slow query you
|
|
2575
|
+
can diagnose and a slow query you cannot.
|
|
2576
|
+
</p>
|
|
2577
|
+
<div class="example">
|
|
2578
|
+
<p class="example__label">Asking after the last request</p>
|
|
2579
|
+
<pre><code><span class="kw">const</span> source = createPushdownSource({ adapter });
|
|
2580
|
+
createGrid(el, { columns, rowKey: 'id', source });
|
|
2581
|
+
|
|
2582
|
+
<span class="kw">const</span> plan = source.lastPlan();
|
|
2583
|
+
plan.pushed; <span class="cmt">// the query the adapter was given</span>
|
|
2584
|
+
plan.residual; <span class="cmt">// { filters, sort, quick } the grid applied after</span>
|
|
2585
|
+
plan.unpushed; <span class="cmt">// ['filter'], the parts that stayed behind</span>
|
|
2586
|
+
plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span></code></pre>
|
|
2587
|
+
</div>
|
|
2588
|
+
<p>
|
|
2589
|
+
The grid also warns once, naming the predicate that could not be pushed, because the fix is
|
|
2590
|
+
usually a better adapter rather than a bigger machine. It warns again if an adapter reports a
|
|
2591
|
+
<code>total</code> larger than the rows it returned while residual work is outstanding: that
|
|
2592
|
+
combination silently produces wrong answers, and it is worth knowing about.
|
|
2593
|
+
</p>
|
|
2594
|
+
|
|
2595
|
+
<h3>A full analytical engine, without carrying one</h3>
|
|
2596
|
+
<p class="lead-in">
|
|
2597
|
+
<code>duckdbAdapter</code> takes a connection you created and imports nothing, so the grid
|
|
2598
|
+
can drive DuckDB while this package stays at zero dependencies.
|
|
2599
|
+
</p>
|
|
2600
|
+
<div class="example">
|
|
2601
|
+
<p class="example__label">Parquet in the browser, no server</p>
|
|
2602
|
+
<pre><code><span class="kw">const</span> db = <span class="kw">await</span> makeDuckDB(); <span class="cmt">// yours: @duckdb/duckdb-wasm</span>
|
|
2603
|
+
<span class="kw">const</span> conn = <span class="kw">await</span> db.connect();
|
|
2604
|
+
|
|
2605
|
+
createGrid(el, {
|
|
2606
|
+
columns,
|
|
2607
|
+
source: createPushdownSource({
|
|
2608
|
+
adapter: duckdbAdapter({
|
|
2609
|
+
connection: conn,
|
|
2610
|
+
from: <span class="str">"read_parquet('readings.parquet')"</span>,
|
|
2611
|
+
}),
|
|
2612
|
+
}),
|
|
2613
|
+
});</code></pre>
|
|
2614
|
+
</div>
|
|
2615
|
+
<div class="why">
|
|
2616
|
+
<p><code>from</code> is any FROM expression, so
|
|
2617
|
+
<code>read_parquet('s3://bucket/*.parquet')</code> is as valid as a table name. Values are
|
|
2618
|
+
bound through prepared statements; a connection without <code>prepare</code> is used only for
|
|
2619
|
+
unfiltered queries, because interpolating a user's filter into SQL is the one thing worse
|
|
2620
|
+
than not filtering at all.</p>
|
|
2621
|
+
<p>Column and table names are checked against an identifier pattern rather than escaped, and
|
|
2622
|
+
a name that fails is refused. Integers past the safe range are kept as strings instead of
|
|
2623
|
+
being rounded into a plausible lie.</p>
|
|
2624
|
+
<p><code>demo/duckdb.html</code> runs this against a Parquet file of several million readings
|
|
2625
|
+
with no server involved.</p>
|
|
2626
|
+
</div>
|
|
2627
|
+
|
|
2628
|
+
<h2 id="derived">Grids built from other grids</h2>
|
|
2629
|
+
<p class="lead-in">
|
|
2630
|
+
A derived grid takes its rows from another grid rather than from a load: grouped and
|
|
2631
|
+
aggregated, unnested, filtered, ranked or profiled. It has its own element and its own
|
|
2632
|
+
columns, and it follows its source live.
|
|
2633
|
+
</p>
|
|
2634
|
+
<div class="example">
|
|
2635
|
+
<p class="example__label">A summary panel beside the detail grid</p>
|
|
2636
|
+
<pre><code><span class="kw">const</span> detail = createGrid(left, { columns, rowKey: 'id', rows });
|
|
2637
|
+
|
|
2638
|
+
createGrid(right, {
|
|
2639
|
+
columns: [
|
|
2640
|
+
{ id: 'region', header: 'Region' },
|
|
2641
|
+
{ id: 'total', header: 'Capacity', type: 'number' },
|
|
2642
|
+
{ id: 'n', header: 'Sites', type: 'number' },
|
|
2643
|
+
],
|
|
2644
|
+
source: {
|
|
2645
|
+
mode: 'derived',
|
|
2646
|
+
from: detail,
|
|
2647
|
+
groupBy: 'region',
|
|
2648
|
+
select: {
|
|
2649
|
+
total: { of: 'capacity', fn: 'sum' },
|
|
2650
|
+
n: { fn: 'count' },
|
|
2651
|
+
},
|
|
2652
|
+
sort: [{ col: 'total', dir: 'desc' }],
|
|
2653
|
+
},
|
|
2654
|
+
});</code></pre>
|
|
2655
|
+
</div>
|
|
2656
|
+
<p>
|
|
2657
|
+
<code>follow</code> chooses which of the source's rows are read:
|
|
2658
|
+
<code>filtered</code> by default, or <code>all</code>, <code>selected</code> or
|
|
2659
|
+
<code>grouped</code>. The pipeline runs <code>unnest</code>, then <code>join</code>, then
|
|
2660
|
+
<code>where</code>, then <code>bucket</code> and <code>groupBy</code>, then
|
|
2661
|
+
<code>select</code>, then <code>sort</code> and <code>limit</code>, so a condition or a total
|
|
2662
|
+
can read a field that an earlier stage produced.
|
|
2663
|
+
</p>
|
|
2664
|
+
<div class="why">
|
|
2665
|
+
<p><strong>A change is patched, not re-derived.</strong> When a row changes in the source, the
|
|
2666
|
+
derived grid updates the groups that row belongs to rather than rebuilding the lot. Five
|
|
2667
|
+
hundred updates against a two hundred thousand row source cost under 300 ms in total. Set
|
|
2668
|
+
<code>refresh</code> to <code>live</code>, <code>manual</code> or a number of milliseconds to
|
|
2669
|
+
change the coalescing; <code>idle</code> is the default and settles to a frame.</p>
|
|
2670
|
+
<p><strong>Derived grids are read-only.</strong> There is one copy of the data and it lives in
|
|
2671
|
+
the source. Write there and the derived grid follows.</p>
|
|
2672
|
+
<p><strong>The key comes for free.</strong> A derived grid keys on <code>__key</code>, which
|
|
2673
|
+
the source writes onto every row it produces: the group value, the profiled column, or the
|
|
2674
|
+
source row's own key when nothing is grouped. Set <code>rowKey</code> only to override it.</p>
|
|
2675
|
+
</div>
|
|
2676
|
+
|
|
2677
|
+
<h3>Other shapes</h3>
|
|
2678
|
+
<div class="table-wrap">
|
|
2679
|
+
<table>
|
|
2680
|
+
<thead><tr><th>Option</th><th>Does</th></tr></thead>
|
|
2681
|
+
<tbody>
|
|
2682
|
+
<tr><td class="name">unnest</td><td class="desc">Expands an array property, one row per element, before anything else runs.</td></tr>
|
|
2683
|
+
<tr><td class="name">bucket</td><td class="desc">Rounds a date column down to a day, week, month, quarter or year and groups on that.</td></tr>
|
|
2684
|
+
<tr><td class="name">limitPer</td><td class="desc">Applies <code>limit</code> within each value of a column rather than overall: a top three per region.</td></tr>
|
|
2685
|
+
<tr><td class="name">cumulative</td><td class="desc">Keeps rows until their running share of the total reaches a fraction: the Pareto head.</td></tr>
|
|
2686
|
+
<tr><td class="name">profile</td><td class="desc">One row per column with the statistics as columns, or one row per statistic with <code>orient: 'metrics'</code>.</td></tr>
|
|
2687
|
+
</tbody>
|
|
2688
|
+
</table>
|
|
2689
|
+
</div>
|
|
2690
|
+
|
|
2691
|
+
<h2 id="cross-filter">Cross-filtering</h2>
|
|
2692
|
+
<p class="lead-in">
|
|
2693
|
+
A derived panel can filter the grid it summarises. Click a region in the summary and the
|
|
2694
|
+
detail grid narrows to it.
|
|
2695
|
+
</p>
|
|
2696
|
+
<div class="example">
|
|
2697
|
+
<p class="example__label">Click to filter, click again to release</p>
|
|
2698
|
+
<pre><code>source: {
|
|
2699
|
+
mode: 'derived',
|
|
2700
|
+
from: detail,
|
|
2701
|
+
groupBy: 'region',
|
|
2702
|
+
select: { total: { of: 'capacity', fn: 'sum' } },
|
|
2703
|
+
crossFilter: <span class="kw">true</span>, <span class="cmt">// or a source column name</span>
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
summary.events.on('rowClick', (e) => summary.crossFilter.toggle(e.key));</code></pre>
|
|
2707
|
+
</div>
|
|
2708
|
+
<p>
|
|
2709
|
+
<code>crossFilter.set</code>, <code>toggle</code>, <code>clear</code>, <code>get</code> and
|
|
2710
|
+
<code>column</code> make up the API. <code>true</code> filters through whatever the grid
|
|
2711
|
+
groups by; a string names a different source column when the two do not share a name.
|
|
2712
|
+
</p>
|
|
2713
|
+
<div class="why">
|
|
2714
|
+
<p><strong>A panel does not filter itself.</strong> The filter a summary pushes onto its
|
|
2715
|
+
source is excluded when that same summary re-derives. Without that, clicking one region
|
|
2716
|
+
collapses the panel to the single row you just clicked, and there is nothing left to click
|
|
2717
|
+
next. With it, the panel keeps its full set of regions with the chosen one marked, which is
|
|
2718
|
+
what makes a second click possible at all.</p>
|
|
2719
|
+
<p><strong>Several panels compose.</strong> Each pushes its own filter onto the shared source
|
|
2720
|
+
and each excludes only its own, so region and status narrow the detail together while both
|
|
2721
|
+
panels stay navigable.</p>
|
|
2722
|
+
</div>
|
|
2723
|
+
|
|
2724
|
+
<h2 id="joins">Joining two grids</h2>
|
|
2725
|
+
<p class="lead-in">
|
|
2726
|
+
Two grids holding their own data, and a third showing where they meet. Both sides stay live.
|
|
2727
|
+
</p>
|
|
2728
|
+
<div class="example">
|
|
2729
|
+
<p class="example__label">Bringing an owner's fields across</p>
|
|
2730
|
+
<pre><code>source: {
|
|
2731
|
+
mode: 'derived',
|
|
2732
|
+
from: sites,
|
|
2733
|
+
join: {
|
|
2734
|
+
with: owners,
|
|
2735
|
+
on: { left: 'ownerId', right: 'id' },
|
|
2736
|
+
type: 'left',
|
|
2737
|
+
select: ['name', 'tier'],
|
|
2738
|
+
prefix: 'owner', <span class="cmt">// owner.name, owner.tier</span>
|
|
2739
|
+
},
|
|
2740
|
+
}</code></pre>
|
|
2741
|
+
</div>
|
|
2742
|
+
<div class="table-wrap">
|
|
2743
|
+
<table>
|
|
2744
|
+
<thead><tr><th>Option</th><th>Does</th></tr></thead>
|
|
2745
|
+
<tbody>
|
|
2746
|
+
<tr><td class="name">on</td><td class="desc">One field name when both sides use it, or <code>{ left, right }</code> when they differ.</td></tr>
|
|
2747
|
+
<tr><td class="name">type</td><td class="desc"><code>inner</code> keeps only rows that matched; <code>left</code> keeps them all.</td></tr>
|
|
2748
|
+
<tr><td class="name">select</td><td class="desc">Which of the partner's fields to bring across. All of them by default.</td></tr>
|
|
2749
|
+
<tr><td class="name">prefix</td><td class="desc">Renames the brought-across fields, for when both sides have a <code>name</code> worth keeping.</td></tr>
|
|
2750
|
+
<tr><td class="name">follow</td><td class="desc">Which of the partner's rows to read: <code>all</code> by default, or <code>filtered</code>.</td></tr>
|
|
2751
|
+
</tbody>
|
|
2752
|
+
</table>
|
|
2753
|
+
</div>
|
|
2754
|
+
<div class="why">
|
|
2755
|
+
<p><strong>A left join is usually the one you want.</strong> An inner join quietly drops the
|
|
2756
|
+
rows that did not match, and those are often the finding: the site with no owner, the payment
|
|
2757
|
+
with no invoice. <code>left</code> keeps them visible with the partner's fields empty, so the
|
|
2758
|
+
gap is something you can see and sort by rather than something you have to notice is
|
|
2759
|
+
missing.</p>
|
|
2760
|
+
<p><strong>First match wins.</strong> The join is a lookup, not a cross product: a row on the
|
|
2761
|
+
left produces exactly one row out, so a grid of ten thousand rows stays a grid of ten thousand
|
|
2762
|
+
rows and cannot silently multiply.</p>
|
|
2763
|
+
<p><strong>Both sides are live.</strong> A change on either grid updates the join, and it is
|
|
2764
|
+
patched from whichever side changed rather than rebuilt.</p>
|
|
2765
|
+
</div>
|
|
2766
|
+
|
|
2484
2767
|
<h2 id="editing">Editing</h2>
|
|
2485
2768
|
<div class="example">
|
|
2486
2769
|
<p class="example__label">Turning it on</p>
|
|
@@ -2620,8 +2903,8 @@ columns: [
|
|
|
2620
2903
|
<p class="lead-in">
|
|
2621
2904
|
<strong>Sorting and filtering need a home</strong> when there are no column headings to click,
|
|
2622
2905
|
and the tool panel is it: set <code>toolPanel: true</code> and its rail stays available in card
|
|
2623
|
-
presentation with the columns and filter panels behind it. <strong>Export is unaffected</strong
|
|
2624
|
-
|
|
2906
|
+
presentation with the columns and filter panels behind it. <strong>Export is unaffected</strong>:
|
|
2907
|
+
the columns are still the data model, so a CSV or an Excel file from a collapsed grid holds
|
|
2625
2908
|
every column, including ones the card does not show.
|
|
2626
2909
|
</p>
|
|
2627
2910
|
|
|
@@ -2697,7 +2980,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2697
2980
|
</div>
|
|
2698
2981
|
|
|
2699
2982
|
<p class="lead-in">
|
|
2700
|
-
Tiles are a fixed height, taken from <code>rowHeight</code
|
|
2983
|
+
Tiles are a fixed height, taken from <code>rowHeight</code>,
|
|
2701
2984
|
<code>rowHeight: 'auto'</code> measures a rendered row and cannot describe a line holding
|
|
2702
2985
|
several of different heights. Variable-height tiles flowing into the shortest column is a
|
|
2703
2986
|
masonry layout, which is a different thing and is not offered.
|
|
@@ -2780,7 +3063,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2780
3063
|
<p><strong>And a load that never answers is a failure too.</strong> A promise that neither
|
|
2781
3064
|
resolves nor rejects is what a dropped request looks like from the page; left alone it spins
|
|
2782
3065
|
until the user gives up, which reads as an application that has hung rather than a request
|
|
2783
|
-
that failed. After <code>timeout</code> milliseconds, two seconds unless you say otherwise
|
|
3066
|
+
that failed. After <code>timeout</code> milliseconds, two seconds unless you say otherwise,
|
|
2784
3067
|
the form stops waiting and shows the same message and retry as any other failure. Set
|
|
2785
3068
|
<code>timeout: false</code> to wait indefinitely, which is right only where your own loader
|
|
2786
3069
|
already has a limit and would rather report that one. A record that turns up after the form
|
|
@@ -2951,7 +3234,7 @@ socket.onmessage = (m) => {
|
|
|
2951
3234
|
<div class="why">
|
|
2952
3235
|
<p><strong>Two behaviours worth knowing.</strong> An unconfirmed edit goes through the normal
|
|
2953
3236
|
pipeline, so if it changes a sorted or filtered column the row moves immediately and moves
|
|
2954
|
-
back if the write fails. And undo of an in-flight edit issues a <em>compensating write</em
|
|
3237
|
+
back if the write fails. And undo of an in-flight edit issues a <em>compensating write</em>,
|
|
2955
3238
|
a fresh write back to the previous value, itself tracked: rather than pretending to cancel a
|
|
2956
3239
|
request that has already gone out.</p>
|
|
2957
3240
|
</div>
|
|
@@ -3129,7 +3412,7 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
|
|
|
3129
3412
|
}
|
|
3130
3413
|
|
|
3131
3414
|
grid.on('detail:cell:changed', (e) => {
|
|
3132
|
-
e.masterKey; <span class="cmt">// 'C1'
|
|
3415
|
+
e.masterKey; <span class="cmt">// 'C1', the row the detail belongs to</span>
|
|
3133
3416
|
e.path; <span class="cmt">// 'ports.1.vlan': where it lands on the master's record</span>
|
|
3134
3417
|
e.value; <span class="cmt">// 999</span>
|
|
3135
3418
|
e.oldValue; <span class="cmt">// 101</span>
|
|
@@ -3147,7 +3430,7 @@ grid.on('detail:cell:changed', (e) => {
|
|
|
3147
3430
|
a host can persist a detail edit against the master and never think about the nested grid at
|
|
3148
3431
|
all. It is worked out by identity: <code>rows(row)</code> usually returns an array that is
|
|
3149
3432
|
already a property of the record, and that property is the prefix. A detail fetched from a
|
|
3150
|
-
server is not part of the master's record, so its <code>path</code> is <code>null</code
|
|
3433
|
+
server is not part of the master's record, so its <code>path</code> is <code>null</code>,
|
|
3151
3434
|
set <code>detail.path</code> to name it yourself when you want one anyway.
|
|
3152
3435
|
</p>
|
|
3153
3436
|
<p class="lead-in">
|
|
@@ -3173,7 +3456,7 @@ grid.on('detail:cell:changed', (e) => {
|
|
|
3173
3456
|
|
|
3174
3457
|
<h2 id="selection-guide">Selection and ranges</h2>
|
|
3175
3458
|
<p class="lead-in">
|
|
3176
|
-
Row selection and cell ranges are separate answers to separate questions
|
|
3459
|
+
Row selection and cell ranges are separate answers to separate questions,
|
|
3177
3460
|
<em>which records</em> versus <em>which values</em>. Dragging across cells does not tick row
|
|
3178
3461
|
checkboxes, and selecting rows does not build a range.
|
|
3179
3462
|
</p>
|
|
@@ -3327,7 +3610,7 @@ grid.updates.log({ since: Date.now() - 60000 }); <span class="cmt">// what arr
|
|
|
3327
3610
|
many it let go through <code>evicted</code> on the progress report.</p>
|
|
3328
3611
|
<p><strong>The log keeps the raw sequence, not the merged one.</strong> Merging is right for
|
|
3329
3612
|
applying a backlog quickly and wrong for looking at what happened, because the intermediate
|
|
3330
|
-
states are exactly what a time scrubber would move between. It survives the flush
|
|
3613
|
+
states are exactly what a time scrubber would move between. It survives the flush,
|
|
3331
3614
|
<code>pending</code> is what is waiting, the log is what happened, and it is capped, so a
|
|
3332
3615
|
grid paused over lunch holds the recent past and reports how much it dropped rather than
|
|
3333
3616
|
taking the tab with it.</p>
|
|
@@ -3371,7 +3654,7 @@ expect(grid.diagnostics.renders().dom.cellWrites - before).toBeLessThan(200);</c
|
|
|
3371
3654
|
single-level header has three because the header is built once per pinned region.</p>
|
|
3372
3655
|
<p><strong>Instrumentation must not change what it measures.</strong> Counters are integers
|
|
3373
3656
|
incremented where the work already happened. Render phases are four <code>performance.now()</code>
|
|
3374
|
-
marks around existing sections. Timings are sampled, a bounded window of recent operations
|
|
3657
|
+
marks around existing sections. Timings are sampled, a bounded window of recent operations,
|
|
3375
3658
|
and every report names which of its figures are sampled, because a number whose provenance is
|
|
3376
3659
|
unclear is worse than no number. Paint wait is the browser's and is deliberately not claimed.</p>
|
|
3377
3660
|
<p><strong>Render causes are captured, not inferred.</strong> By the time a paint runs,
|
|
@@ -3427,8 +3710,8 @@ const providerFor = (id) => ({
|
|
|
3427
3710
|
<p><strong>Positions are row keys, resolved against your view at render time.</strong> Peers
|
|
3428
3711
|
sort and filter independently, so index 12 is a different record on every screen. Publishing
|
|
3429
3712
|
an index would put a colleague's cursor on an unrelated row the moment either of you sorted.
|
|
3430
|
-
The cost of this is real
|
|
3431
|
-
|
|
3713
|
+
The cost of this is real: the grid resolves a key to a position rather than reading one,
|
|
3714
|
+
and it is the difference between the feature working and the feature lying.</p>
|
|
3432
3715
|
<p><strong>Idle is measured from when a message arrived, not from what it says.</strong>
|
|
3433
3716
|
Clocks between clients disagree by seconds routinely and by minutes occasionally. Keying idle
|
|
3434
3717
|
detection on the sender's timestamp means a peer with a fast clock never goes idle and one
|
|
@@ -3454,7 +3737,7 @@ const providerFor = (id) => ({
|
|
|
3454
3737
|
in; only the roster does, because it is a control.</p>
|
|
3455
3738
|
<p><strong>The roster is the part people use.</strong> More than the cursors, in practice. It
|
|
3456
3739
|
carries the name as well as the colour, because colour alone is not a signal everyone can
|
|
3457
|
-
read, and it reports peers whose rows are not in your view rather than omitting them
|
|
3740
|
+
read, and it reports peers whose rows are not in your view rather than omitting them,
|
|
3458
3741
|
an absent peer reads as a disconnection that has not happened.</p>
|
|
3459
3742
|
<p><strong>A parked cursor does not fade.</strong> The label does, after a couple of seconds,
|
|
3460
3743
|
because permanent labels over a dense grid are unreadable. The border stays, dims at idle, and
|
|
@@ -3503,8 +3786,8 @@ const providerFor = (id) => ({
|
|
|
3503
3786
|
the whole comment dataset over a long session to render a few triangles.</p>
|
|
3504
3787
|
<p><strong>Stable row identity is a hard requirement, enforced rather than documented.</strong>
|
|
3505
3788
|
A comment is keyed on row identity plus field. Row index changes under sort, filter and
|
|
3506
|
-
grouping, so a comment keyed on it reattaches to whichever row later occupies that position
|
|
3507
|
-
|
|
3789
|
+
grouping, so a comment keyed on it reattaches to whichever row later occupies that position,
|
|
3790
|
+
and a comment on the wrong row is worse than no comment at all. A grid with no
|
|
3508
3791
|
<code>rowKey</code> disables comments and names them in the same single warning as the other
|
|
3509
3792
|
identity-dependent features.</p>
|
|
3510
3793
|
<p><strong>Identity has to survive more than the session.</strong> Comments outlive the page
|
|
@@ -3549,7 +3832,7 @@ const providerFor = (id) => ({
|
|
|
3549
3832
|
<pre><code>statusBar: { panels: ['rowCount', 'comments'] }</code></pre>
|
|
3550
3833
|
</div>
|
|
3551
3834
|
<p>The count returns zero rather than a number it cannot stand behind: it is only meaningful
|
|
3552
|
-
once the index covers every row, so it stays at zero, and the panel stays silent
|
|
3835
|
+
once the index covers every row, so it stays at zero, and the panel stays silent,
|
|
3553
3836
|
until <code>loadAll()</code> has resolved.</p>
|
|
3554
3837
|
<p><strong>The comments-only filter is refused rather than approximated.</strong> Restricting
|
|
3555
3838
|
the grid to rows carrying comments needs the index to cover the whole row set, not just what
|
|
@@ -3718,8 +4001,8 @@ grid.timeline.detach();</code></pre>
|
|
|
3718
4001
|
it a scrub is nearly unreadable: the grid moves, and on a row twelve columns wide the one
|
|
3719
4002
|
number you are hunting for goes past unseen.</p>
|
|
3720
4003
|
<p><strong>The mark is held, not flashed.</strong> It stays until the next seek clears it.
|
|
3721
|
-
Every other transient signal in the grid fades on a timer, and this one deliberately does not
|
|
3722
|
-
|
|
4004
|
+
Every other transient signal in the grid fades on a timer, and this one deliberately does not,
|
|
4005
|
+
a scrub is someone hunting for what changed, and a highlight they can miss while reading the
|
|
3723
4006
|
other end of the row helps nobody. <code>timeline:seeking</code> fires before any change is
|
|
3724
4007
|
applied, so a five-step drag clears once and marks once rather than strobing per entry.</p>
|
|
3725
4008
|
<p><strong>It compares column values, not raw fields.</strong> A computed column has no field
|
|
@@ -3773,7 +4056,7 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
|
|
|
3773
4056
|
</div>
|
|
3774
4057
|
<div class="why">
|
|
3775
4058
|
<p><strong>The scale multiplies your density, it does not replace it.</strong> A grid built at
|
|
3776
|
-
<code>spacious</code> presented at 1.5x is still recognisably that grid, half as big again
|
|
4059
|
+
<code>spacious</code> presented at 1.5x is still recognisably that grid, half as big again,
|
|
3777
4060
|
which is what makes a presentation look like the product rather than like a different one.
|
|
3778
4061
|
Virtualisation follows the enlargement, so rows are positioned at the size they are drawn.</p>
|
|
3779
4062
|
<p><strong>Full-screen is the maximiser, not a second implementation.</strong> A grid the user
|
|
@@ -4034,7 +4317,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
4034
4317
|
<li><strong>The filter icon is a small target, deliberately.</strong> It is 16 × 16,
|
|
4035
4318
|
below the 24-pixel minimum of WCAG 2.5.8, and conforms under that criterion's
|
|
4036
4319
|
<em>equivalent</em> allowance: filtering is also a column-menu item, and the menu button
|
|
4037
|
-
meets the size on its own. Worth knowing if you are pointing at it on a touch screen
|
|
4320
|
+
meets the size on its own. Worth knowing if you are pointing at it on a touch screen,
|
|
4038
4321
|
the menu is the larger route to the same thing.</li>
|
|
4039
4322
|
</ul>
|
|
4040
4323
|
|
|
@@ -4385,6 +4668,171 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
|
|
|
4385
4668
|
value by value tells a listener less than the sentence does.</p>
|
|
4386
4669
|
</div>
|
|
4387
4670
|
|
|
4671
|
+
<h2 id="statistics">Profiling and statistics</h2>
|
|
4672
|
+
<p class="lead-in">
|
|
4673
|
+
<code>grid.statistics</code> answers questions about the rows the filters left, so every
|
|
4674
|
+
figure describes what the user is looking at rather than the whole table.
|
|
4675
|
+
</p>
|
|
4676
|
+
<div class="example">
|
|
4677
|
+
<p class="example__label">Everything worth knowing about a column, in one pass</p>
|
|
4678
|
+
<pre><code><span class="kw">const</span> p = grid.statistics.profile('capacity');
|
|
4679
|
+
|
|
4680
|
+
p.count; p.missing; p.distinct;
|
|
4681
|
+
p.min; p.q1; p.median; p.q3; p.max;
|
|
4682
|
+
p.mean; p.stdDev;
|
|
4683
|
+
p.outliers; <span class="cmt">// by the interquartile rule</span>
|
|
4684
|
+
p.histogram; <span class="cmt">// bins, ready to draw</span>
|
|
4685
|
+
p.alerts; <span class="cmt">// what is worth looking at</span></code></pre>
|
|
4686
|
+
</div>
|
|
4687
|
+
<p>
|
|
4688
|
+
<code>alerts</code> is the part that saves time: a column that never varies, a key that turns
|
|
4689
|
+
out not to be unique, a fifth of the rows missing. A profile that reports only numbers leaves
|
|
4690
|
+
the reader to notice those, and readers reliably do not.
|
|
4691
|
+
</p>
|
|
4692
|
+
<div class="table-wrap">
|
|
4693
|
+
<table>
|
|
4694
|
+
<thead><tr><th>Method</th><th>Answers</th></tr></thead>
|
|
4695
|
+
<tbody>
|
|
4696
|
+
<tr><td class="name">reduce</td><td class="desc">A column through any of the thirty-eight named kernels, or one of your own.</td></tr>
|
|
4697
|
+
<tr><td class="name">correlation</td><td class="desc">Pearson's r between two columns. <code>spearman</code> resists an outlier; <code>kendall</code> is tau-b.</td></tr>
|
|
4698
|
+
<tr><td class="name">regression</td><td class="desc">A least-squares fit of one column on another, with slope, intercept and r².</td></tr>
|
|
4699
|
+
<tr><td class="name">weightedAverage</td><td class="desc">One column averaged by another. <code>weightedQuantile</code> for the median and beyond.</td></tr>
|
|
4700
|
+
<tr><td class="name">series</td><td class="desc">How a column varies along an ordering. <code>by</code> is required and never guessed.</td></tr>
|
|
4701
|
+
<tr><td class="name">shadow</td><td class="desc">What the grid knows about a row over time: updates, delta, rate, rank, percentile, streak.</td></tr>
|
|
4702
|
+
</tbody>
|
|
4703
|
+
</table>
|
|
4704
|
+
</div>
|
|
4705
|
+
<div class="why">
|
|
4706
|
+
<p><strong>The filters are part of the question.</strong> Every one of these reads the
|
|
4707
|
+
filtered rows. Narrow the grid and the statistics narrow with it, which is the behaviour you
|
|
4708
|
+
want when the filter <em>is</em> the analysis.</p>
|
|
4709
|
+
<p><strong>Kernels see arrival order, not display order.</strong> Anything order-dependent
|
|
4710
|
+
takes an explicit <code>by</code> rather than inferring one from the current sort, so the
|
|
4711
|
+
answer does not change when a user clicks a column header.</p>
|
|
4712
|
+
</div>
|
|
4713
|
+
|
|
4714
|
+
<h2 id="spc">Process control and capability</h2>
|
|
4715
|
+
<p class="lead-in">
|
|
4716
|
+
Whether a process sits inside the tolerance it was given, and whether it is behaving or
|
|
4717
|
+
drifting. The tolerance is declared once, on the column.
|
|
4718
|
+
</p>
|
|
4719
|
+
<div class="example">
|
|
4720
|
+
<p class="example__label">The specification lives with the column</p>
|
|
4721
|
+
<pre><code>{ id: 'diameter', type: 'number', spec: { lower: 9.95, upper: 10.05, target: 10 } }</code></pre>
|
|
4722
|
+
</div>
|
|
4723
|
+
<div class="example">
|
|
4724
|
+
<p class="example__label">Asking for the capability</p>
|
|
4725
|
+
<pre><code><span class="kw">const</span> c = grid.statistics.capability('diameter', { rules: 'nelson' });
|
|
4726
|
+
|
|
4727
|
+
c.cp; c.cpk; <span class="cmt">// short-term spread, from the moving range</span>
|
|
4728
|
+
c.pp; c.ppk; <span class="cmt">// overall spread</span>
|
|
4729
|
+
c.outOfSpec; <span class="cmt">// parts outside the customer's tolerance</span>
|
|
4730
|
+
c.limits; <span class="cmt">// { centre, upper, lower, sigma }</span>
|
|
4731
|
+
c.violations; <span class="cmt">// [{ index, rule, description }, …]</span>
|
|
4732
|
+
c.interval; <span class="cmt">// a confidence interval for cpk</span></code></pre>
|
|
4733
|
+
</div>
|
|
4734
|
+
<div class="why">
|
|
4735
|
+
<p><strong>One declared tolerance, so nothing can disagree.</strong> The indices, the charts
|
|
4736
|
+
and any conditional format all read the same <code>spec</code>. A tolerance passed separately
|
|
4737
|
+
to each is a tolerance that eventually differs between them, and a capability report that
|
|
4738
|
+
contradicts the cell colouring is worse than neither.</p>
|
|
4739
|
+
<p><strong>Cp and Cpk use short-term variation, Pp and Ppk overall.</strong> The first pair
|
|
4740
|
+
comes from the moving range, which is what the process can do when it is behaving; the second
|
|
4741
|
+
from the whole spread, which is what it actually delivered. Ppk well below Cpk is the signal
|
|
4742
|
+
that the process drifted rather than that it is incapable.</p>
|
|
4743
|
+
<p><strong>A baseline finds a shift instead of absorbing it.</strong> <code>baseline: 30</code>
|
|
4744
|
+
fixes the limits over the first thirty readings. Limits recomputed over all the data widen to
|
|
4745
|
+
accommodate the very shift you are looking for, and then report no violation.</p>
|
|
4746
|
+
<p><strong>The point estimate alone overstates the case.</strong> A Cpk of 1.35 measured on
|
|
4747
|
+
thirty parts has a lower bound below 1.0, so a process that has "passed" a 1.33 requirement on
|
|
4748
|
+
thirty parts has demonstrated very little. <code>interval</code> is reported alongside it for
|
|
4749
|
+
that reason.</p>
|
|
4750
|
+
</div>
|
|
4751
|
+
|
|
4752
|
+
<h3>Drawing it</h3>
|
|
4753
|
+
<p class="lead-in">
|
|
4754
|
+
Three chart types complete the picture, and they read the same specification.
|
|
4755
|
+
</p>
|
|
4756
|
+
<div class="table-wrap">
|
|
4757
|
+
<table>
|
|
4758
|
+
<thead><tr><th>Type</th><th>Shows</th></tr></thead>
|
|
4759
|
+
<tbody>
|
|
4760
|
+
<tr><td class="name">control</td><td class="desc">Readings against the centre line and control limits, with every rule break numbered.</td></tr>
|
|
4761
|
+
<tr><td class="name">movingRange</td><td class="desc">The companion chart: variation between consecutive readings.</td></tr>
|
|
4762
|
+
<tr><td class="name">capability</td><td class="desc">The distribution against the tolerance, with a curve for each of the two spreads.</td></tr>
|
|
4763
|
+
</tbody>
|
|
4764
|
+
</table>
|
|
4765
|
+
</div>
|
|
4766
|
+
<p>
|
|
4767
|
+
Rule breaks are numbered rather than merely marked, under Western Electric's four rules or
|
|
4768
|
+
Nelson's eight. The two sets number differently, so the chart names which it applied: a
|
|
4769
|
+
"rule 3" that could mean either is not a finding anyone can act on.
|
|
4770
|
+
</p>
|
|
4771
|
+
|
|
4772
|
+
<h2 id="intervals">Confidence intervals</h2>
|
|
4773
|
+
<p class="lead-in">
|
|
4774
|
+
How firmly the data pins a figure down. An interval narrows as the grid does, because it
|
|
4775
|
+
describes the filtered rows and not the whole table.
|
|
4776
|
+
</p>
|
|
4777
|
+
<div class="example">
|
|
4778
|
+
<p class="example__label">A mean and a rate</p>
|
|
4779
|
+
<pre><code>grid.statistics.interval('capacity');
|
|
4780
|
+
<span class="cmt">// { lower, upper, mean, n, confidence }, by Student's t</span>
|
|
4781
|
+
|
|
4782
|
+
grid.statistics.interval('status', {
|
|
4783
|
+
kind: 'proportion',
|
|
4784
|
+
where: (v) => v === 'failed',
|
|
4785
|
+
});
|
|
4786
|
+
<span class="cmt">// Wilson score, which stays sensible at small n and near 0 or 1</span></code></pre>
|
|
4787
|
+
</div>
|
|
4788
|
+
<p>
|
|
4789
|
+
Intervals are also available on a regression slope and on a capability index. Each uses the
|
|
4790
|
+
method that suits it: Student's <em>t</em> for a mean, the Wilson score for a proportion, and
|
|
4791
|
+
Bissell's approximation for Cpk.
|
|
4792
|
+
</p>
|
|
4793
|
+
<div class="why">
|
|
4794
|
+
<p><strong>The line the product draws.</strong> Lattice quantifies uncertainty. It does not
|
|
4795
|
+
adjudicate hypotheses: there are no p-values and no significance tests. An interval says how
|
|
4796
|
+
precisely a figure is known and leaves the judgement where it belongs. A tool that returns a
|
|
4797
|
+
verdict invites it to be read as one, and a grid is the wrong place for that.</p>
|
|
4798
|
+
<p><strong>Wilson, not the textbook formula.</strong> The normal approximation gives bounds
|
|
4799
|
+
below zero and above one at small counts, which is visibly wrong to anyone who reads it. The
|
|
4800
|
+
Wilson score stays inside the interval it is describing.</p>
|
|
4801
|
+
</div>
|
|
4802
|
+
|
|
4803
|
+
<h2 id="stat-tiles">Statistic tiles</h2>
|
|
4804
|
+
<p class="lead-in">
|
|
4805
|
+
A headline figure over a grid, with the change since a baseline, a tone from thresholds, and
|
|
4806
|
+
the interval underneath.
|
|
4807
|
+
</p>
|
|
4808
|
+
<div class="example">
|
|
4809
|
+
<p class="example__label">A tile that follows the grid</p>
|
|
4810
|
+
<pre><code><span class="kw">import</span> { createStat } <span class="kw">from</span> 'lattice-grid';
|
|
4811
|
+
|
|
4812
|
+
createStat({
|
|
4813
|
+
grid,
|
|
4814
|
+
container: tile,
|
|
4815
|
+
title: 'Total capacity',
|
|
4816
|
+
value: { of: 'capacity', fn: 'sum' },
|
|
4817
|
+
baseline: (g) => lastMonth,
|
|
4818
|
+
bands: { good: 5000, warn: 3000, direction: 'up' },
|
|
4819
|
+
interval: (v, g) => g.statistics.interval('capacity'),
|
|
4820
|
+
});</code></pre>
|
|
4821
|
+
</div>
|
|
4822
|
+
<div class="why">
|
|
4823
|
+
<p><strong><code>bands</code> and <code>goodWhen</code> judge different things.</strong>
|
|
4824
|
+
<code>goodWhen</code> says whether a rise is good news, and colours the change indicator.
|
|
4825
|
+
<code>bands</code> judge the value itself. They are separate because a Cpk of 0.9 is bad news
|
|
4826
|
+
whether it rose or fell to get there.</p>
|
|
4827
|
+
<p><strong>The tile follows the grid by default.</strong> Filter the grid and the figure
|
|
4828
|
+
updates. <code>scope</code> chooses filtered, all or selected rows; <code>live: false</code>
|
|
4829
|
+
detaches it and leaves <code>refresh()</code> to you.</p>
|
|
4830
|
+
<p><code>value</code> also takes <code>show</code>, which reports a field from the row holding
|
|
4831
|
+
an extreme rather than the extreme itself: <code>{ of: 'sales', fn: 'max', show: 'rep' }</code>
|
|
4832
|
+
is the <em>name</em> of the best rep. It needs <code>min</code> or <code>max</code>, because no
|
|
4833
|
+
single row holds an average.</p>
|
|
4834
|
+
</div>
|
|
4835
|
+
|
|
4388
4836
|
<h2 id="formulas-guide">Formulas</h2>
|
|
4389
4837
|
<p class="lead-in">
|
|
4390
4838
|
A leading <code>=</code> in a numeric cell is a formula. Type
|
|
@@ -4658,7 +5106,7 @@ grid.highlight.clear();</code></pre>
|
|
|
4658
5106
|
<div class="why">
|
|
4659
5107
|
<p><strong>Applying a view is a destination, not a patch.</strong> A view's state names only
|
|
4660
5108
|
the sections it cares about, so applying one resets to the grid's starting state first. Without
|
|
4661
|
-
that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns
|
|
5109
|
+
that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns,
|
|
4662
5110
|
the same view giving a different grid depending on what preceded it, which is the one thing a
|
|
4663
5111
|
named view must not do.</p>
|
|
4664
5112
|
</div>
|
|
@@ -4682,7 +5130,7 @@ grid.highlight.clear();</code></pre>
|
|
|
4682
5130
|
</div>
|
|
4683
5131
|
|
|
4684
5132
|
<div class="why">
|
|
4685
|
-
<p><strong>Applying a view never throws and never refuses.</strong> It returns a report
|
|
5133
|
+
<p><strong>Applying a view never throws and never refuses.</strong> It returns a report,
|
|
4686
5134
|
<code>{ applied, skipped }</code>: naming each thing it could not use and why. Refusing the
|
|
4687
5135
|
whole view because one column has gone would lose a layout the user built deliberately, and
|
|
4688
5136
|
throwing during a page load would lose the page. So a view degrades to as much of itself as
|
|
@@ -4737,7 +5185,7 @@ grid.on('view:default', e => api.patch(`/views/${e.view.id}`, { isDefault: tr
|
|
|
4737
5185
|
grid has one to plug in, and a picker offering "Save" that quietly does nothing until a backend
|
|
4738
5186
|
exists is worse than not offering it. <code>views.local: true</code> is the no-backend answer:
|
|
4739
5187
|
saved views live in this browser's own <code>localStorage</code>, under a default key shared by
|
|
4740
|
-
every grid on the origin unless you pass one of your own
|
|
5188
|
+
every grid on the origin unless you pass one of your own,
|
|
4741
5189
|
<code>views: { local: { key: 'orders-grid-views' } }</code>: to keep two grids' views apart.
|
|
4742
5190
|
Given alongside an explicit <code>storage</code>, the explicit adapter always wins and
|
|
4743
5191
|
<code>local</code> is silently (well, not silently: it warns once) ignored, so a page cannot
|
|
@@ -4779,7 +5227,7 @@ grid.history.list(); <span class="cmt">// the timeline, newest first</sp
|
|
|
4779
5227
|
|
|
4780
5228
|
<h2 id="permissions-guide">Column permissions</h2>
|
|
4781
5229
|
<p class="lead-in">
|
|
4782
|
-
Four levels, resolved per column from configuration or a callback. They are not a ladder
|
|
5230
|
+
Four levels, resolved per column from configuration or a callback. They are not a ladder,
|
|
4783
5231
|
reading and writing are independent, so they are the four corners of a 2×2.
|
|
4784
5232
|
</p>
|
|
4785
5233
|
<div class="table-wrap">
|
|
@@ -4859,8 +5307,8 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
|
|
|
4859
5307
|
is shown and the grid is the one you already had.</p>
|
|
4860
5308
|
<p><strong>Neither sorts or filters it among the live rows.</strong> A removed row's values
|
|
4861
5309
|
are yesterday's; ordering them among today's presents two data sets as one, and lets a filter
|
|
4862
|
-
written for current values decide the fate of historical ones. Neither permits an edit either
|
|
4863
|
-
|
|
5310
|
+
written for current values decide the fate of historical ones. Neither permits an edit either,
|
|
5311
|
+
a write aimed at a removed row is refused and returns <code>0</code>, rather than being
|
|
4864
5312
|
counted as applied against a record that is not there.</p>
|
|
4865
5313
|
</div>
|
|
4866
5314
|
|
|
@@ -4884,12 +5332,12 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
|
|
|
4884
5332
|
<p class="lead-in">
|
|
4885
5333
|
That mounts a prompt bar. The user types "EMEA circuits over 500 gigs, biggest first"; the
|
|
4886
5334
|
grid composes a message including its schema; your callback returns the model's reply; the
|
|
4887
|
-
grid validates it and shows what it would do in plain English
|
|
4888
|
-
<em>"Filter Region is EMEA and Capacity is more than 500, sort Capacity descending"</em
|
|
5335
|
+
grid validates it and shows what it would do in plain English,
|
|
5336
|
+
<em>"Filter Region is EMEA and Capacity is more than 500, sort Capacity descending"</em>,
|
|
4889
5337
|
with Apply and Discard.
|
|
4890
5338
|
</p>
|
|
4891
5339
|
<div class="why">
|
|
4892
|
-
<p><strong>Nothing is executed on trust.</strong> The vocabulary is seven actions
|
|
5340
|
+
<p><strong>Nothing is executed on trust.</strong> The vocabulary is seven actions,
|
|
4893
5341
|
<code>setFilters</code>, <code>setSort</code>, <code>groupBy</code>,
|
|
4894
5342
|
<code>showColumns</code>, <code>hideColumns</code>, <code>setQuick</code>,
|
|
4895
5343
|
<code>clear</code>, and a reply naming a column that does not exist is rejected with a
|
|
@@ -5127,7 +5575,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
|
|
|
5127
5575
|
<footer>
|
|
5128
5576
|
<p>
|
|
5129
5577
|
Lattice Grid 1.7.1 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
5130
|
-
Written against the shipped source. Where this guide and the code disagree, the code wins
|
|
5578
|
+
Written against the shipped source. Where this guide and the code disagree, the code wins,
|
|
5131
5579
|
please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
5132
5580
|
</p>
|
|
5133
5581
|
</footer>
|