@toclocoinc/lattice-grid 1.10.0 → 1.11.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 +135 -5
- package/lattice-grid.d.ts +50 -1
- package/lattice-grid.esm.min.js +460 -17
- package/lattice-grid.min.cjs +460 -17
- package/lattice-grid.min.js +460 -17
- package/modules/charts.esm.min.js +1 -1
- package/modules/devtools.esm.min.js +1 -1
- package/modules/dhtmlx-compat.esm.min.js +460 -17
- package/modules/htmx.esm.min.js +460 -17
- package/modules/htmx.min.cjs +460 -17
- package/modules/htmx.min.js +460 -17
- 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 +460 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
dependencies, no build step required. Optional adapters for React, Vue, Svelte
|
|
5
5
|
and Web Components ship alongside it.
|
|
6
6
|
|
|
7
|
-
Version 1.
|
|
7
|
+
Version 1.11.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/docs/API.html
CHANGED
|
@@ -2138,6 +2138,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'tria
|
|
|
2138
2138
|
<tr><td class="name">from</td><td class="type">Grid</td><td class="desc">Required. The grid to read.</td></tr>
|
|
2139
2139
|
<tr><td class="name">follow</td><td class="type">'filtered' | 'all' | 'selected' | 'grouped'</td><td class="desc">Which of its rows to read. <code>filtered</code> by default. <code>grouped</code> re-aggregates by whatever dimension the user has grouped the source by, so a panel tracks the reader rather than a dimension fixed when the page was built; with the source ungrouped it falls back to <code>groupBy</code>.</td></tr>
|
|
2140
2140
|
<tr><td class="name">unnest</td><td class="type">string</td><td class="desc">Expand an array property, one row per element, keeping the parent's fields. Address the element with a dotted path afterwards — <code>lines.sku</code> is the element, <code>region</code> is still the parent. A row whose property is absent or empty contributes nothing.</td></tr>
|
|
2141
|
+
<tr><td class="name">join</td><td class="type">{ with, on, type, select, prefix, follow }</td><td class="desc">Match each row against a second grid on a shared key and bring some of its fields across. Runs after <code>unnest</code> and before <code>where</code>, so a condition — and a grouping, and a total — can read a field the join produced.</td></tr>
|
|
2141
2142
|
<tr><td class="name">where</td><td class="type">(row) => boolean</td><td class="desc">A row predicate, applied before grouping. With no <code>groupBy</code> the rows pass through as themselves, which is how an exceptions list is built.</td></tr>
|
|
2142
2143
|
<tr><td class="name">bucket</td><td class="type">{ of, by }</td><td class="desc">Round a date column down to the start of its period and group on that. <code>by</code> is <code>day</code>, <code>week</code>, <code>month</code>, <code>quarter</code> or <code>year</code>; weeks start on the ISO Monday.</td></tr>
|
|
2143
2144
|
<tr><td class="name">groupBy</td><td class="type">string | string[]</td><td class="desc">The dimension, or dimensions, to group by. Omit to pass rows through.</td></tr>
|
|
@@ -2148,6 +2149,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'tria
|
|
|
2148
2149
|
<tr><td class="name">cumulative</td><td class="type">{ of, upTo }</td><td class="desc">Keep rows until their running share of the total reaches <code>upTo</code>, 0 to 1. The Pareto question. The row that crosses the cutoff is kept, because the set has to reach the share.</td></tr>
|
|
2149
2150
|
<tr><td class="name">profile</td><td class="type">string | string[]</td><td class="desc">Replaces the pipeline with a transpose: one row per column, with count, present, missing, distinct, min, max, mean, median, quartiles, deviation and outlier count as its columns.</td></tr>
|
|
2150
2151
|
<tr><td class="name">orient</td><td class="type">'columns' | 'metrics'</td><td class="desc">With <code>profile</code>, emit one row per statistic instead of one per column — the shape a dashboard tile wants.</td></tr>
|
|
2152
|
+
<tr><td class="name">crossFilter</td><td class="type">boolean | string</td><td class="desc">Let this grid filter the grid it derives from. <code>true</code> cross-filters through whatever it groups by; a string names a different source column.</td></tr>
|
|
2151
2153
|
<tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">When to re-derive. <code>idle</code> by default, coalescing to a frame, because a hundred cell updates in one frame are one derivation. A number debounces by that many milliseconds.</td></tr>
|
|
2152
2154
|
</tbody>
|
|
2153
2155
|
</table>
|
|
@@ -2168,14 +2170,142 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'tria
|
|
|
2168
2170
|
the values under it changing.
|
|
2169
2171
|
</p>
|
|
2170
2172
|
<p class="section-note">
|
|
2171
|
-
<strong>Cost.</strong>
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2173
|
+
<strong>Cost.</strong> The first derivation is linear in the rows read and largely independent
|
|
2174
|
+
of what is reduced — roughly 900 ms per 200,000 rows grouped into forty, whether the selection is one sum
|
|
2175
|
+
or four statistics. After that, a change that names the rows it touched is <em>patched</em>
|
|
2176
|
+
rather than re-derived: only the groups those rows entered or left are reduced again, so a live
|
|
2177
|
+
feed costs time proportional to what changed rather than to the table. Five hundred updates
|
|
2178
|
+
against that same source take under 300 ms in total, not 300 ms each. A joined
|
|
2179
|
+
derivation is maintained the same way from both sides: the lookup is held between derivations
|
|
2180
|
+
rather than rebuilt, a change to the fact table rejoins only the rows that moved, and a change
|
|
2181
|
+
to the lookup rejoins only the rows behind the keys whose match actually changed — about
|
|
2182
|
+
2 ms per fact update and 1.5 ms per lookup edit against a 200,000-row source joined to
|
|
2183
|
+
2,000 customers, where every one of them previously cost a full derivation of roughly a second.
|
|
2184
|
+
A change that cannot be reasoned about that way — a new filter, a regrouping, a
|
|
2185
|
+
derivation using <code>unnest</code> or <code>where</code>, or a lookup row arriving for
|
|
2186
|
+
rows an inner join had dropped — falls back to a full derivation, which is correct but
|
|
2187
|
+
costs the full linear pass. Narrow with
|
|
2175
2188
|
<code>follow: 'filtered'</code> so the derivation reads what the user is looking at rather than
|
|
2176
2189
|
the whole table.
|
|
2177
2190
|
</p>
|
|
2178
2191
|
|
|
2192
|
+
<h3 id="change-contract">What a change firing promises</h3>
|
|
2193
|
+
<p class="section-note">
|
|
2194
|
+
Anything maintaining state from <code>rows:changed</code> — a derived grid, a chart, your
|
|
2195
|
+
own cache — needs to know whether a firing names the rows that moved or merely says that
|
|
2196
|
+
something did. One <code>rows.apply</code> announces itself more than once: the source reports
|
|
2197
|
+
how many rows moved, the row model reports which ones, and the grid reports that a change
|
|
2198
|
+
happened. Acting on all three does the work three times over.
|
|
2199
|
+
</p>
|
|
2200
|
+
<div class="table-wrap">
|
|
2201
|
+
<table>
|
|
2202
|
+
<thead><tr><th>Field</th><th>Meaning</th></tr></thead>
|
|
2203
|
+
<tbody>
|
|
2204
|
+
<tr><td class="name">identified: true</td><td class="desc"><code>added</code>, <code>updated</code> and <code>removed</code> are arrays naming exactly the rows that moved. Safe to patch from.</td></tr>
|
|
2205
|
+
<tr><td class="name">companion: true</td><td class="desc">A second announcement of a change already reported with identity, or one made before the grid's own view caught up. Ignore it.</td></tr>
|
|
2206
|
+
<tr><td class="name">neither</td><td class="desc">A real change whose extent cannot be named — rows replaced wholesale, or a row moved, where what changed is the order. Re-read.</td></tr>
|
|
2207
|
+
</tbody>
|
|
2208
|
+
</table>
|
|
2209
|
+
</div>
|
|
2210
|
+
<p class="section-note">
|
|
2211
|
+
<strong>The default is the safe one.</strong> A firing that says nothing is treated as a change
|
|
2212
|
+
of unknown extent, so an emitter added later that forgets the flag costs a rescan rather than
|
|
2213
|
+
silently leaving a total stale. This was previously inferred from the payload's <em>shape</em>
|
|
2214
|
+
— arrays meant identity, numbers meant a duplicate — which was wrong for exactly one
|
|
2215
|
+
case: a row <em>move</em>, which carries counts because nothing changed value. Moves were
|
|
2216
|
+
dropped, and every order-dependent derivation below kept its old answer indefinitely.
|
|
2217
|
+
</p>
|
|
2218
|
+
|
|
2219
|
+
<h3 id="derived-join">Joining two grids</h3>
|
|
2220
|
+
<p class="section-note">
|
|
2221
|
+
Two grids each holding their own data, and a third showing where they meet. Orders against
|
|
2222
|
+
customers; shipments against carriers; enrolments against students. The third grid derives from
|
|
2223
|
+
one side and names the other as its <code>join</code> partner.
|
|
2224
|
+
</p>
|
|
2225
|
+
<pre><code>const joined = createGrid(host, {
|
|
2226
|
+
source: {
|
|
2227
|
+
mode: 'derived',
|
|
2228
|
+
from: orders,
|
|
2229
|
+
join: {
|
|
2230
|
+
with: customers,
|
|
2231
|
+
on: { left: 'customerId', right: 'id' },
|
|
2232
|
+
select: ['name', 'tier'],
|
|
2233
|
+
},
|
|
2234
|
+
},
|
|
2235
|
+
columns: [{ field: 'ref' }, { field: 'name' }, { field: 'tier' }, { field: 'amount' }],
|
|
2236
|
+
});</code></pre>
|
|
2237
|
+
<div class="table-wrap">
|
|
2238
|
+
<table>
|
|
2239
|
+
<thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
|
|
2240
|
+
<tbody>
|
|
2241
|
+
<tr><td class="name">with</td><td class="type">Grid</td><td class="desc">Required. The grid holding the other side.</td></tr>
|
|
2242
|
+
<tr><td class="name">on</td><td class="type">string | { left, right }</td><td class="desc">Required. The shared key: one field name when both sides use it, or one each.</td></tr>
|
|
2243
|
+
<tr><td class="name">type</td><td class="type">'inner' | 'left'</td><td class="desc"><code>inner</code> by default, keeping only rows that matched — which is usually what “common data” means. <code>left</code> keeps every row and leaves the brought-across fields undefined, the shape you want when the unmatched rows <em>are</em> the finding.</td></tr>
|
|
2244
|
+
<tr><td class="name">select</td><td class="type">string[]</td><td class="desc">Which of the partner's fields to bring across. All of them by default.</td></tr>
|
|
2245
|
+
<tr><td class="name">prefix</td><td class="type">string</td><td class="desc">Rename the brought-across fields, for when both sides have a <code>name</code> worth keeping.</td></tr>
|
|
2246
|
+
<tr><td class="name">follow</td><td class="type">'all' | 'filtered'</td><td class="desc">Which of the partner's rows to read. <code>all</code> by default: a lookup table is normally the whole table, and a customer list filtered to Europe would otherwise silently drop every other order from a grid the reader takes to be all orders.</td></tr>
|
|
2247
|
+
</tbody>
|
|
2248
|
+
</table>
|
|
2249
|
+
</div>
|
|
2250
|
+
<p class="section-note">
|
|
2251
|
+
<strong>The row count does not change.</strong> A key appearing twice on the right keeps the
|
|
2252
|
+
first match rather than emitting a row per pair. SQL would multiply them out; here that would
|
|
2253
|
+
change the row count of a grid the reader thinks of as “the orders” and quietly
|
|
2254
|
+
double every total taken from it.
|
|
2255
|
+
</p>
|
|
2256
|
+
<p class="section-note">
|
|
2257
|
+
<strong>Both sides are live.</strong> The partner is read at derivation time, not captured when
|
|
2258
|
+
the grid was built, and editing it re-derives — a corrected tier in the customer grid
|
|
2259
|
+
moves the order into a different band in the joined one.
|
|
2260
|
+
</p>
|
|
2261
|
+
|
|
2262
|
+
<h3 id="cross-filter">Cross-filtering: the path back up</h3>
|
|
2263
|
+
<p class="section-note">
|
|
2264
|
+
Derivation runs one way. A derived grid reads its source and never writes to it, which is what
|
|
2265
|
+
makes a chain of them safe to reason about. Cross-filtering is the single deliberate path back
|
|
2266
|
+
up: clicking a row in a summary panel filters the grid it summarises.
|
|
2267
|
+
</p>
|
|
2268
|
+
<pre><code>const byRep = createGrid(panel, {
|
|
2269
|
+
source: {
|
|
2270
|
+
mode: 'derived', from: main, groupBy: 'rep', refresh: 'live',
|
|
2271
|
+
crossFilter: true,
|
|
2272
|
+
select: { total: { of: 'amount', fn: 'sum' } },
|
|
2273
|
+
},
|
|
2274
|
+
columns: [{ field: 'rep' }, { field: 'total' }],
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
byRep.on('row:click', (e) => byRep.crossFilter.toggle(e.key));</code></pre>
|
|
2278
|
+
<div class="table-wrap">
|
|
2279
|
+
<table>
|
|
2280
|
+
<thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
|
|
2281
|
+
<tbody>
|
|
2282
|
+
<tr><td class="sig">enabled()</td><td class="type">boolean</td><td class="desc">Whether this grid can cross-filter a source. False on a grid that is not derived, or whose source has no <code>crossFilter</code>.</td></tr>
|
|
2283
|
+
<tr><td class="sig">column()</td><td class="type">string | null</td><td class="desc">The source column the filter is pushed onto.</td></tr>
|
|
2284
|
+
<tr><td class="sig">get()</td><td class="type">string[]</td><td class="desc">The keys currently filtering the source.</td></tr>
|
|
2285
|
+
<tr><td class="sig">set(keys)</td><td class="type">void</td><td class="desc">Filter the source to these derived rows. <code>null</code> clears.</td></tr>
|
|
2286
|
+
<tr><td class="sig">toggle(key)</td><td class="type">void</td><td class="desc">Add or remove one key — what a click handler wants.</td></tr>
|
|
2287
|
+
<tr><td class="sig">clear()</td><td class="type">void</td><td class="desc">Take this grid's filter off its source.</td></tr>
|
|
2288
|
+
</tbody>
|
|
2289
|
+
</table>
|
|
2290
|
+
</div>
|
|
2291
|
+
<p class="section-note">
|
|
2292
|
+
<strong>A panel does not filter itself.</strong> The grid pushing the filter leaves its own
|
|
2293
|
+
condition out when it reads the source back. Without that, clicking one rep would collapse the
|
|
2294
|
+
panel to that single row and strand the reader with nothing else to click. It is the same rule
|
|
2295
|
+
that keeps a header histogram showing every bar after you click one (<a href="#facets">facets</a>),
|
|
2296
|
+
applied between grids instead of within one.
|
|
2297
|
+
</p>
|
|
2298
|
+
<p class="section-note">
|
|
2299
|
+
<strong>Several panels compose.</strong> Each leaves out only its own condition, so two panels
|
|
2300
|
+
over different columns narrow each other while both stay whole — pick a rep and the region
|
|
2301
|
+
panel shows that rep's regions, pick a region and the rep panel shows that region's reps.
|
|
2302
|
+
</p>
|
|
2303
|
+
<p class="section-note">
|
|
2304
|
+
<strong>It is an ordinary filter.</strong> The condition goes through the source's
|
|
2305
|
+
<code>filters.set</code>, so it undoes, rides in a saved view, and appears in whatever filter UI
|
|
2306
|
+
the grid already has. There is no second filter model beside the real one.
|
|
2307
|
+
</p>
|
|
2308
|
+
|
|
2179
2309
|
<h3>The remote request</h3>
|
|
2180
2310
|
<p class="section-note">Your <code>fetch</code> receives one object and returns <code>{ rows, total }</code>.</p>
|
|
2181
2311
|
<div class="table-wrap">
|
|
@@ -2220,7 +2350,7 @@ off(); <span class="cmt">// on() returns i
|
|
|
2220
2350
|
<tr><td class="name">render:first</td><td class="type">{}</td><td class="desc">First paint — the number to measure time-to-first-row against.</td></tr>
|
|
2221
2351
|
<tr><td class="name">destroy</td><td class="type">{}</td><td class="desc"><code>grid.destroy()</code> has run.</td></tr>
|
|
2222
2352
|
<tr><td class="name">model:changed</td><td class="type">{ reason }</td><td class="desc">Columns, grouping, pivot or another structural change.</td></tr>
|
|
2223
|
-
<tr><td class="name">rows:changed</td><td class="type">{ added, updated, removed, plan }</td><td class="desc">The
|
|
2353
|
+
<tr><td class="name">rows:changed</td><td class="type">{ identified?, companion?, added, updated, removed, plan }</td><td class="desc">The row set changed. See <a href="#change-contract">what a change firing promises</a>: <code>identified</code> means the three arrays name the rows that moved, <code>companion</code> marks a duplicate announcement of a change already made with identity, and a firing with neither is a real change of unknown extent.</td></tr>
|
|
2224
2354
|
<tr><td class="name">rows:queued</td><td class="type">{ pending }</td><td class="desc">A batched change is waiting for the next frame.</td></tr>
|
|
2225
2355
|
<tr><td class="name">cell:changed</td><td class="type">{ row, key, colId, value, oldValue, undo }</td><td class="desc">A committed edit reached the data. <code>undo</code> distinguishes a rollback.</td></tr>
|
|
2226
2356
|
<tr><td class="name">cell:pending</td><td class="type">{ row, key, colId, value, before, id }</td><td class="desc">Applied optimistically, not yet durable. Only with <code>edit.commit</code>.</td></tr>
|
package/lattice-grid.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Lattice Grid 1.
|
|
2
|
+
* Lattice Grid 1.11.0 — type declarations
|
|
3
3
|
* Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
4
4
|
* https://latticegrid.dev
|
|
5
5
|
*/
|
|
@@ -784,6 +784,12 @@ export interface DerivedSourceConfig {
|
|
|
784
784
|
|
|
785
785
|
/** An array property to expand, one row per element, before anything else. */
|
|
786
786
|
unnest?: string;
|
|
787
|
+
/**
|
|
788
|
+
* Match each row against a second grid on a shared key, and bring some of its
|
|
789
|
+
* fields across. Runs after `unnest` and before `where`, so a condition — and
|
|
790
|
+
* a grouping, and a total — can read a field the join produced.
|
|
791
|
+
*/
|
|
792
|
+
join?: DerivedJoin;
|
|
787
793
|
/** A row predicate, applied before grouping. */
|
|
788
794
|
where?: (row: unknown) => boolean;
|
|
789
795
|
/** Round a date column down to a period, and group on that. */
|
|
@@ -808,6 +814,42 @@ export interface DerivedSourceConfig {
|
|
|
808
814
|
|
|
809
815
|
/** When to re-derive. `idle` by default — coalesced to a frame. */
|
|
810
816
|
refresh?: 'live' | 'idle' | 'manual' | number;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Let this grid filter the grid it derives from. `true` cross-filters through
|
|
820
|
+
* whatever it groups by; a string names a different source column.
|
|
821
|
+
*/
|
|
822
|
+
crossFilter?: boolean | string | { col?: string };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export interface DerivedJoin {
|
|
826
|
+
/** The grid holding the other side. */
|
|
827
|
+
with: Grid;
|
|
828
|
+
/** The shared key: one field name when both sides use it, or one each. */
|
|
829
|
+
on: string | { left?: string; right?: string };
|
|
830
|
+
/** `inner` keeps only rows that matched; `left` keeps them all. */
|
|
831
|
+
type?: 'inner' | 'left';
|
|
832
|
+
/** Which of the partner's fields to bring across. All of them by default. */
|
|
833
|
+
select?: string[];
|
|
834
|
+
/** Rename the brought-across fields, when both sides have one worth keeping. */
|
|
835
|
+
prefix?: string;
|
|
836
|
+
/** Which of the partner's rows to read. `all` by default. */
|
|
837
|
+
follow?: 'all' | 'filtered';
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export interface CrossFilter {
|
|
841
|
+
/** Whether this grid can cross-filter a source. */
|
|
842
|
+
enabled(): boolean;
|
|
843
|
+
/** The source column the filter is pushed onto. */
|
|
844
|
+
column(): string | null;
|
|
845
|
+
/** The keys currently filtering the source. */
|
|
846
|
+
get(): string[];
|
|
847
|
+
/** Filter the source to these derived rows. */
|
|
848
|
+
set(keys: string | string[] | null): void;
|
|
849
|
+
/** Add or remove one key, for click-to-filter. */
|
|
850
|
+
toggle(key: string): void;
|
|
851
|
+
/** Take this grid's filter off its source. */
|
|
852
|
+
clear(): void;
|
|
811
853
|
}
|
|
812
854
|
|
|
813
855
|
export type SourceConfig =
|
|
@@ -1878,6 +1920,11 @@ export interface RowsApi {
|
|
|
1878
1920
|
forEach(fn: (row: Row, index: number) => void): void;
|
|
1879
1921
|
/** Every row in the data, before any filter. Leaf rows, in physical order. */
|
|
1880
1922
|
forEachAll(fn: (row: Row, index: number) => void): void;
|
|
1923
|
+
/**
|
|
1924
|
+
* Visit the rows surviving every filter except one column's own — the
|
|
1925
|
+
* faceting question, asked of the rows.
|
|
1926
|
+
*/
|
|
1927
|
+
forEachExcept(colId: string, fn: (row: Row, index: number) => void): void;
|
|
1881
1928
|
value(key: string, colId: string): unknown;
|
|
1882
1929
|
text(key: string, colId: string): string;
|
|
1883
1930
|
values(key: string): Record<string, unknown>;
|
|
@@ -2775,6 +2822,8 @@ export interface Grid {
|
|
|
2775
2822
|
readonly presentation: PresentationApi;
|
|
2776
2823
|
readonly updates: UpdatesApi;
|
|
2777
2824
|
readonly timeline: TimelineApi;
|
|
2825
|
+
/** Cross-filtering — a derived grid filtering the grid it derives from. */
|
|
2826
|
+
readonly crossFilter: CrossFilter;
|
|
2778
2827
|
readonly facets: FacetsApi;
|
|
2779
2828
|
readonly detail: DetailApi;
|
|
2780
2829
|
readonly comments: CommentsApi;
|