@svgrid/grid 3.0.8 → 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.
Files changed (80) hide show
  1. package/dist/GridMenus.svelte +3 -2
  2. package/dist/SvGrid.controller.svelte.d.ts +60 -8
  3. package/dist/SvGrid.controller.svelte.js +475 -110
  4. package/dist/SvGrid.css +186 -3
  5. package/dist/SvGrid.svelte +281 -163
  6. package/dist/SvGrid.types.d.ts +24 -7
  7. package/dist/cdn/{GridMenus-D15bhpX5.js → GridMenus-B1sipj6y.js} +165 -165
  8. package/dist/cdn/{GridMenus-B8-Gbfq2.js → GridMenus-DlAzfHLk.js} +201 -201
  9. package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
  10. package/dist/cdn/{src-Dz625Fhx.js → src-BYsa66I0.js} +9738 -10174
  11. package/dist/cdn/{src-CleI0g9l.js → src-pA4RroLV.js} +10702 -11138
  12. package/dist/cdn/svgrid.js +27 -27
  13. package/dist/cdn/svgrid.svelte-external.js +27 -27
  14. package/dist/cdn/{validate-GEs4ZPSW.js → validate-XuMe4_KR.js} +2 -2
  15. package/dist/cell-render.js +25 -3
  16. package/dist/columns.js +16 -1
  17. package/dist/conditional-formatting.js +9 -2
  18. package/dist/core.d.ts +93 -0
  19. package/dist/core.js +313 -52
  20. package/dist/filtering/excel-filters.js +41 -6
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/selection.d.ts +2 -2
  24. package/dist/selection.js +32 -8
  25. package/dist/server-block-cache.d.ts +7 -32
  26. package/dist/server-block-cache.js +21 -12
  27. package/dist/server-data-source.js +3 -1
  28. package/dist/server.d.ts +1 -0
  29. package/dist/server.js +1 -0
  30. package/dist/spreadsheet.js +7 -0
  31. package/dist/validate.js +3 -11
  32. package/dist/virtualization/column-virtualizer.d.ts +4 -0
  33. package/dist/virtualization/column-virtualizer.js +2 -0
  34. package/dist/virtualization/types.d.ts +20 -0
  35. package/dist/virtualization/virtualizer.js +90 -24
  36. package/dist/windowed-brand.d.ts +22 -0
  37. package/dist/windowed-brand.js +60 -0
  38. package/dist/windowed-data.d.ts +6 -0
  39. package/dist/windowed-data.js +34 -0
  40. package/dist/windowed-row-model.d.ts +1 -0
  41. package/dist/windowed-row-model.js +97 -0
  42. package/package.json +1 -1
  43. package/src/GridMenus.svelte +3 -2
  44. package/src/SvGrid.controller.svelte.ts +469 -114
  45. package/src/SvGrid.css +186 -3
  46. package/src/SvGrid.svelte +281 -163
  47. package/src/SvGrid.types.ts +24 -7
  48. package/src/cell-render.ts +24 -2
  49. package/src/columns.ts +16 -1
  50. package/src/conditional-formatting.ts +6 -4
  51. package/src/core.sort.test.ts +61 -0
  52. package/src/core.ts +309 -52
  53. package/src/counting-cell.test.svelte +11 -0
  54. package/src/filtering/excel-filters.ts +39 -6
  55. package/src/headless.reactivity.svelte.test.ts +1 -1
  56. package/src/index.ts +6 -0
  57. package/src/new-features.test.ts +24 -0
  58. package/src/selection.test.ts +14 -0
  59. package/src/selection.ts +31 -8
  60. package/src/server-block-cache.ts +27 -10
  61. package/src/server-data-source.ts +3 -1
  62. package/src/server.ts +6 -0
  63. package/src/spreadsheet.ts +6 -0
  64. package/src/svgrid.behavior.test.ts +29 -0
  65. package/src/svgrid.column-cell-recycling.test.ts +120 -0
  66. package/src/svgrid.interaction.test.ts +4 -1
  67. package/src/svgrid.new-features.wrapper.test.ts +5 -4
  68. package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
  69. package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
  70. package/src/validate.test.ts +4 -4
  71. package/src/validate.ts +3 -14
  72. package/src/virtualization/column-virtualizer.ts +6 -0
  73. package/src/virtualization/types.ts +20 -0
  74. package/src/virtualization/virtualizer.test.ts +152 -0
  75. package/src/virtualization/virtualizer.ts +93 -27
  76. package/src/windowed-brand.ts +67 -0
  77. package/src/windowed-data.test.ts +105 -0
  78. package/src/windowed-data.ts +36 -0
  79. package/src/windowed-row-model.ts +113 -0
  80. package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
