@svgrid/grid 3.0.7 → 3.0.9
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/dist/GridMenus.svelte +3 -2
- package/dist/SvGrid.controller.svelte.d.ts +60 -8
- package/dist/SvGrid.controller.svelte.js +490 -114
- package/dist/SvGrid.css +186 -3
- package/dist/SvGrid.svelte +285 -164
- package/dist/SvGrid.types.d.ts +24 -7
- package/dist/cdn/{GridMenus-CcwU8I4t.js → GridMenus-B1sipj6y.js} +165 -165
- package/dist/cdn/{GridMenus-BgUS-W3o.js → GridMenus-DlAzfHLk.js} +201 -201
- package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
- package/dist/cdn/{src-kE9B98r6.js → src-BYsa66I0.js} +9747 -10178
- package/dist/cdn/{src-25JmJ_7g.js → src-pA4RroLV.js} +10709 -11140
- package/dist/cdn/svgrid.js +27 -27
- package/dist/cdn/svgrid.svelte-external.js +27 -27
- package/dist/cdn/validate-XuMe4_KR.js +80 -0
- package/dist/cell-render.js +25 -3
- package/dist/columns.js +16 -1
- package/dist/conditional-formatting.js +9 -2
- package/dist/core.d.ts +93 -0
- package/dist/core.js +313 -47
- package/dist/filtering/excel-filters.js +41 -6
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/keyboard-handlers.js +5 -1
- package/dist/selection.d.ts +2 -2
- package/dist/selection.js +46 -12
- package/dist/server-block-cache.d.ts +7 -32
- package/dist/server-block-cache.js +21 -12
- package/dist/server-data-source.js +3 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/spreadsheet.js +7 -0
- package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
- package/dist/validate.js +13 -14
- package/dist/virtualization/column-virtualizer.d.ts +4 -0
- package/dist/virtualization/column-virtualizer.js +2 -0
- package/dist/virtualization/types.d.ts +20 -0
- package/dist/virtualization/virtualizer.js +90 -24
- package/dist/windowed-brand.d.ts +22 -0
- package/dist/windowed-brand.js +60 -0
- package/dist/windowed-data.d.ts +6 -0
- package/dist/windowed-data.js +34 -0
- package/dist/windowed-row-model.d.ts +1 -0
- package/dist/windowed-row-model.js +97 -0
- package/package.json +1 -1
- package/src/GridMenus.svelte +3 -2
- package/src/SvGrid.controller.svelte.ts +483 -122
- package/src/SvGrid.css +186 -3
- package/src/SvGrid.svelte +285 -164
- package/src/SvGrid.types.ts +24 -7
- package/src/cell-render.ts +24 -2
- package/src/columns.ts +16 -1
- package/src/conditional-formatting.ts +6 -4
- package/src/core.sort.test.ts +61 -0
- package/src/core.ts +309 -49
- package/src/counting-cell.test.svelte +11 -0
- package/src/filtering/excel-filters.ts +39 -6
- package/src/headless.reactivity.svelte.test.ts +89 -0
- package/src/headless.ts +1 -1
- package/src/index.ts +7 -1
- package/src/keyboard-handlers.ts +5 -1
- package/src/new-features.test.ts +24 -0
- package/src/selection.test.ts +51 -0
- package/src/selection.ts +47 -14
- package/src/server-block-cache.ts +27 -10
- package/src/server-data-source.ts +3 -1
- package/src/server.ts +6 -0
- package/src/spreadsheet.ts +6 -0
- package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
- package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
- package/src/svgrid.behavior.test.ts +58 -0
- package/src/svgrid.column-cell-recycling.test.ts +120 -0
- package/src/svgrid.interaction.test.ts +4 -1
- package/src/svgrid.new-features.wrapper.test.ts +5 -4
- package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
- package/src/validate.test.ts +20 -4
- package/src/validate.ts +13 -16
- package/src/virtualization/column-virtualizer.ts +6 -0
- package/src/virtualization/types.ts +20 -0
- package/src/virtualization/virtualizer.test.ts +152 -0
- package/src/virtualization/virtualizer.ts +93 -27
- package/src/windowed-brand.ts +67 -0
- package/src/windowed-data.test.ts +105 -0
- package/src/windowed-data.ts +36 -0
- package/src/windowed-row-model.ts +113 -0
- package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
- package/dist/cdn/validate-AZoD8BoR.js +0 -80
- /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
package/dist/columns.js
CHANGED
|
@@ -146,11 +146,26 @@ export function createColumns(ctx) {
|
|
|
146
146
|
? g.filter((x) => x !== columnId)
|
|
147
147
|
: [...g, columnId]);
|
|
148
148
|
}
|
|
149
|
+
// id -> column for the current column list. Width math asks for every
|
|
150
|
+
// column's width in loops, so a linear `find` per call made those loops
|
|
151
|
+
// quadratic: ~1.2 s of a 5,000-column mount. Rebuilt when the list changes.
|
|
152
|
+
let columnsById = new Map();
|
|
153
|
+
let columnsByIdFor = null;
|
|
154
|
+
function columnById(columnId) {
|
|
155
|
+
const all = ctx.grid.getAllColumns();
|
|
156
|
+
if (all !== columnsByIdFor) {
|
|
157
|
+
columnsById = new Map();
|
|
158
|
+
for (const c of all)
|
|
159
|
+
columnsById.set(c.id, c);
|
|
160
|
+
columnsByIdFor = all;
|
|
161
|
+
}
|
|
162
|
+
return columnsById.get(columnId);
|
|
163
|
+
}
|
|
149
164
|
/** Width before fitColumns scaling - what the columnDef/user actually set. */
|
|
150
165
|
function getColumnBaseWidth(columnId) {
|
|
151
166
|
if (ctx.columnWidths[columnId] !== undefined)
|
|
152
167
|
return ctx.columnWidths[columnId];
|
|
153
|
-
const column =
|
|
168
|
+
const column = columnById(columnId);
|
|
154
169
|
if (column?.columnDef.width !== undefined)
|
|
155
170
|
return column.columnDef.width;
|
|
156
171
|
return ctx.props.columnWidth ?? 140;
|
|
@@ -271,8 +271,15 @@ export function resolveCellFormat(value, row, columnId, formats, stat) {
|
|
|
271
271
|
* on the same row (`compareColumn`) - those read the row, not column stats.
|
|
272
272
|
*/
|
|
273
273
|
export function formatNeedsStats(f) {
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
if (f.type !== 'colorScale' && f.type !== 'dataBar')
|
|
275
|
+
return false;
|
|
276
|
+
if ('compareColumn' in f && f.compareColumn)
|
|
277
|
+
return false;
|
|
278
|
+
// Both ends fixed in absolute units: the column's range is never read, so
|
|
279
|
+
// there is nothing to scan (and on a server-backed grid no way to scan it).
|
|
280
|
+
if (f.minValue != null && f.maxValue != null && (f.bounds ?? 'absolute') === 'absolute')
|
|
281
|
+
return false;
|
|
282
|
+
return true;
|
|
276
283
|
}
|
|
277
284
|
export function formatsNeedingStats(formats) {
|
|
278
285
|
return formats.some(formatNeedsStats);
|
package/dist/core.d.ts
CHANGED
|
@@ -614,6 +614,99 @@ export declare const filterFns: {
|
|
|
614
614
|
includesString: (value: unknown, query: string) => boolean;
|
|
615
615
|
equals: (value: unknown, query: unknown) => boolean;
|
|
616
616
|
};
|
|
617
|
+
/**
|
|
618
|
+
* Everything a base row needs that is the same for every row in the table.
|
|
619
|
+
*
|
|
620
|
+
* One object per table, referenced by every row, instead of one closure scope
|
|
621
|
+
* per row. See {@link BASE_ROW_METHODS}.
|
|
622
|
+
*/
|
|
623
|
+
export type BaseRowCtx<TData extends RowData> = {
|
|
624
|
+
grid: SvGrid<TData>;
|
|
625
|
+
store: {
|
|
626
|
+
state: Record<string, any>;
|
|
627
|
+
};
|
|
628
|
+
columns: Array<Column<TData>>;
|
|
629
|
+
columnCount: number;
|
|
630
|
+
columnIndexById: Map<string, number>;
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Keys for a base row's private fields.
|
|
634
|
+
*
|
|
635
|
+
* Symbols, not string keys, and that is load-bearing. A row's shared methods
|
|
636
|
+
* need a pointer back to the table, but `_ctx` as a normal property made every
|
|
637
|
+
* row serialise the entire grid: `JSON.stringify(oneRow)` grew with the dataset
|
|
638
|
+
* (981 chars at 3 rows, 67,719 at 3,000) because `options.data` is reachable
|
|
639
|
+
* through it, so stringifying a row model was quadratic. Rows used to serialise
|
|
640
|
+
* to a small constant and must again.
|
|
641
|
+
*
|
|
642
|
+
* A symbol key is invisible to `JSON.stringify`, `Object.keys` and `for...in`,
|
|
643
|
+
* yet IS copied by object spread - which matters because several row models
|
|
644
|
+
* legitimately do `{ ...row, depth }` and the clone needs these to work.
|
|
645
|
+
* Non-enumerable string keys would have hidden them from JSON but also from the
|
|
646
|
+
* spread, silently breaking every cloned row.
|
|
647
|
+
*/
|
|
648
|
+
export declare const ROW_CTX: unique symbol;
|
|
649
|
+
export declare const ROW_VALUES: unique symbol;
|
|
650
|
+
export declare const ROW_CELLS: unique symbol;
|
|
651
|
+
export declare const ROW_FILTER_GEN: unique symbol;
|
|
652
|
+
export declare const ROW_FILTER_POS: unique symbol;
|
|
653
|
+
export declare const ROW_SORT_DROPPED: unique symbol;
|
|
654
|
+
export declare const ROW_SORT_KEY: unique symbol;
|
|
655
|
+
/**
|
|
656
|
+
* The rows as a plain dense array. For a lazyRows() list, makes the rows not
|
|
657
|
+
* made yet and returns its backing array - the same row objects, without the
|
|
658
|
+
* Proxy in front. Anything that reads every row (a sort, a filter) should go
|
|
659
|
+
* through this first: a trap per read, over several passes of a million rows,
|
|
660
|
+
* cost more than building the rows did.
|
|
661
|
+
*/
|
|
662
|
+
export declare function denseRows<T>(rows: T[]): T[];
|
|
663
|
+
/**
|
|
664
|
+
* The row model for windowed data (windowed-data.ts), or null when `data` is
|
|
665
|
+
* an ordinary array. It lives in windowed-row-model.ts and is installed by the
|
|
666
|
+
* first createWindowedData() call, so a grid that is never handed windowed
|
|
667
|
+
* data does not bundle it: this file imports nothing from either module.
|
|
668
|
+
*/
|
|
669
|
+
export type WindowedRowModel<TData extends RowData> = (data: unknown, columns: Array<Column<TData>>) => RowModel<TData> | null;
|
|
670
|
+
export type WindowedRowModelInstall = <TData extends RowData>(ctx: {
|
|
671
|
+
rowCtxFor: (columns: Array<Column<TData>>) => BaseRowCtx<TData>;
|
|
672
|
+
getRowId: () => ((row: TData, index: number) => string) | undefined;
|
|
673
|
+
}) => WindowedRowModel<TData>;
|
|
674
|
+
/** Internal: windowed-row-model.ts installs itself here. */
|
|
675
|
+
export declare function registerWindowedRowModel(install: WindowedRowModelInstall): void;
|
|
676
|
+
/** A base row's private fields, on top of the public {@link Row} surface. */
|
|
677
|
+
export type BaseRowState<TData extends RowData> = Row<TData> & {
|
|
678
|
+
[ROW_CTX]: BaseRowCtx<TData>;
|
|
679
|
+
[ROW_VALUES]: Array<unknown> | null;
|
|
680
|
+
[ROW_CELLS]: Array<Cell<TData>> | null;
|
|
681
|
+
[ROW_FILTER_GEN]: number;
|
|
682
|
+
[ROW_FILTER_POS]: number;
|
|
683
|
+
[ROW_SORT_DROPPED]: number;
|
|
684
|
+
[ROW_SORT_KEY]: unknown;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* The methods every base row carries, defined ONCE and assigned by reference.
|
|
688
|
+
*
|
|
689
|
+
* Rows used to be built as object literals whose methods were closures, which
|
|
690
|
+
* meant a 100k-row grid allocated 700k closures and a closure scope per row
|
|
691
|
+
* before painting anything. Measured at 100k x 9: 13.8 ms and 56.5 MB to build,
|
|
692
|
+
* against 2.0 ms and 14.5 MB for this shape - the single largest cost in
|
|
693
|
+
* mounting a large grid.
|
|
694
|
+
*
|
|
695
|
+
* They read their row through `this` rather than a captured variable, which is
|
|
696
|
+
* why they can be shared. Note they are assigned as OWN properties rather than
|
|
697
|
+
* put on a prototype: `Row` is public, several row models legitimately do
|
|
698
|
+
* `{ ...row, depth }`, and a spread copies own properties but not a prototype.
|
|
699
|
+
* A class here would silently strip every method off a cloned row.
|
|
700
|
+
*/
|
|
701
|
+
export declare const BASE_ROW_METHODS: {
|
|
702
|
+
getCanExpand(this: BaseRowState<RowData>): boolean;
|
|
703
|
+
getIsExpanded(this: BaseRowState<RowData>): boolean;
|
|
704
|
+
toggleExpanded(this: BaseRowState<RowData>): void;
|
|
705
|
+
getIsSelected(this: BaseRowState<RowData>): boolean;
|
|
706
|
+
toggleSelected(this: BaseRowState<RowData>): void;
|
|
707
|
+
getAllCells(this: BaseRowState<RowData>): Cell<RowData>[];
|
|
708
|
+
getCellValueByColumnId(this: BaseRowState<RowData>, columnId: string): unknown;
|
|
709
|
+
};
|
|
617
710
|
/**
|
|
618
711
|
* One stage of the row pipeline: takes the rows produced so far and returns the
|
|
619
712
|
* next set. Stages compose in the order given to `_rowModels`, so filtering
|
package/dist/core.js
CHANGED
|
@@ -202,22 +202,102 @@ export const filterFns = {
|
|
|
202
202
|
* Non-enumerable string keys would have hidden them from JSON but also from the
|
|
203
203
|
* spread, silently breaking every cloned row.
|
|
204
204
|
*/
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
205
|
+
// Exported for windowed-row-model.ts only; index.ts does not re-export them.
|
|
206
|
+
export const ROW_CTX = Symbol('svgrid.row.ctx');
|
|
207
|
+
export const ROW_VALUES = Symbol('svgrid.row.values');
|
|
208
|
+
export const ROW_CELLS = Symbol('svgrid.row.cells');
|
|
208
209
|
// Where the filtered stage last put this row: the generation of that full
|
|
209
210
|
// filter and the row's index in its output. Lets a tick swap a replaced row
|
|
210
211
|
// into place without re-filtering everything (see createFilteredRowModel).
|
|
211
|
-
const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen');
|
|
212
|
-
const ROW_FILTER_POS = Symbol('svgrid.row.filterPos');
|
|
212
|
+
export const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen');
|
|
213
|
+
export const ROW_FILTER_POS = Symbol('svgrid.row.filterPos');
|
|
213
214
|
// Stamped on the rows a sort repair drops, so the merge skips them with one
|
|
214
215
|
// property read per row instead of a Set lookup (3 ms per 100k rows).
|
|
215
|
-
const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped');
|
|
216
|
+
export const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped');
|
|
216
217
|
// The key(s) the last full sort computed for a row: the key itself for a
|
|
217
218
|
// one-clause sort, an array for several. Read by the repair path so a tick
|
|
218
219
|
// never recomputes a kept row's key.
|
|
219
|
-
const ROW_SORT_KEY = Symbol('svgrid.row.sortKey');
|
|
220
|
+
export const ROW_SORT_KEY = Symbol('svgrid.row.sortKey');
|
|
220
221
|
let sortDropGeneration = 0;
|
|
222
|
+
const LAZY_ROWS = Symbol('svgrid.lazyRows');
|
|
223
|
+
const LAZY_FILL = Symbol('svgrid.lazyRows.fill');
|
|
224
|
+
/**
|
|
225
|
+
* A real array of `length` rows whose entries are made on first read.
|
|
226
|
+
*
|
|
227
|
+
* A Proxy over a holey array: `get` fills a hole by calling `make`, and
|
|
228
|
+
* `has` / `getOwnPropertyDescriptor` report every index as present, so
|
|
229
|
+
* map, filter, slice, sort and iteration see a dense array (without them
|
|
230
|
+
* they would skip the holes). Writes go to the backing array.
|
|
231
|
+
*/
|
|
232
|
+
function lazyRows(length, make) {
|
|
233
|
+
const store = new Array(length);
|
|
234
|
+
const at = (key) => {
|
|
235
|
+
if (typeof key !== 'string')
|
|
236
|
+
return -1;
|
|
237
|
+
const c = key.charCodeAt(0);
|
|
238
|
+
if (c < 48 || c > 57)
|
|
239
|
+
return -1;
|
|
240
|
+
const i = +key;
|
|
241
|
+
return i < length && (i | 0) === i ? i : -1;
|
|
242
|
+
};
|
|
243
|
+
// Make every row not made yet, in one plain loop, and return the backing
|
|
244
|
+
// array: dense from then on, and read without the trap.
|
|
245
|
+
const fill = () => {
|
|
246
|
+
for (let i = 0; i < length; i++)
|
|
247
|
+
if (store[i] === undefined)
|
|
248
|
+
store[i] = make(i);
|
|
249
|
+
return store;
|
|
250
|
+
};
|
|
251
|
+
return new Proxy(store, {
|
|
252
|
+
get(t, key, receiver) {
|
|
253
|
+
if (key === LAZY_ROWS)
|
|
254
|
+
return t;
|
|
255
|
+
if (key === LAZY_FILL)
|
|
256
|
+
return fill;
|
|
257
|
+
const i = at(key);
|
|
258
|
+
if (i < 0)
|
|
259
|
+
return Reflect.get(t, key, receiver);
|
|
260
|
+
let row = t[i];
|
|
261
|
+
if (row === undefined)
|
|
262
|
+
t[i] = row = make(i);
|
|
263
|
+
return row;
|
|
264
|
+
},
|
|
265
|
+
has(t, key) {
|
|
266
|
+
if (key === LAZY_ROWS)
|
|
267
|
+
return true;
|
|
268
|
+
return at(key) >= 0 || Reflect.has(t, key);
|
|
269
|
+
},
|
|
270
|
+
getOwnPropertyDescriptor(t, key) {
|
|
271
|
+
const i = at(key);
|
|
272
|
+
if (i < 0)
|
|
273
|
+
return Reflect.getOwnPropertyDescriptor(t, key);
|
|
274
|
+
let row = t[i];
|
|
275
|
+
if (row === undefined)
|
|
276
|
+
t[i] = row = make(i);
|
|
277
|
+
return { value: row, writable: true, enumerable: true, configurable: true };
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The rows as a plain dense array. For a lazyRows() list, makes the rows not
|
|
283
|
+
* made yet and returns its backing array - the same row objects, without the
|
|
284
|
+
* Proxy in front. Anything that reads every row (a sort, a filter) should go
|
|
285
|
+
* through this first: a trap per read, over several passes of a million rows,
|
|
286
|
+
* cost more than building the rows did.
|
|
287
|
+
*/
|
|
288
|
+
export function denseRows(rows) {
|
|
289
|
+
const fill = rows[LAZY_FILL];
|
|
290
|
+
return fill ? fill() : rows;
|
|
291
|
+
}
|
|
292
|
+
/** The backing array of a lazyRows() list (rows made so far, holes elsewhere), or null. */
|
|
293
|
+
function lazyRowsBacking(rows) {
|
|
294
|
+
return rows ? (rows[LAZY_ROWS] ?? null) : null;
|
|
295
|
+
}
|
|
296
|
+
let installWindowedRowModel = null;
|
|
297
|
+
/** Internal: windowed-row-model.ts installs itself here. */
|
|
298
|
+
export function registerWindowedRowModel(install) {
|
|
299
|
+
installWindowedRowModel = install;
|
|
300
|
+
}
|
|
221
301
|
/**
|
|
222
302
|
* The methods every base row carries, defined ONCE and assigned by reference.
|
|
223
303
|
*
|
|
@@ -233,7 +313,7 @@ let sortDropGeneration = 0;
|
|
|
233
313
|
* `{ ...row, depth }`, and a spread copies own properties but not a prototype.
|
|
234
314
|
* A class here would silently strip every method off a cloned row.
|
|
235
315
|
*/
|
|
236
|
-
const BASE_ROW_METHODS = {
|
|
316
|
+
export const BASE_ROW_METHODS = {
|
|
237
317
|
getCanExpand() {
|
|
238
318
|
return false;
|
|
239
319
|
},
|
|
@@ -777,6 +857,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
777
857
|
let compare;
|
|
778
858
|
let keyOf;
|
|
779
859
|
let autoMode;
|
|
860
|
+
let rankCount;
|
|
780
861
|
if (comparator === sortFns.number) {
|
|
781
862
|
keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId));
|
|
782
863
|
for (let i = 0; i < n; i++)
|
|
@@ -827,9 +908,20 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
827
908
|
// column. Reading every k-th row is no more expensive and does not care
|
|
828
909
|
// how the rows are arranged.
|
|
829
910
|
const rankLimit = n >> 1;
|
|
830
|
-
|
|
911
|
+
// Each distinct value's id (its insertion order), when ranking.
|
|
912
|
+
let idOf = null;
|
|
831
913
|
if (n > 0) {
|
|
832
|
-
|
|
914
|
+
// Ranking pays when the column has at most n/2 distinct values. A
|
|
915
|
+
// small sample cannot see that by asking whether half of it repeats:
|
|
916
|
+
// 1M rows of 80k values, each one 12 times over, gave 256 samples
|
|
917
|
+
// with almost no repeats, so ranking was skipped and the sort made
|
|
918
|
+
// 20M collator calls (1.75 s; ranked, it is a fraction of that).
|
|
919
|
+
// Count the repeats in a larger sample instead. S samples from u
|
|
920
|
+
// equally common values repeat about S^2 / 2u times, so u <= n/2
|
|
921
|
+
// shows as at least S^2 / n repeats. Up to SAMPLE rows the whole
|
|
922
|
+
// column is the sample and the test is exact.
|
|
923
|
+
const SAMPLE = 2048;
|
|
924
|
+
const sampleTarget = Math.min(n, SAMPLE);
|
|
833
925
|
const stride = Math.max(1, Math.floor(n / sampleTarget));
|
|
834
926
|
const sample = new Set();
|
|
835
927
|
let sampled = 0;
|
|
@@ -837,17 +929,30 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
837
929
|
sample.add(autoTextKey(rows[i].getCellValueByColumnId(columnId)));
|
|
838
930
|
sampled++;
|
|
839
931
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
932
|
+
const repeats = sampled - sample.size;
|
|
933
|
+
const worthRanking = stride === 1 ? sample.size <= rankLimit : repeats * n >= sampled * sampled;
|
|
934
|
+
if (worthRanking)
|
|
935
|
+
idOf = new Map();
|
|
843
936
|
}
|
|
937
|
+
// One hash per row: the value's id is looked up (or assigned) as the
|
|
938
|
+
// row is read, and the rank lookup further down is an array read by
|
|
939
|
+
// id. It used to add every value to a Set and then look every value up
|
|
940
|
+
// again in a rank Map, two hashes per row.
|
|
941
|
+
const ids = idOf ? new Int32Array(n) : null;
|
|
844
942
|
for (let i = 0; i < n; i++) {
|
|
845
943
|
const s = autoTextKey(rows[i].getCellValueByColumnId(columnId));
|
|
846
944
|
strings[i] = s;
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
945
|
+
if (idOf) {
|
|
946
|
+
let id = idOf.get(s);
|
|
947
|
+
if (id === undefined) {
|
|
948
|
+
id = idOf.size;
|
|
949
|
+
idOf.set(s, id);
|
|
950
|
+
if (idOf.size > rankLimit) {
|
|
951
|
+
idOf = null;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
ids[i] = id;
|
|
851
956
|
}
|
|
852
957
|
}
|
|
853
958
|
// Collation is by far the most expensive comparison we do - a CPU
|
|
@@ -863,13 +968,17 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
863
968
|
// Guarded on the uniqueness ratio: when nearly every value is distinct
|
|
864
969
|
// the ranking pass cannot save any collator calls and would just add an
|
|
865
970
|
// O(n) Map build, so that case keeps comparing directly.
|
|
866
|
-
if (
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
971
|
+
if (idOf) {
|
|
972
|
+
// Map keys come back in insertion order, which is id order.
|
|
973
|
+
const values = Array.from(idOf.keys());
|
|
974
|
+
const byRank = values.map((_, id) => id).sort((a, b) => compareCollatedKeys(values[a], values[b]));
|
|
975
|
+
const rankById = new Int32Array(values.length);
|
|
976
|
+
for (let r = 0; r < byRank.length; r++)
|
|
977
|
+
rankById[byRank[r]] = r;
|
|
871
978
|
for (let i = 0; i < n; i++)
|
|
872
|
-
keys[i] =
|
|
979
|
+
keys[i] = rankById[ids[i]];
|
|
980
|
+
const rankIds = idOf;
|
|
981
|
+
rankCount = values.length;
|
|
873
982
|
compare = compareNumericKeys;
|
|
874
983
|
// A replacement whose text is not among the ranked values has no
|
|
875
984
|
// rank; the repair path treats that as "sort everything again".
|
|
@@ -878,7 +987,8 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
878
987
|
// A number arriving in a tick makes the column mixed; resort fully.
|
|
879
988
|
if (typeof value === 'number')
|
|
880
989
|
return MISSING_KEY;
|
|
881
|
-
|
|
990
|
+
const id = rankIds.get(autoTextKey(value));
|
|
991
|
+
return id === undefined ? MISSING_KEY : rankById[id];
|
|
882
992
|
};
|
|
883
993
|
}
|
|
884
994
|
else {
|
|
@@ -896,14 +1006,16 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
896
1006
|
keys[i] = keyOf(rows[i]);
|
|
897
1007
|
compare = comparator;
|
|
898
1008
|
}
|
|
899
|
-
clauses.push({ keys, desc: clause.desc, compare, keyOf });
|
|
1009
|
+
clauses.push({ keys, desc: clause.desc, compare, keyOf, rankCount });
|
|
900
1010
|
}
|
|
901
1011
|
if (!clauses.length)
|
|
902
1012
|
return null;
|
|
903
1013
|
// Sort an index array, then materialise. `Array.prototype.sort` is stable,
|
|
904
1014
|
// so equal keys keep their original relative order exactly as the previous
|
|
905
1015
|
// `[...rows].sort(...)` did.
|
|
906
|
-
|
|
1016
|
+
// An index array the comparator sorts in place, or a typed array from
|
|
1017
|
+
// countingOrder / radixOrder for a large one-column numeric sort.
|
|
1018
|
+
let order = new Array(rows.length);
|
|
907
1019
|
for (let i = 0; i < order.length; i++)
|
|
908
1020
|
order[i] = i;
|
|
909
1021
|
// Single-clause sorts get a specialised comparator.
|
|
@@ -920,19 +1032,28 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
920
1032
|
// which the spec coerces to 0, and they differ only in producing 0 versus
|
|
921
1033
|
// -0 for equal keys, which sorts identically.
|
|
922
1034
|
if (clauses.length === 1) {
|
|
923
|
-
const { keys, compare, desc } = clauses[0];
|
|
924
|
-
if (
|
|
1035
|
+
const { keys, compare, desc, rankCount } = clauses[0];
|
|
1036
|
+
if (rankCount !== undefined && rows.length >= STABLE_NUMERIC_SORT_MIN) {
|
|
1037
|
+
order = countingOrder(keys, rankCount, desc);
|
|
1038
|
+
}
|
|
1039
|
+
else if (compare === compareNumericKeys && rows.length >= STABLE_NUMERIC_SORT_MIN) {
|
|
1040
|
+
order = radixOrder(keys, desc);
|
|
1041
|
+
}
|
|
1042
|
+
else if (compare === compareNumericKeys) {
|
|
1043
|
+
;
|
|
925
1044
|
order.sort(desc
|
|
926
1045
|
? function compareOneNumericDesc(ia, ib) { return keys[ib] - keys[ia]; }
|
|
927
1046
|
: function compareOneNumericAsc(ia, ib) { return keys[ia] - keys[ib]; });
|
|
928
1047
|
}
|
|
929
1048
|
else {
|
|
1049
|
+
;
|
|
930
1050
|
order.sort(desc
|
|
931
1051
|
? function compareOneDesc(ia, ib) { return -compare(keys[ia], keys[ib]); }
|
|
932
1052
|
: function compareOneAsc(ia, ib) { return compare(keys[ia], keys[ib]); });
|
|
933
1053
|
}
|
|
934
1054
|
}
|
|
935
1055
|
else {
|
|
1056
|
+
;
|
|
936
1057
|
order.sort(function compareRowsByClauses(ia, ib) {
|
|
937
1058
|
for (let k = 0; k < clauses.length; k++) {
|
|
938
1059
|
const clause = clauses[k];
|
|
@@ -1159,6 +1280,94 @@ function autoSortMode(rows, columnId) {
|
|
|
1159
1280
|
* Left as its own function so the sort path has one place to change if that
|
|
1160
1281
|
* ever stops being true. Re-measure before "optimising" this again.
|
|
1161
1282
|
*/
|
|
1283
|
+
/**
|
|
1284
|
+
* From this many rows, a one-column numeric sort orders its row indices
|
|
1285
|
+
* without a comparator. `Array.prototype.sort` with a JS comparator made
|
|
1286
|
+
* ~20M comparator calls for 1M rows, most of a numeric sort's time; both
|
|
1287
|
+
* orders below are stable, as that sort is, so equal keys keep input order.
|
|
1288
|
+
* Under it the comparator sort is cheaper than the bucket arrays.
|
|
1289
|
+
*/
|
|
1290
|
+
const STABLE_NUMERIC_SORT_MIN = 20000;
|
|
1291
|
+
/** Stable order of `keys` that are integer ranks in [0, rankCount): one counting pass. */
|
|
1292
|
+
function countingOrder(keys, rankCount, desc) {
|
|
1293
|
+
const n = keys.length;
|
|
1294
|
+
const starts = new Uint32Array(rankCount + 1);
|
|
1295
|
+
for (let i = 0; i < n; i++)
|
|
1296
|
+
starts[(desc ? rankCount - 1 - keys[i] : keys[i]) + 1]++;
|
|
1297
|
+
for (let r = 0; r < rankCount; r++)
|
|
1298
|
+
starts[r + 1] += starts[r];
|
|
1299
|
+
const out = new Uint32Array(n);
|
|
1300
|
+
for (let i = 0; i < n; i++)
|
|
1301
|
+
out[starts[desc ? rankCount - 1 - keys[i] : keys[i]]++] = i;
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
1305
|
+
/**
|
|
1306
|
+
* Stable order of float keys: an LSD radix sort over the IEEE bits, four
|
|
1307
|
+
* 16-bit passes, a pass skipped when every key shares its digit. The bits are
|
|
1308
|
+
* mapped so unsigned order is numeric order (sign bit flipped for positives,
|
|
1309
|
+
* every bit for negatives), and inverted for a descending sort, which keeps
|
|
1310
|
+
* equal keys in input order exactly as a descending comparator does. -0 is
|
|
1311
|
+
* folded into 0 first, since the comparator treated them as equal. Keys are
|
|
1312
|
+
* never NaN here: numberSortKey and dateSortKey map those to -Infinity.
|
|
1313
|
+
*/
|
|
1314
|
+
function radixOrder(keys, desc) {
|
|
1315
|
+
const n = keys.length;
|
|
1316
|
+
const f64 = new Float64Array(n);
|
|
1317
|
+
for (let i = 0; i < n; i++)
|
|
1318
|
+
f64[i] = keys[i] + 0;
|
|
1319
|
+
const words = new Uint32Array(f64.buffer);
|
|
1320
|
+
const lo = new Uint32Array(n);
|
|
1321
|
+
const hi = new Uint32Array(n);
|
|
1322
|
+
const loAt = LITTLE_ENDIAN ? 0 : 1;
|
|
1323
|
+
const hiAt = 1 - loAt;
|
|
1324
|
+
for (let i = 0; i < n; i++) {
|
|
1325
|
+
let h = words[2 * i + hiAt];
|
|
1326
|
+
let l = words[2 * i + loAt];
|
|
1327
|
+
if (h & 0x80000000) {
|
|
1328
|
+
h = ~h >>> 0;
|
|
1329
|
+
l = ~l >>> 0;
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
h = (h | 0x80000000) >>> 0;
|
|
1333
|
+
}
|
|
1334
|
+
if (desc) {
|
|
1335
|
+
h = ~h >>> 0;
|
|
1336
|
+
l = ~l >>> 0;
|
|
1337
|
+
}
|
|
1338
|
+
hi[i] = h;
|
|
1339
|
+
lo[i] = l;
|
|
1340
|
+
}
|
|
1341
|
+
let src = new Uint32Array(n);
|
|
1342
|
+
for (let i = 0; i < n; i++)
|
|
1343
|
+
src[i] = i;
|
|
1344
|
+
let dst = new Uint32Array(n);
|
|
1345
|
+
const counts = new Uint32Array(65536);
|
|
1346
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
1347
|
+
const word = pass < 2 ? lo : hi;
|
|
1348
|
+
const shift = pass % 2 === 0 ? 0 : 16;
|
|
1349
|
+
counts.fill(0);
|
|
1350
|
+
for (let i = 0; i < n; i++)
|
|
1351
|
+
counts[(word[i] >>> shift) & 0xffff]++;
|
|
1352
|
+
// Every key in one bucket: this digit cannot change the order.
|
|
1353
|
+
if (counts[(word[src[0]] >>> shift) & 0xffff] === n)
|
|
1354
|
+
continue;
|
|
1355
|
+
let sum = 0;
|
|
1356
|
+
for (let d = 0; d < 65536; d++) {
|
|
1357
|
+
const c = counts[d];
|
|
1358
|
+
counts[d] = sum;
|
|
1359
|
+
sum += c;
|
|
1360
|
+
}
|
|
1361
|
+
for (let i = 0; i < n; i++) {
|
|
1362
|
+
const idx = src[i];
|
|
1363
|
+
dst[counts[(word[idx] >>> shift) & 0xffff]++] = idx;
|
|
1364
|
+
}
|
|
1365
|
+
const t = src;
|
|
1366
|
+
src = dst;
|
|
1367
|
+
dst = t;
|
|
1368
|
+
}
|
|
1369
|
+
return src;
|
|
1370
|
+
}
|
|
1162
1371
|
function compareCollatedKeys(a, b) {
|
|
1163
1372
|
return a.localeCompare(b);
|
|
1164
1373
|
}
|
|
@@ -1192,6 +1401,24 @@ export function createSvGridCore(options) {
|
|
|
1192
1401
|
/** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */
|
|
1193
1402
|
let replacedRows = null;
|
|
1194
1403
|
let cachedRowCtx = null;
|
|
1404
|
+
/** This grid's windowed-data row model, made on first use from the installed hook. */
|
|
1405
|
+
let windowedRowModel = null;
|
|
1406
|
+
/** The shared row context for `columns`, reused while they are the same. */
|
|
1407
|
+
function rowCtxFor(columns) {
|
|
1408
|
+
if (cachedRowCtx && cachedRowCtx.columns === columns)
|
|
1409
|
+
return cachedRowCtx;
|
|
1410
|
+
const columnIndexById = new Map();
|
|
1411
|
+
for (let i = 0; i < columns.length; i++)
|
|
1412
|
+
columnIndexById.set(columns[i].id, i);
|
|
1413
|
+
cachedRowCtx = {
|
|
1414
|
+
grid: grid,
|
|
1415
|
+
store,
|
|
1416
|
+
columns,
|
|
1417
|
+
columnCount: columns.length,
|
|
1418
|
+
columnIndexById,
|
|
1419
|
+
};
|
|
1420
|
+
return cachedRowCtx;
|
|
1421
|
+
}
|
|
1195
1422
|
let cachedRowModel = null;
|
|
1196
1423
|
let cachedRowModelBaseRows = null;
|
|
1197
1424
|
let cachedPipeline = options._rowModels;
|
|
@@ -1375,6 +1602,12 @@ export function createSvGridCore(options) {
|
|
|
1375
1602
|
// (a $state.raw signal), and the rebuild below touched it twice per row:
|
|
1376
1603
|
// 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.
|
|
1377
1604
|
const data = options.data;
|
|
1605
|
+
if (installWindowedRowModel) {
|
|
1606
|
+
windowedRowModel ?? (windowedRowModel = installWindowedRowModel({ rowCtxFor, getRowId: () => options.getRowId }));
|
|
1607
|
+
const model = windowedRowModel(data, columns);
|
|
1608
|
+
if (model)
|
|
1609
|
+
return model;
|
|
1610
|
+
}
|
|
1378
1611
|
if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {
|
|
1379
1612
|
// Same columns as last time: the shared context still describes them,
|
|
1380
1613
|
// and a row whose data object sits at the same index can keep its
|
|
@@ -1384,7 +1617,10 @@ export function createSvGridCore(options) {
|
|
|
1384
1617
|
// landing. The reused row drops its memoised values and cells, since
|
|
1385
1618
|
// an app may have changed the object in place before passing a new
|
|
1386
1619
|
// array - that is the case the old rebuild covered by accident.
|
|
1387
|
-
|
|
1620
|
+
// A lazy row list (lazyRows below) is read through its backing array,
|
|
1621
|
+
// so reusing rows never makes the ones nobody looked at.
|
|
1622
|
+
const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? (lazyRowsBacking(cachedBaseRows) ?? cachedBaseRows) : null;
|
|
1623
|
+
const previousInput = cachedBaseRowsInput;
|
|
1388
1624
|
// Same length as last time: a replacement, not an add or remove, so
|
|
1389
1625
|
// each new row stands in for the old row at its index and the stages
|
|
1390
1626
|
// can repair rather than recompute. Anything else is structural.
|
|
@@ -1412,26 +1648,14 @@ export function createSvGridCore(options) {
|
|
|
1412
1648
|
};
|
|
1413
1649
|
cachedRowCtx = rowCtx;
|
|
1414
1650
|
}
|
|
1415
|
-
cachedBaseRows = new Array(data.length);
|
|
1416
1651
|
const getRowId = options.getRowId;
|
|
1417
1652
|
const m = BASE_ROW_METHODS;
|
|
1418
|
-
|
|
1653
|
+
// `_values` and `_cells` stay null until something reads them - a
|
|
1654
|
+
// 100k-row grid showing twenty rows must not materialise every row's
|
|
1655
|
+
// values or cell objects to paint.
|
|
1656
|
+
const makeRow = (index) => {
|
|
1419
1657
|
const original = data[index];
|
|
1420
|
-
|
|
1421
|
-
if (kept && kept.original === original) {
|
|
1422
|
-
kept[ROW_VALUES] = null;
|
|
1423
|
-
kept[ROW_CELLS] = null;
|
|
1424
|
-
cachedBaseRows[index] = kept;
|
|
1425
|
-
continue;
|
|
1426
|
-
}
|
|
1427
|
-
// Past a quarter of the rows the merge stops being cheaper than a
|
|
1428
|
-
// sort with the ranking pass; let the stages do the full work.
|
|
1429
|
-
if (replacedRows && replacedRows.size * 4 >= data.length)
|
|
1430
|
-
replacedRows = null;
|
|
1431
|
-
// `_values` and `_cells` stay null until something reads them - a
|
|
1432
|
-
// 100k-row grid showing twenty rows must not materialise every row's
|
|
1433
|
-
// values or cell objects to paint.
|
|
1434
|
-
const row = {
|
|
1658
|
+
return {
|
|
1435
1659
|
id: getRowId ? getRowId(original, index) : String(index),
|
|
1436
1660
|
index,
|
|
1437
1661
|
original,
|
|
@@ -1451,6 +1675,42 @@ export function createSvGridCore(options) {
|
|
|
1451
1675
|
getAllCells: m.getAllCells,
|
|
1452
1676
|
getCellValueByColumnId: m.getCellValueByColumnId,
|
|
1453
1677
|
};
|
|
1678
|
+
};
|
|
1679
|
+
if (!previous) {
|
|
1680
|
+
// A first build has nothing to reuse or repair: make each row the
|
|
1681
|
+
// first time something reads it. A grid that only shows the first
|
|
1682
|
+
// screen makes ~40 row objects, not 100k; a sort or a filter reads
|
|
1683
|
+
// every row and makes them then. Building them all up front was
|
|
1684
|
+
// most of a 100k-row mount (~24 ms, plus the garbage).
|
|
1685
|
+
cachedBaseRows = lazyRows(data.length, makeRow);
|
|
1686
|
+
}
|
|
1687
|
+
else
|
|
1688
|
+
cachedBaseRows = new Array(data.length);
|
|
1689
|
+
for (let index = 0; previous && index < data.length; index++) {
|
|
1690
|
+
const original = data[index];
|
|
1691
|
+
const kept = previous[index];
|
|
1692
|
+
if (kept && kept.original === original) {
|
|
1693
|
+
// Read before writing: a row nobody rendered (a million off-screen
|
|
1694
|
+
// placeholders under a row model) has nothing memoised, and a
|
|
1695
|
+
// store per row was a measurable share of each block landing.
|
|
1696
|
+
if (kept[ROW_VALUES] !== null)
|
|
1697
|
+
kept[ROW_VALUES] = null;
|
|
1698
|
+
if (kept[ROW_CELLS] !== null)
|
|
1699
|
+
kept[ROW_CELLS] = null;
|
|
1700
|
+
cachedBaseRows[index] = kept;
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
// Past a quarter of the rows the merge stops being cheaper than a
|
|
1704
|
+
// sort with the ranking pass; let the stages do the full work.
|
|
1705
|
+
if (replacedRows && replacedRows.size * 4 >= data.length)
|
|
1706
|
+
replacedRows = null;
|
|
1707
|
+
const row = makeRow(index);
|
|
1708
|
+
// The previous list was lazy and never made this row, but its data
|
|
1709
|
+
// object is the same one: hand the old list this row too, so a
|
|
1710
|
+
// caller still holding it gets the same object for the same data,
|
|
1711
|
+
// as when every row was built up front.
|
|
1712
|
+
if (kept === undefined && previousInput?.[index] === original)
|
|
1713
|
+
previous[index] = row;
|
|
1454
1714
|
cachedBaseRows[index] = row;
|
|
1455
1715
|
if (replacedRows && kept)
|
|
1456
1716
|
replacedRows.set(kept, row);
|
|
@@ -1494,7 +1754,13 @@ export function createSvGridCore(options) {
|
|
|
1494
1754
|
cachedSlices?.expanded === currentSlices.expanded) {
|
|
1495
1755
|
return cachedRowModel;
|
|
1496
1756
|
}
|
|
1497
|
-
|
|
1757
|
+
// A stage with work to do reads every row; give it the dense array.
|
|
1758
|
+
const anyStageActive = (currentSlices.sorting?.length ?? 0) > 0 ||
|
|
1759
|
+
(currentSlices.columnFilters?.length ?? 0) > 0 ||
|
|
1760
|
+
(currentSlices.grouping?.length ?? 0) > 0 ||
|
|
1761
|
+
currentSlices.expanded === true ||
|
|
1762
|
+
Object.keys(currentSlices.expanded ?? {}).length > 0;
|
|
1763
|
+
let rows = anyStageActive ? denseRows(cachedBaseRows) : cachedBaseRows;
|
|
1498
1764
|
const pipeline = options._rowModels ?? {};
|
|
1499
1765
|
const ordered = [
|
|
1500
1766
|
pipeline.coreRowModel,
|