@@ -1851,13 +1851,17 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1851
1851
  * `virtualization`. **On by default**, which is what makes a 100-column grid
1852
1852
  * scroll smoothly.
1853
1853
  *
1854
- * It recycles column DOM nodes, so it cannot coexist with sticky pinned
1855
- * columns - turn it off if you need pinning to survive horizontal scrolling.
1854
+ * Pinned columns stay rendered while it is on: the pinned-left and
1855
+ * pinned-right columns are drawn as their own runs either side of the
1856
+ * virtual window, so a wide grid with pinned edges still renders only the
1857
+ * columns in view plus the pinned ones.
1856
1858
  */
1857
1859
  columnVirtualization?: boolean;
1858
1860
  /**
1859
- * Extra columns rendered either side of the viewport when
1860
- * `columnVirtualization` is on. Defaults to 3.
1861
+ * Columns kept rendered beyond the viewport when `columnVirtualization`
1862
+ * is on: this many ahead of a horizontal scroll at all times, one behind
1863
+ * it. The rendered columns move three at a time, so right after a move up
1864
+ * to `columnOverscan + 2` are rendered ahead. Defaults to 3.
1861
1865
  */
1862
1866
  columnOverscan?: number;
1863
1867
  /**
@@ -1870,9 +1874,7 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1870
1874
  * column id (matches `ColumnDef.field` when no explicit id is set).
1871
1875
  * The internal pinning state is seeded once at mount; user-driven
1872
1876
  * pinning via the column menu still works and overrides this default.
1873
- * Requires `columnVirtualization={false}` to be visible in the menu
1874
- * UI (sticky positioning can't co-exist with column virtualization
1875
- * since the virtualizer recycles DOM nodes).
1877
+ * Works with column virtualization on (the default).
1876
1878
  */
1877
1879
  initialColumnPinning?: {
1878
1880
  left?: ReadonlyArray<string>;
@@ -2302,6 +2304,12 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
2302
2304
  * triangles by threshold), and `rule` (apply a style when a predicate
2303
2305
  * matches). Scope a format to specific columns with `columns: [...]`,
2304
2306
  * or omit it to apply to every column. Later entries win on conflict.
2307
+ *
2308
+ * `colorScale` and `dataBar` apply to data rows only: group, footer and
2309
+ * grand-total rows carry subtotals on a different scale, so they keep the
2310
+ * `rule` and `iconSet` formats and drop the scaled ones. On a server row
2311
+ * model, give scaled formats `minValue` / `maxValue`; the grid does not
2312
+ * scan rows it has not loaded for a range.
2305
2313
  */
2306
2314
  conditionalFormats?: ReadonlyArray<ConditionalFormat<TData>>;
2307
2315
  /**
@@ -2600,6 +2608,15 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
2600
2608
  * through the column's `editable`.
2601
2609
  */
2602
2610
  mergedCells?: ReadonlyArray<{ rowIndex: number; colIndex: number; rowSpan: number; colSpan: number }>;
2611
+ /**
2612
+ * How rows are laid out. `'flex'` (the default) draws each row as a line
2613
+ * of fixed-width cells, so adding or removing cells in one row - what a
2614
+ * horizontal scroll does every frame - lays out that row only. `'table'`
2615
+ * uses the HTML table algorithm, which re-lays every row on such a change
2616
+ * but can draw a merged cell spanning rows; the grid switches to it on its
2617
+ * own when `mergedCells` has one.
2618
+ */
2619
+ rowLayout?: "flex" | "table";
2603
2620
  /**
2604
2621
  * Rows to pin to the BOTTOM of the grid - rendered below the regular
2605
2622
  * rows and sticky-positioned (sticks to the bottom of the viewport
@@ -29,6 +29,7 @@ import {
29
29
  } from "./SvGrid.helpers";
30
30
  import {
31
31
  getPinnedCellValue,
32
+ isGroupRow,
32
33
  } from "./cell-values";
33
34
  import { parseEditorValue } from "./editors/cell-editors";
34
35
 
@@ -36,13 +37,34 @@ export function createCellRender<
36
37
  TFeatures extends TableFeatures = TableFeatures,
37
38
  TData extends RowData = RowData,
38
39
  >(ctx: any) {
40
+ // `colorScale` and `dataBar` are scaled to the data rows. A group, footer or
41
+ // grand-total row carries a subtotal on a different scale (a sum of a million
42
+ // rows against a range drawn from one), so those rows keep only the rule and
43
+ // icon formats. Filtered once per formats array.
44
+ let rowLevelFormatsFor: unknown = null;
45
+ let rowLevelFormats: ReadonlyArray<any> = [];
46
+ function aggregateRowFormats(formats: ReadonlyArray<any>): ReadonlyArray<any> {
47
+ if (formats !== rowLevelFormatsFor) {
48
+ rowLevelFormats = formats.filter((f) => f.type !== "colorScale" && f.type !== "dataBar");
49
+ rowLevelFormatsFor = formats;
50
+ }
51
+ return rowLevelFormats;
52
+ }
53
+ function isAggregateRow(row: Row<TData>): boolean {
54
+ const kind = (row.original as { __group?: { kind?: string } } | null)?.__group?.kind;
55
+ if (kind) return kind !== "leaf" && kind !== "detail";
56
+ return isGroupRow(row);
57
+ }
58
+
39
59
  function cellConditionalFormat(
40
60
  row: Row<TData>,
41
61
  column: Column<TData>,
42
62
  value: unknown,
43
63
  ): ResolvedCellFormat | null {
44
- const formats = ctx.props.conditionalFormats;
45
- if (!formats?.length) return null;
64
+ const all = ctx.props.conditionalFormats;
65
+ if (!all?.length) return null;
66
+ const formats = isAggregateRow(row) ? aggregateRowFormats(all) : all;
67
+ if (!formats.length) return null;
46
68
  return resolveCellFormat(
47
69
  value,
48
70
  row.original,
package/src/columns.ts CHANGED
@@ -157,10 +157,25 @@ export function createColumns<
157
157
  );
158
158
  }
159
159
 
160
+ // id -> column for the current column list. Width math asks for every
161
+ // column's width in loops, so a linear `find` per call made those loops
162
+ // quadratic: ~1.2 s of a 5,000-column mount. Rebuilt when the list changes.
163
+ let columnsById: Map<string, any> = new Map();
164
+ let columnsByIdFor: unknown = null;
165
+ function columnById(columnId: string) {
166
+ const all = ctx.grid.getAllColumns();
167
+ if (all !== columnsByIdFor) {
168
+ columnsById = new Map();
169
+ for (const c of all as any[]) columnsById.set(c.id, c);
170
+ columnsByIdFor = all;
171
+ }
172
+ return columnsById.get(columnId);
173
+ }
174
+
160
175
  /** Width before fitColumns scaling - what the columnDef/user actually set. */
161
176
  function getColumnBaseWidth(columnId: string) {
162
177
  if (ctx.columnWidths[columnId] !== undefined) return ctx.columnWidths[columnId];
163
- const column = ctx.grid.getAllColumns().find((c: any) => c.id === columnId);
178
+ const column = columnById(columnId);
164
179
  if (column?.columnDef.width !== undefined) return column.columnDef.width;
165
180
  return ctx.props.columnWidth ?? 140;
166
181
  }
@@ -386,10 +386,12 @@ export function resolveCellFormat<TData = unknown>(
386
386
  * on the same row (`compareColumn`) - those read the row, not column stats.
387
387
  */
388
388
  export function formatNeedsStats(f: ConditionalFormat<any>): boolean {
389
- return (
390
- (f.type === 'colorScale' || f.type === 'dataBar') &&
391
- !('compareColumn' in f && f.compareColumn)
392
- )
389
+ if (f.type !== 'colorScale' && f.type !== 'dataBar') return false
390
+ if ('compareColumn' in f && f.compareColumn) return false
391
+ // Both ends fixed in absolute units: the column's range is never read, so
392
+ // there is nothing to scan (and on a server-backed grid no way to scan it).
393
+ if (f.minValue != null && f.maxValue != null && (f.bounds ?? 'absolute') === 'absolute') return false
394
+ return true
393
395
  }
394
396
 
395
397
  export function formatsNeedingStats(
@@ -201,6 +201,67 @@ describe('createSortedRowModel - equivalence with the original comparator', () =
201
201
  }
202
202
  })
203
203
 
204
+ // From 20,000 rows a one-column numeric sort orders indices with a radix
205
+ // sort over the float bits (and ranked text with a counting sort) instead
206
+ // of a comparator. Same order, ties included.
207
+ describe('large numeric columns (the radix path)', () => {
208
+ const values: unknown[] = [0, -0, -1, 1, 2.5, -2.5, 1e9, -1e9, null, undefined, '', 'n/a', Infinity, -Infinity, 7, 7, 7, 0.1, -0.1, '12']
209
+ const big = (count: number): Row[] =>
210
+ Array.from({ length: count }, (_, i) => ({ text: 't', num: values[(i * 31) % values.length], when: null, tie: i }))
211
+ for (const desc of [false, true]) {
212
+ it(`matches the comparator sort, desc=${desc}`, () => {
213
+ const rows = big(25_000)
214
+ const sorting: SortingState = [{ id: 'num', desc }]
215
+ expectSameOrder(actualSort(rows, COLUMNS, sorting), referenceSort(rows, COLUMNS, sorting))
216
+ })
217
+ }
218
+ it('matches on date keys', () => {
219
+ const dates = ['2020-01-01', '2021-05-05', null, 'junk', 1600000000000, '1999-12-31', '2020-01-01']
220
+ const rows: Row[] = Array.from({ length: 25_000 }, (_, i) => ({ text: 't', num: 0, when: dates[(i * 13) % dates.length], tie: i }))
221
+ for (const desc of [false, true]) {
222
+ const sorting: SortingState = [{ id: 'when', desc }]
223
+ expectSameOrder(actualSort(rows, COLUMNS, sorting), referenceSort(rows, COLUMNS, sorting))
224
+ }
225
+ })
226
+ })
227
+
228
+ // Past 2,048 rows the choice is made from a sample. A 256-row sample used
229
+ // to miss repetition at this shape - every value a dozen times over, but
230
+ // spread thin enough that 256 rows hardly repeat - so a 1M-row column of
231
+ // 80k values was collated row by row (20M collator calls, 1.75 s).
232
+ describe('sampled columns (more rows than the sample)', () => {
233
+ const tag = (i: number) => `w${(i * 7919) % 2500}`
234
+ function countCollations<T>(run: () => T): { result: T; calls: number } {
235
+ const original = String.prototype.localeCompare
236
+ let calls = 0
237
+ String.prototype.localeCompare = function (this: string, ...args: Parameters<typeof original>) {
238
+ calls++
239
+ return original.apply(this, args)
240
+ }
241
+ try {
242
+ return { result: run(), calls }
243
+ } finally {
244
+ String.prototype.localeCompare = original
245
+ }
246
+ }
247
+
248
+ it('ranks a column of repeated values, and the order is unchanged', () => {
249
+ // 30,000 rows over 2,500 values, each about 12 times.
250
+ const rows: Row[] = Array.from({ length: 30_000 }, (_, i) => ({ text: tag(i), num: i, when: null, tie: 'x' }))
251
+ const sorting: SortingState = [{ id: 'text', desc: false }]
252
+ const { result, calls } = countCollations(() => actualSort(rows, COLUMNS, sorting))
253
+ expectSameOrder(result, referenceSort(rows, COLUMNS, sorting))
254
+ // Ranked: the 2,500 distinct values are collated, not the 30,000 rows.
255
+ expect(calls).toBeLessThan(rows.length)
256
+ })
257
+
258
+ it('collates a mostly-unique column row by row, in the same order', () => {
259
+ const rows: Row[] = Array.from({ length: 30_000 }, (_, i) => ({ text: `u${(i * 7919) % 29_000}`, num: i, when: null, tie: 'x' }))
260
+ const sorting: SortingState = [{ id: 'text', desc: true }]
261
+ expectSameOrder(actualSort(rows, COLUMNS, sorting), referenceSort(rows, COLUMNS, sorting))
262
+ })
263
+ })
264
+
204
265
  it('agrees with the direct path on the same data at both cardinalities', () => {
205
266
  // 22 rows over 11 values takes the rank path (11 * 2 <= 22); 20 rows over
206
267
  // the same 11 values does not (11 * 2 > 20). Same inputs, same order.