@svgrid/grid 3.0.3 → 4.0.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.
Files changed (318) hide show
  1. package/README.md +5 -4
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +122 -41
  8. package/dist/SvGrid.controller.svelte.js +845 -207
  9. package/dist/SvGrid.css +178 -2
  10. package/dist/SvGrid.svelte +467 -516
  11. package/dist/SvGrid.types.d.ts +315 -12
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +119 -28
  29. package/dist/cdn/GridMenus-CLI93jke.js +635 -0
  30. package/dist/cdn/{GridMenus-DhdPF700.js → GridMenus-CwooL3QA.js} +240 -207
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
  67. package/dist/cdn/{src-D6XvA1Ij.js → src-B6HXdfHY.js} +10448 -10049
  68. package/dist/cdn/{src-Bugf5XlI.js → src-C6Iowdvs.js} +6908 -6509
  69. package/dist/cdn/svgrid.js +27 -15
  70. package/dist/cdn/svgrid.svelte-external.js +27 -15
  71. package/dist/cdn/validate-CchXzyrX.js +76 -0
  72. package/dist/chart-axes.d.ts +53 -0
  73. package/dist/chart-axes.js +351 -0
  74. package/dist/chart-cartesian.d.ts +88 -0
  75. package/dist/chart-cartesian.js +1862 -0
  76. package/dist/chart-decimate.d.ts +51 -0
  77. package/dist/chart-decimate.js +199 -0
  78. package/dist/chart-export-pdf.d.ts +47 -0
  79. package/dist/chart-export-pdf.js +187 -0
  80. package/dist/chart-export.d.ts +2 -1
  81. package/dist/chart-export.js +18 -3
  82. package/dist/chart-financial.d.ts +91 -0
  83. package/dist/chart-financial.js +175 -0
  84. package/dist/chart-flow.d.ts +10 -0
  85. package/dist/chart-flow.js +298 -0
  86. package/dist/chart-format.d.ts +18 -0
  87. package/dist/chart-format.js +65 -0
  88. package/dist/chart-grid.d.ts +8 -0
  89. package/dist/chart-grid.js +190 -0
  90. package/dist/chart-hierarchy.d.ts +17 -0
  91. package/dist/chart-hierarchy.js +146 -0
  92. package/dist/chart-indicators.d.ts +115 -0
  93. package/dist/chart-indicators.js +389 -0
  94. package/dist/chart-messages.d.ts +95 -0
  95. package/dist/chart-messages.js +92 -0
  96. package/dist/chart-motion.d.ts +16 -0
  97. package/dist/chart-motion.js +141 -0
  98. package/dist/chart-panel-messages.d.ts +265 -0
  99. package/dist/chart-panel-messages.js +285 -0
  100. package/dist/chart-pivot.d.ts +87 -0
  101. package/dist/chart-pivot.js +156 -0
  102. package/dist/chart-polar.d.ts +25 -0
  103. package/dist/chart-polar.js +700 -0
  104. package/dist/chart-samples.d.ts +16 -0
  105. package/dist/chart-samples.js +153 -0
  106. package/dist/chart-scale.d.ts +113 -0
  107. package/dist/chart-scale.js +405 -0
  108. package/dist/chart-stats.d.ts +111 -0
  109. package/dist/chart-stats.js +419 -0
  110. package/dist/chart-stream.d.ts +45 -0
  111. package/dist/chart-stream.js +91 -0
  112. package/dist/chart-summary.d.ts +27 -0
  113. package/dist/chart-summary.js +205 -0
  114. package/dist/chart-sync.svelte.d.ts +32 -0
  115. package/dist/chart-sync.svelte.js +23 -0
  116. package/dist/chart-table.d.ts +35 -0
  117. package/dist/chart-table.js +165 -0
  118. package/dist/chart-trend.d.ts +19 -0
  119. package/dist/chart-trend.js +61 -0
  120. package/dist/chart-types.d.ts +1456 -0
  121. package/dist/chart-types.js +6 -0
  122. package/dist/chart-validate.d.ts +43 -0
  123. package/dist/chart-validate.js +272 -0
  124. package/dist/chart-zoom.d.ts +40 -0
  125. package/dist/chart-zoom.js +145 -0
  126. package/dist/chart.d.ts +186 -865
  127. package/dist/chart.js +460 -2204
  128. package/dist/clipboard.d.ts +1 -0
  129. package/dist/clipboard.js +137 -18
  130. package/dist/column-resize.d.ts +3 -0
  131. package/dist/column-resize.js +4 -1
  132. package/dist/columns.js +3 -0
  133. package/dist/command-context.d.ts +19 -0
  134. package/dist/command-context.js +116 -0
  135. package/dist/core.d.ts +7 -0
  136. package/dist/core.js +36 -13
  137. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  138. package/dist/editing.d.ts +2 -8
  139. package/dist/editing.js +272 -33
  140. package/dist/fill-patterns.d.ts +0 -5
  141. package/dist/fill-patterns.js +56 -2
  142. package/dist/grid-icons.d.ts +2 -2
  143. package/dist/grid-icons.js +2 -2
  144. package/dist/grid-messages.d.ts +12 -3
  145. package/dist/grid-messages.js +7 -0
  146. package/dist/history.d.ts +83 -0
  147. package/dist/history.js +133 -0
  148. package/dist/index.d.ts +27 -10
  149. package/dist/index.js +22 -5
  150. package/dist/keyboard-handlers.js +88 -31
  151. package/dist/keyboard.d.ts +55 -0
  152. package/dist/keyboard.js +132 -0
  153. package/dist/menus.d.ts +3 -1
  154. package/dist/menus.js +7 -2
  155. package/dist/merges.d.ts +97 -0
  156. package/dist/merges.js +147 -0
  157. package/dist/row-model.d.ts +133 -0
  158. package/dist/row-model.js +16 -0
  159. package/dist/row-resize.d.ts +3 -0
  160. package/dist/row-resize.js +14 -0
  161. package/dist/scroll-sync.d.ts +1 -0
  162. package/dist/scroll-sync.js +24 -0
  163. package/dist/selection.d.ts +18 -1
  164. package/dist/selection.js +138 -18
  165. package/dist/server-block-cache.d.ts +214 -0
  166. package/dist/server-block-cache.js +531 -0
  167. package/dist/server-data-source.d.ts +240 -6
  168. package/dist/server-data-source.js +220 -20
  169. package/dist/server.d.ts +13 -0
  170. package/dist/server.js +13 -0
  171. package/dist/shortcut-registry.d.ts +145 -0
  172. package/dist/shortcut-registry.js +44 -0
  173. package/dist/svgrid-wrapper.types.d.ts +213 -22
  174. package/dist/validate.d.ts +2 -0
  175. package/dist/validate.js +8 -3
  176. package/dist/virtualization/virtualizer.js +38 -17
  177. package/package.json +11 -1
  178. package/src/GridMenus.svelte +44 -2
  179. package/src/SvChartMenu.svelte +69 -0
  180. package/src/SvChartPanes.svelte +113 -0
  181. package/src/SvChartPanes.test.ts +66 -0
  182. package/src/SvGrid.controller.svelte.ts +818 -179
  183. package/src/SvGrid.css +178 -2
  184. package/src/SvGrid.svelte +467 -516
  185. package/src/SvGrid.types.ts +293 -11
  186. package/src/SvGridCellEditor.svelte +17 -0
  187. package/src/SvGridChart.svelte +2257 -329
  188. package/src/SvGridChart.test.ts +1419 -1
  189. package/src/SvGridChart.types.ts +336 -0
  190. package/src/SvGridChartBuilder.svelte +323 -0
  191. package/src/SvGridChartPanel.svelte +221 -275
  192. package/src/SvGridChartPickers.svelte +417 -0
  193. package/src/SvGridChartView.svelte +13 -14
  194. package/src/SvModal.svelte +7 -1
  195. package/src/SvModal.test.ts +9 -0
  196. package/src/ai.test.ts +57 -2
  197. package/src/ai.ts +166 -22
  198. package/src/aria.d.ts +11 -0
  199. package/src/build-api.coverage.test.ts +184 -0
  200. package/src/build-api.ts +118 -22
  201. package/src/chart-axes.test.ts +573 -0
  202. package/src/chart-axes.ts +342 -0
  203. package/src/chart-cartesian.ts +1863 -0
  204. package/src/chart-decimate.test.ts +249 -0
  205. package/src/chart-decimate.ts +186 -0
  206. package/src/chart-export-pdf.test.ts +91 -0
  207. package/src/chart-export-pdf.ts +207 -0
  208. package/src/chart-export.test.ts +14 -0
  209. package/src/chart-export.ts +13 -3
  210. package/src/chart-financial.ts +208 -0
  211. package/src/chart-flow.ts +286 -0
  212. package/src/chart-format.ts +67 -0
  213. package/src/chart-grid.ts +179 -0
  214. package/src/chart-hierarchy.ts +135 -0
  215. package/src/chart-indicators.test.ts +443 -0
  216. package/src/chart-indicators.ts +384 -0
  217. package/src/chart-messages.test.ts +21 -0
  218. package/src/chart-messages.ts +184 -0
  219. package/src/chart-motion.test.ts +71 -0
  220. package/src/chart-motion.ts +148 -0
  221. package/src/chart-panel-messages.test.ts +29 -0
  222. package/src/chart-panel-messages.ts +550 -0
  223. package/src/chart-pivot.test.ts +54 -0
  224. package/src/chart-pivot.ts +225 -0
  225. package/src/chart-polar.ts +674 -0
  226. package/src/chart-samples.ts +106 -0
  227. package/src/chart-scale.ts +394 -0
  228. package/src/chart-series-types.test.ts +714 -0
  229. package/src/chart-stats.ts +409 -0
  230. package/src/chart-stream.test.ts +75 -0
  231. package/src/chart-stream.ts +114 -0
  232. package/src/chart-summary.test.ts +90 -0
  233. package/src/chart-summary.ts +197 -0
  234. package/src/chart-sync.svelte.ts +41 -0
  235. package/src/chart-table.test.ts +76 -0
  236. package/src/chart-table.ts +178 -0
  237. package/src/chart-trend.ts +47 -0
  238. package/src/chart-types.ts +1309 -0
  239. package/src/chart-validate.test.ts +100 -0
  240. package/src/chart-validate.ts +247 -0
  241. package/src/chart-zoom.test.ts +137 -0
  242. package/src/chart-zoom.ts +124 -0
  243. package/src/chart.coverage.test.ts +81 -0
  244. package/src/chart.test.ts +56 -3
  245. package/src/chart.ts +557 -2784
  246. package/src/clipboard.test.ts +151 -0
  247. package/src/clipboard.ts +145 -16
  248. package/src/column-resize.ts +7 -1
  249. package/src/columns.ts +2 -0
  250. package/src/command-context.test.ts +200 -0
  251. package/src/command-context.ts +137 -0
  252. package/src/core.rowmodel-cache.test.ts +51 -0
  253. package/src/core.ts +40 -12
  254. package/src/editing.test.ts +281 -3
  255. package/src/editing.ts +264 -38
  256. package/src/fill-patterns.test.ts +35 -0
  257. package/src/fill-patterns.ts +56 -2
  258. package/src/grid-icons.ts +2 -2
  259. package/src/grid-messages.ts +21 -3
  260. package/src/history.test.ts +196 -0
  261. package/src/history.ts +162 -0
  262. package/src/icon-seam.test.ts +5 -6
  263. package/src/index.ts +169 -12
  264. package/src/keyboard-handlers.coverage.test.ts +126 -6
  265. package/src/keyboard-handlers.ts +91 -30
  266. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  267. package/src/keyboard.test.ts +124 -1
  268. package/src/keyboard.ts +142 -0
  269. package/src/menus.test.ts +26 -0
  270. package/src/menus.ts +10 -3
  271. package/src/merges.test.ts +112 -0
  272. package/src/merges.ts +168 -0
  273. package/src/pivot.grid.test.ts +40 -5
  274. package/src/row-model.ts +146 -0
  275. package/src/row-resize.test.ts +44 -0
  276. package/src/row-resize.ts +17 -0
  277. package/src/scroll-sync.test.ts +42 -1
  278. package/src/scroll-sync.ts +25 -0
  279. package/src/selection.multi-range.test.ts +34 -0
  280. package/src/selection.test.ts +33 -2
  281. package/src/selection.ts +143 -21
  282. package/src/server-block-cache.test.ts +645 -0
  283. package/src/server-block-cache.ts +677 -0
  284. package/src/server-data-source.infinite.test.ts +343 -0
  285. package/src/server-data-source.ts +450 -27
  286. package/src/server.ts +47 -0
  287. package/src/shortcut-registry.test.ts +97 -0
  288. package/src/shortcut-registry.ts +181 -0
  289. package/src/svgrid-wrapper.types.ts +184 -13
  290. package/src/svgrid.behavior.test.ts +135 -0
  291. package/src/svgrid.charting.test.ts +513 -3
  292. package/src/svgrid.context-menu.test.ts +31 -0
  293. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  294. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  295. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  296. package/src/validate.test.ts +10 -0
  297. package/src/validate.ts +10 -3
  298. package/src/virtualization/virtualizer.ts +39 -17
  299. package/dist/SvGroupCell.svelte +0 -141
  300. package/dist/SvGroupCell.svelte.d.ts +0 -49
  301. package/dist/SvRowGroupPanel.svelte +0 -186
  302. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  303. package/dist/cdn/GridMenus-CHOMnW-c.js +0 -602
  304. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  305. package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
  306. package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
  307. package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
  308. package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
  309. package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
  310. package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
  311. package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
  312. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  313. package/dist/server-group-model.d.ts +0 -98
  314. package/dist/server-group-model.js +0 -263
  315. package/src/SvGroupCell.svelte +0 -141
  316. package/src/SvRowGroupPanel.svelte +0 -186
  317. package/src/server-group-model.test.ts +0 -294
  318. package/src/server-group-model.ts +0 -370
@@ -25,7 +25,13 @@ import {
25
25
  type TableFeatures,
26
26
  type ChartType,
27
27
  type ChartSpec,
28
+ type ChartReducer,
29
+ type ChartTimeBucket,
28
30
  type ChartValueFormat,
31
+ type ChartZoomWindow,
32
+ type ChartPanelIndicator,
33
+ type ChartFormatState,
34
+ type OhlcColumns,
29
35
  } from "./index";
30
36
  import {
31
37
  createRowScrollScaling,
@@ -53,6 +59,8 @@ import {
53
59
  rawToNumber,
54
60
  } from "./SvGrid.helpers";
55
61
  import { createFeatures } from "./features";
62
+ import { pushHistory, type HistoryStep as SharedHistoryStep } from "./history";
63
+ import { buildMergeIndex } from "./merges";
56
64
  import {
57
65
  createScrollSync,
58
66
  } from "./scroll-sync";
@@ -301,6 +309,60 @@ export function createSvGridController<
301
309
  // preserves reference identity for non-overridden keys, which the columns-identity
302
310
  // column cache and the `data`/`columns` sync effects rely on.
303
311
  let optionOverrides = $state<Record<string, unknown>>({});
312
+
313
+ /**
314
+ * A `rowModel` answers for the props it can, so a server-backed grid is
315
+ * one prop rather than a dozen.
316
+ *
317
+ * Bumped by the model itself through `subscribe` (see the effect below),
318
+ * which is what makes `getRows()` and `isLoading()` - plain method calls,
319
+ * invisible to the reactive system - re-read when the model changes.
320
+ */
321
+ let rowModelVersion = $state(0);
322
+ const rowModelProps = $derived.by(function rowModelProps_d() {
323
+ const model = rawProps.rowModel;
324
+ if (!model) return null;
325
+ rowModelVersion; // read, so a notification re-derives
326
+ const paging = model.pagination;
327
+ return {
328
+ data: model.getRows(),
329
+ loading: model.isLoading(),
330
+ loadingOverlay: true,
331
+ getRowId: model.getRowId,
332
+ // A model that sorts or filters server-side is telling the grid not
333
+ // to do it locally; one that does neither leaves the grid in charge.
334
+ externalSort: model.setSort ? true : undefined,
335
+ externalFilter: model.setFilter ? true : undefined,
336
+ onSortingChange: model.setSort ? (s: any) => model.setSort!(s) : undefined,
337
+ onFiltersChange: model.setFilter ? (f: any) => model.setFilter!(f) : undefined,
338
+ onVisibleRangeChange: model.setViewport
339
+ ? (r: { startIndex: number; endIndex: number }) =>
340
+ model.setViewport!(r.startIndex, r.endIndex)
341
+ : undefined,
342
+ rowPlaceholder: model.rowPlaceholder,
343
+ onRetryRow: model.retryRow,
344
+ serverGroup: model.group,
345
+ rowSelectionModel: model.selection,
346
+ serverFilterValues: model.filterValues
347
+ ? (columnId: string) => model.filterValues!(columnId)
348
+ : undefined,
349
+ pinnedTopRows: model.pinnedTopRows,
350
+ pinnedBottomRows: model.pinnedBottomRows,
351
+ pivotResultColumns: model.pivotResultColumns ?? undefined,
352
+ externalPagination: paging ? true : undefined,
353
+ rowCount: paging?.rowCount,
354
+ pageIndex: paging?.pageIndex,
355
+ pageSize: paging?.pageSize,
356
+ pageSizeOptions: paging?.pageSizes,
357
+ onPaginationChange: paging
358
+ ? (p: { pageIndex: number; pageSize: number }) => {
359
+ if (p.pageSize !== paging.pageSize) paging.setPageSize(p.pageSize);
360
+ else paging.setPage(p.pageIndex);
361
+ }
362
+ : undefined,
363
+ } as Partial<Props<TFeatures, TData>>;
364
+ });
365
+
304
366
  const props = new Proxy(rawProps, {
305
367
  // Read the override as a PROPERTY (tracked by `$state`), not via `in` (the `has`
306
368
  // trap is not tracked) - so a reactive read of `props.X` re-runs when an override
@@ -310,7 +372,21 @@ export function createSvGridController<
310
372
  get: (t, k) => {
311
373
  const v =
312
374
  typeof k === "string" ? optionOverrides[k] : undefined;
313
- return v !== undefined ? v : Reflect.get(t, k);
375
+ if (v !== undefined) return v;
376
+ // Pivot mode swaps the column list: `pivotResultColumns` (the prop, or
377
+ // the row model's) stands in for `columns` while it is set, and
378
+ // `columns` itself stays the app's own list for when pivot ends.
379
+ if (k === "columns") {
380
+ const pivot =
381
+ Reflect.get(t, "pivotResultColumns") ??
382
+ (rowModelProps as Record<string, unknown> | null)?.pivotResultColumns;
383
+ if (pivot) return pivot;
384
+ }
385
+ const own = Reflect.get(t, k);
386
+ // A prop written on the grid always beats the row model, so adopting
387
+ // `rowModel` is never all-or-nothing.
388
+ if (own !== undefined || typeof k !== "string") return own;
389
+ return (rowModelProps as Record<string, unknown> | null)?.[k];
314
390
  },
315
391
  }) as Props<TFeatures, TData>;
316
392
 
@@ -373,7 +449,12 @@ export function createSvGridController<
373
449
  let pendingScrollTop = 0;
374
450
  let pendingScrollLeft = 0;
375
451
  let scrollSyncRaf: number | null = null;
376
- let selectionRange = $state<SelectionRange>({ anchor: null, focus: null });
452
+ // `$state.raw`: every writer replaces the whole object, and every rendered
453
+ // cell's selection derived reads it. Under a deep proxy each of those ~300
454
+ // deriveds tracked the nested anchor/focus row/col signals separately (six
455
+ // dependencies per cell to mark and re-check on every arrow key) where one
456
+ // will do.
457
+ let selectionRange = $state.raw<SelectionRange>({ anchor: null, focus: null });
377
458
  // Extra committed ranges for multi-range (Ctrl+drag) selection. The
378
459
  // `selectionRange` above is always the ACTIVE range being manipulated; these
379
460
  // are the finished ones. Full selection = these + the active range.
@@ -418,20 +499,21 @@ export function createSvGridController<
418
499
  // Full-row editing: the row currently in whole-row edit + its per-column
419
500
  // draft (keyed by column id). Null when not in full-row mode.
420
501
  let fullRowEdit = $state<{ rowId: string; draft: Record<string, unknown> } | null>(null);
421
- let editedCellValues = $state<Record<string, unknown>>({});
502
+ // `$state.raw`, not `$state`: every writer replaces the whole object, and
503
+ // the render path asks `key in editedCellValues` once per rendered cell. On
504
+ // a deep proxy that `in` check, run inside an effect, creates and keeps a
505
+ // per-key signal for every cell that ever scrolls into view - a leak that
506
+ // grows with rows x columns and a Map lookup per cell per render.
507
+ let editedCellValues = $state.raw<Record<string, unknown>>({});
422
508
 
423
509
  // ---- Undo / redo (history + pointer model) ---------------------------
424
510
  // VSCode-style: one ordered history array, plus a pointer to the index
425
511
  // of the NEXT undo step. Avoids the dual-stack edge cases where
426
512
  // multiple undo-redo cycles can lose entries.
427
- // exported for the editing slice (undo/redo)
428
- type HistoryStep = {
429
- rowId: string
430
- columnId: string
431
- field: string
432
- before: unknown
433
- after: unknown
434
- }
513
+ // exported for the editing slice (undo/redo). The shape lives in history.ts
514
+ // so the controller, editing.ts and clipboard.ts cannot drift apart - they
515
+ // previously declared it twice and open-coded the push in three places.
516
+ type HistoryStep = SharedHistoryStep
435
517
  const UNDO_LIMIT = 200
436
518
  let history = $state<HistoryStep[]>([])
437
519
  /** Index in `history` of the LAST applied step. -1 means "nothing applied".
@@ -441,6 +523,11 @@ export function createSvGridController<
441
523
  /** Bumps on every undo / redo / record so $derived consumers can
442
524
  * observe via the api without subscribing to history directly. */
443
525
  let historyVersion = $state(0)
526
+ /** Set while a `runHistoryGroup` call is on the stack, so every step pushed
527
+ * inside one action shares a group id and undoes together. Deliberately not
528
+ * $state: it is written and read synchronously inside a single call and
529
+ * nothing renders from it. */
530
+ let historyGroupId: string | undefined = undefined
444
531
 
445
532
  // ---- Hover tooltip (custom popover, not native title=) ---------------
446
533
  // Triggered by per-column `tooltip` field OR per-cell `notes` prop.
@@ -479,6 +566,24 @@ export function createSvGridController<
479
566
  let editorSelectAll = true;
480
567
  /** Per-column width overrides set by the resize handles. */
481
568
  let columnWidths = $state<Record<string, number>>({});
569
+ /**
570
+ * Columns folded to nothing, the way a sheet hides a column: the column
571
+ * keeps its index, its cells and its width for later, and takes no room.
572
+ * Distinct from `hiddenColumns`, which takes a column out of the model
573
+ * and shifts every index after it; a spreadsheet's formulas and formats
574
+ * are keyed by index and could not follow that.
575
+ */
576
+ let collapsedColumns = $state<Record<string, boolean>>({});
577
+ function setColumnCollapsed(columnId: string, collapsed: boolean): void {
578
+ if (!!collapsedColumns[columnId] === collapsed) return;
579
+ if (collapsed) {
580
+ collapsedColumns = { ...collapsedColumns, [columnId]: true };
581
+ } else {
582
+ const next = { ...collapsedColumns };
583
+ delete next[columnId];
584
+ collapsedColumns = next;
585
+ }
586
+ }
482
587
  const MIN_COLUMN_WIDTH = 40;
483
588
  /** Columns pinned to the left or right edge of the grid (sticky positioning).
484
589
  * Seeded from `props.initialColumnPinning` so demos / tests can show the
@@ -570,26 +675,54 @@ export function createSvGridController<
570
675
  const hb = column.columnDef?.hideBelow;
571
676
  return hb != null && viewportWidth > 0 && viewportWidth < hb;
572
677
  }
573
- const scrollMetrics = $derived.by(function scrollMetrics_d() {
678
+ /**
679
+ * Live scroll geometry of the scroll container (feeds the custom
680
+ * scrollbars and `hasVerticalOverflow`).
681
+ *
682
+ * Measured from a plain `$effect`, NOT inside a `$derived`, on purpose.
683
+ * `scrollTop` / `scrollHeight` / `scrollWidth` reads force a synchronous
684
+ * layout whenever the tree is dirty. As a derived this was pulled by the
685
+ * shell's `class:sv-grid-has-vscroll` at the very start of every scroll
686
+ * flush - before the recycled rows had been rewritten - so it forced a
687
+ * full table layout that the row writes then invalidated again, and the
688
+ * browser laid the table out a second time before painting. Two table
689
+ * layouts per arrow key on a large grid. An effect runs after the flush
690
+ * has written the DOM, so the reads below land on a tree the browser has
691
+ * to lay out anyway, and the pre-paint pass is incremental.
692
+ *
693
+ * The version reads keep the same triggers the derived had: a scroll
694
+ * tick, a shell resize, and either virtualizer's window/size change (data
695
+ * load, row / column count change) - without those the scrollbar kept a
696
+ * stale `content-size` ≈ 0, its hidden-check tripped, it set
697
+ * `pointer-events: none`, and the user could not drag it.
698
+ */
699
+ let scrollMetrics = $state.raw({
700
+ scrollTop: 0,
701
+ scrollLeft: 0,
702
+ scrollHeight: 0,
703
+ scrollWidth: 0,
704
+ });
705
+ $effect(function scrollMetrics_e() {
574
706
  scrollVersion;
575
707
  viewportVersion;
576
- // Track the virtualizers' versions too so when data loads or row /
577
- // column counts change, scrollMetrics re-reads the DOM's grown
578
- // scrollHeight / scrollWidth. Without these deps the scrollbar
579
- // receives a stale `content-size` ≈ 0, its hidden-check trips, it
580
- // sets `pointer-events: none`, and the user can't drag it. The
581
- // identifiers below are declared further down - derived callbacks
582
- // run lazily, so by the time this fires they're in scope.
583
708
  virtualizer.version;
584
709
  columnVirtualizerVersion;
585
- return {
586
- scrollTop: scrollContainer?.scrollTop ?? 0,
587
- scrollLeft: scrollContainer?.scrollLeft ?? 0,
588
- clientHeight: scrollContainer?.clientHeight ?? 0,
589
- clientWidth: scrollContainer?.clientWidth ?? 0,
590
- scrollHeight: scrollContainer?.scrollHeight ?? 0,
591
- scrollWidth: scrollContainer?.scrollWidth ?? 0,
592
- };
710
+ const el = scrollContainer;
711
+ if (!el) return;
712
+ const scrollTop = el.scrollTop;
713
+ const scrollLeft = el.scrollLeft;
714
+ const scrollHeight = el.scrollHeight;
715
+ const scrollWidth = el.scrollWidth;
716
+ const prev = untrack(() => scrollMetrics);
717
+ if (
718
+ prev.scrollTop === scrollTop &&
719
+ prev.scrollLeft === scrollLeft &&
720
+ prev.scrollHeight === scrollHeight &&
721
+ prev.scrollWidth === scrollWidth
722
+ ) {
723
+ return;
724
+ }
725
+ scrollMetrics = { scrollTop, scrollLeft, scrollHeight, scrollWidth };
593
726
  });
594
727
  /** Vertical overflow from the virtualizer's authoritative total size,
595
728
  * NOT from `scrollMetrics.scrollHeight` alone. Reading DOM dimensions
@@ -674,7 +807,10 @@ export function createSvGridController<
674
807
  // re-synced whenever the parent passes a new array; the imperative API
675
808
  // mutates these so add/remove operations don't need a callback round-trip.
676
809
  // svelte-ignore state_referenced_locally
677
- let internalData = $state.raw<ReadonlyArray<TData>>(props.data);
810
+ // `data` is optional on the type (a `rowModel` may supply the rows
811
+ // instead), so every read falls back to empty rather than to undefined.
812
+ const EMPTY_DATA: ReadonlyArray<TData> = [];
813
+ let internalData = $state.raw<ReadonlyArray<TData>>(props.data ?? EMPTY_DATA);
678
814
  // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
679
815
  // format defaults once, up front, so every downstream reader sees a normal
680
816
  // column. Explicit fields on the ColumnDef always win.
@@ -726,7 +862,7 @@ export function createSvGridController<
726
862
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
727
863
  // would keep returning the old edited values from `editedCellValues`
728
864
  // even though the underlying data has been replaced.
729
- internalData = props.data;
865
+ internalData = props.data ?? EMPTY_DATA;
730
866
  editedCellValues = {};
731
867
  });
732
868
  $effect(() => {
@@ -972,6 +1108,20 @@ export function createSvGridController<
972
1108
  *
973
1109
  * Returns null when the cell should render nothing.
974
1110
  */
1111
+ /**
1112
+ * Why a row has no data yet, or null when it does.
1113
+ *
1114
+ * Group rows are never placeholders, so the check is skipped for them:
1115
+ * their `original` is a synthetic bucket, not a row the consumer handed
1116
+ * us, and passing it to `rowPlaceholder` would ask a question about a row
1117
+ * that does not exist.
1118
+ */
1119
+ function placeholderStateOf(row: Row<TData>): "loading" | "failed" | null {
1120
+ const ask = props.rowPlaceholder;
1121
+ if (!ask || isGroupRow(row)) return null;
1122
+ return ask(row.original, row.index);
1123
+ }
1124
+
975
1125
  function autoGroupCell(
976
1126
  row: Row<TData>,
977
1127
  columnId: string,
@@ -1697,8 +1847,14 @@ export function createSvGridController<
1697
1847
  const paginationPageSize = $derived(
1698
1848
  externalPaginationEnabled ? (props.pageSize ?? 10) : paginationState.pageSize,
1699
1849
  );
1850
+ // Tracks `dataStateVersion`, which the store subscription bumps when the
1851
+ // rowSelection slice (among others) changes - NOT the catch-all
1852
+ // `gridStateVersion`, which also bumps on every active-cell move. Every
1853
+ // rendered row and cell reads this (`sv-grid-row-selected`, `aria-selected`),
1854
+ // so on the catch-all each arrow key re-marked and re-checked ~400 effects
1855
+ // to find the same object.
1700
1856
  const rowSelectionState = $derived.by(function rowSelectionState_d() {
1701
- gridStateVersion;
1857
+ dataStateVersion;
1702
1858
  return grid.getState().rowSelection ?? {};
1703
1859
  });
1704
1860
 
@@ -1734,15 +1890,42 @@ export function createSvGridController<
1734
1890
  const rows: TData[] = [];
1735
1891
  const ids: string[] = [];
1736
1892
  if (!selectionBarOn) return { rows, ids };
1893
+ // Under an external selection model the loaded rows are the ones the
1894
+ // model says are selected; rows that never loaded are not in `allRows`
1895
+ // and are counted by `selectionBarCount` below instead. This walks
1896
+ // every loaded row, so it runs on every block a row model delivers:
1897
+ // a model that can say "nothing is selected" spares the walk, and the
1898
+ // props it needs are read once, not through the proxy per row.
1899
+ const model = props.rowSelectionModel;
1900
+ if (model && model.selectedCount?.() === 0) return { rows, ids };
1901
+ const ask = props.rowPlaceholder;
1902
+ const isPlaceholder = ask
1903
+ ? (row: Row<TData>) => !isGroupRow(row) && !!ask(row.original as TData, row.index)
1904
+ : () => false;
1737
1905
  for (const row of allRows) {
1738
- if (isGroupRow(row)) continue;
1739
- if (rowSelectionState[row.id]) {
1906
+ if (isGroupRow(row) || isPlaceholder(row)) continue;
1907
+ const selected = model
1908
+ ? model.isSelected(row.id, row.original as TData)
1909
+ : !!rowSelectionState[row.id];
1910
+ if (selected) {
1740
1911
  rows.push(row.original as TData);
1741
1912
  ids.push(row.id);
1742
1913
  }
1743
1914
  }
1744
1915
  return { rows, ids };
1745
1916
  });
1917
+ /**
1918
+ * What the selection bar's count chip says. A model that stores the rule
1919
+ * ("everything except these") knows about rows the grid never loaded, so
1920
+ * its number wins over counting ticks on screen; `null` from it means it
1921
+ * cannot say, and the bar falls back to the loaded count.
1922
+ */
1923
+ const selectionBarCount = $derived.by(function selectionBarCount_d() {
1924
+ const fromModel = props.rowSelectionModel?.selectedCount?.();
1925
+ return fromModel ?? selectionBarTarget.ids.length;
1926
+ });
1927
+ /** Whether the bar has anything to show: the model's count when it has one, else the loaded ticks. */
1928
+ const selectionBarVisible = $derived(selectionBarOn && selectionBarCount > 0);
1746
1929
 
1747
1930
  // Forward selection changes to the consumer. Skips the very first invocation
1748
1931
  // (the initial empty state) so consumers don't get a spurious callback on mount.
@@ -2017,6 +2200,7 @@ export function createSvGridController<
2017
2200
  let chartAiHandler = $state<
2018
2201
  ((prompt: string) => Promise<Record<string, unknown> | null>) | null
2019
2202
  >(null);
2203
+ let chartExplainHandler = $state<(() => Promise<{ summary: string; insights: string[] } | null>) | null>(null);
2020
2204
 
2021
2205
  // Multiple charts, each an independently-configured working set switched by a
2022
2206
  // tab strip. Pickers / spec read the ACTIVE chart.
@@ -2024,7 +2208,7 @@ export function createSvGridController<
2024
2208
  id: string;
2025
2209
  title: string;
2026
2210
  type: ChartType;
2027
- reduce: "sum" | "avg" | "count";
2211
+ reduce: ChartReducer;
2028
2212
  dimensionId: string | null;
2029
2213
  measureId: string | null;
2030
2214
  seriesId: string | null | undefined;
@@ -2045,8 +2229,28 @@ export function createSvGridController<
2045
2229
  logScale: boolean | null;
2046
2230
  timeAxis: boolean | null;
2047
2231
  valueFormat: ChartValueFormat | null;
2232
+ /** Calendar unit a date dimension is grouped by. Null = exact values. */
2233
+ bucket: ChartTimeBucket | null;
2234
+ /** Histogram bin count. Null = Sturges' rule. */
2235
+ bins: number | null;
2236
+ funnelShape: "trapezoid" | "pyramid" | "cone" | null;
2237
+ candleStyle: "classic" | "hollow" | "heikin-ashi" | null;
2238
+ /** The chart's zoom window (category index range), kept per tab. */
2239
+ zoom: ChartZoomWindow | null;
2240
+ /** Candlestick / OHLC: which columns hold the prices (column ids). Null
2241
+ * guesses from the column names. */
2242
+ ohlc: { open: string | null; high: string | null; low: string | null; close: string | null; volume: string | null } | null;
2243
+ /** Indicators: panes under the price (volume, rsi, macd, stochastic,
2244
+ * atr, obv) and overlays on it (sma, ema, bb, vwap). */
2245
+ indicators: ChartPanelIndicator[];
2246
+ /** The builder's Format tab: titles, axes, legend, per-series style. */
2247
+ format: ChartFormatState | null;
2248
+ /** An unlinked chart: the spec it was frozen with and when. */
2249
+ frozen: { spec: ChartSpec; at: string } | null;
2048
2250
  };
2049
2251
  let chartSeq = 0;
2252
+ const isZoomWindow = (v: unknown): v is ChartZoomWindow =>
2253
+ !!v && typeof v === "object" && Number.isInteger((v as ChartZoomWindow).i0) && Number.isInteger((v as ChartZoomWindow).i1);
2050
2254
  const makeChart = (title: string): ChartTabState => ({
2051
2255
  id: `chart-${chartSeq++}`,
2052
2256
  title,
@@ -2067,10 +2271,49 @@ export function createSvGridController<
2067
2271
  logScale: null,
2068
2272
  timeAxis: null,
2069
2273
  valueFormat: null,
2274
+ bucket: null,
2275
+ bins: null,
2276
+ funnelShape: null,
2277
+ candleStyle: null,
2278
+ zoom: null,
2279
+ ohlc: null,
2280
+ indicators: [],
2281
+ format: null,
2282
+ frozen: null,
2070
2283
  });
2284
+ // The saved view (`GridState.charts[i]`) names four fields differently
2285
+ // from the tab; everything else is the tab's own key.
2286
+ const TAB_ALIASES: Record<string, string> = { dimensionId: "dimension", measureId: "measure", seriesId: "series", measure2Id: "measure2" };
2287
+ /** A tab as the saved view carries it, without the runtime id. */
2288
+ const tabToState = (c: ChartTabState): Record<string, unknown> => {
2289
+ const out: Record<string, unknown> = {};
2290
+ for (const k in c) if (k !== "id") out[TAB_ALIASES[k] ?? k] = k === "seriesId" ? (c.seriesId ?? null) : c[k as keyof ChartTabState];
2291
+ return out;
2292
+ };
2293
+ /** A tab from a saved-view snapshot: a fresh tab with every known field
2294
+ * the snapshot carries, and the shape-checked ones dropped when malformed. */
2295
+ const tabFromState = (c: Record<string, unknown>, i: number): ChartTabState => {
2296
+ const t = makeChart(typeof c.title === "string" ? c.title : `Chart ${i + 1}`) as unknown as Record<string, unknown>;
2297
+ for (const k in t) {
2298
+ const v = c[TAB_ALIASES[k] ?? k];
2299
+ if (k !== "id" && k !== "title" && v !== undefined) t[k] = v;
2300
+ }
2301
+ if (!isZoomWindow(t.zoom)) t.zoom = null;
2302
+ if (!Array.isArray(t.indicators)) t.indicators = [];
2303
+ if (typeof t.bins !== "number") t.bins = null;
2304
+ if (!t.frozen || typeof t.frozen !== "object" || !(t.frozen as { spec?: unknown }).spec) t.frozen = null;
2305
+ return t as unknown as ChartTabState;
2306
+ };
2071
2307
  // svelte-ignore state_referenced_locally
2072
2308
  let charts = $state<ChartTabState[]>([makeChart("Chart 1")]);
2073
2309
  let activeChartIndex = $state(0);
2310
+ /**
2311
+ * Named chart configurations the user keeps: a tab's whole state under a
2312
+ * name, applied to any tab later. Part of the saved view (`GridState.savedCharts`)
2313
+ * only when there is one, so existing snapshots do not change.
2314
+ */
2315
+ type SavedChartEntry = { name: string; savedAt: string; tab: Record<string, unknown> };
2316
+ let savedCharts = $state<SavedChartEntry[]>([]);
2074
2317
  const activeChart = $derived(
2075
2318
  (charts[Math.min(activeChartIndex, charts.length - 1)] ?? charts[0])!,
2076
2319
  );
@@ -2083,6 +2326,10 @@ export function createSvGridController<
2083
2326
  const chartDataLabels = $derived(activeChart.dataLabels);
2084
2327
  const chartLogScale = $derived(activeChart.logScale);
2085
2328
  const chartTimeAxis = $derived(activeChart.timeAxis);
2329
+ const chartBucket = $derived(activeChart.bucket);
2330
+ const chartBins = $derived(activeChart.bins);
2331
+ const chartFunnelShape = $derived(activeChart.funnelShape);
2332
+ const chartCandleStyle = $derived(activeChart.candleStyle);
2086
2333
  const chartValueFormat = $derived(activeChart.valueFormat);
2087
2334
 
2088
2335
  const asMeasureList = (m: string | string[] | undefined): string[] =>
@@ -2198,9 +2445,13 @@ export function createSvGridController<
2198
2445
  const col = allColumns.find((c) => c.id === effectiveChartMeasureId);
2199
2446
  const label = col ? columnLabel(col) : "";
2200
2447
  if (!label) return undefined;
2201
- return chartReduce === "count"
2202
- ? "Count"
2203
- : `${chartReduce === "avg" ? "Average" : "Sum"} of ${label}`;
2448
+ const word: Record<string, string> = {
2449
+ sum: "Sum", avg: "Average", min: "Min", max: "Max", median: "Median",
2450
+ first: "First", last: "Last", countDistinct: "Distinct",
2451
+ };
2452
+ if (chartReduce === "count") return "Count";
2453
+ const w = word[chartReduce] ?? (chartReduce.startsWith("p") ? `P${chartReduce.slice(1)}` : "Sum");
2454
+ return `${w} of ${label}`;
2204
2455
  });
2205
2456
  /**
2206
2457
  * Does this column hold dates? Declared type first, then a probe of the
@@ -2223,6 +2474,41 @@ export function createSvGridController<
2223
2474
  const chartDimensionIsDate = $derived.by<boolean>(() =>
2224
2475
  columnIsDate(allColumns.find((c) => c.id === effectiveChartDimensionId)),
2225
2476
  );
2477
+ // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
2478
+ // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
2479
+ // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
2480
+ // null until it resolves, a tick after the panel itself starts loading (which it
2481
+ // already does). The `getAggregate` server path uses the local bucketsToSpec and
2482
+ // does not need the engine.
2483
+ // The whole module, not just `rowsToChartSpec`: the panel can now reach
2484
+ // every chart type, and the ones that do not read a categories-by-series
2485
+ // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
2486
+ // from the same lazy chunk. Same `import()`, same boundary.
2487
+ let chartEngine = $state<typeof import("./chart") | null>(null);
2488
+ $effect(() => {
2489
+ if (chartingEnabled && !chartEngine)
2490
+ import("./chart").then((m) => (chartEngine = m));
2491
+ });
2492
+
2493
+ const chartIsOhlc = $derived(chartType === "candlestick" || chartType === "ohlc");
2494
+ /** The price columns: what the reader picked, else the engine's guess
2495
+ * from the column names (nothing until the engine chunk has loaded). */
2496
+ const effectiveChartOhlc = $derived<OhlcColumns>(
2497
+ chartEngine
2498
+ ? chartEngine.guessOhlcColumns(chartableColumns.measures, activeChart.ohlc)
2499
+ : { open: null, high: null, low: null, close: null, volume: null, ...activeChart.ohlc },
2500
+ );
2501
+ /** The date column a candlestick reads: the picked dimension when it is a
2502
+ * date, else the first date column. */
2503
+ const effectiveChartOhlcDateId = $derived(
2504
+ chartDimensionIsDate ? effectiveChartDimensionId : (chartableColumns.dates[0]?.id ?? null),
2505
+ );
2506
+ const chartIndicators = $derived(activeChart.indicators);
2507
+ /** The panes under a price chart and the overlay on it, split by the engine. */
2508
+ const chartIndicatorSplit = $derived(
2509
+ chartIsOhlc && chartEngine ? chartEngine.splitPanelIndicators(chartIndicators) : { panes: [], overlay: null },
2510
+ );
2511
+ const chartIndicatorPanes = $derived(chartIndicatorSplit.panes);
2226
2512
  const chartRows = $derived.by<TData[]>(() => {
2227
2513
  const rects = getSelectionRects();
2228
2514
  const pushLeaf = (out: TData[], row: Row<TData> | undefined) => {
@@ -2272,52 +2558,6 @@ export function createSvGridController<
2272
2558
  if (seq === chartReqSeq) chartServerBuckets = [];
2273
2559
  });
2274
2560
  });
2275
- const bucketsToSpec = (
2276
- buckets: ReadonlyArray<{ category: string; series?: string; value: number }>,
2277
- type: ChartType,
2278
- hasSeries: boolean,
2279
- ): ChartSpec => {
2280
- const cats: string[] = [];
2281
- const catIdx = new Map<string, number>();
2282
- const ensureCat = (c: string) => {
2283
- let i = catIdx.get(c);
2284
- if (i === undefined) { i = cats.length; catIdx.set(c, i); cats.push(c); }
2285
- return i;
2286
- };
2287
- const seriesMap = new Map<string, number[]>();
2288
- for (const b of buckets) {
2289
- const ci = ensureCat(b.category);
2290
- const key = hasSeries ? (b.series ?? "") : "value";
2291
- let arr = seriesMap.get(key);
2292
- if (!arr) { arr = []; seriesMap.set(key, arr); }
2293
- arr[ci] = (arr[ci] ?? 0) + b.value;
2294
- }
2295
- return {
2296
- type,
2297
- categories: cats,
2298
- series: [...seriesMap.entries()].map(([label, values]) => ({
2299
- label,
2300
- values: cats.map((_, i) => values[i] ?? 0),
2301
- })),
2302
- };
2303
- };
2304
-
2305
- // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
2306
- // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
2307
- // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
2308
- // null until it resolves, a tick after the panel itself starts loading (which it
2309
- // already does). The `getAggregate` server path uses the local bucketsToSpec and
2310
- // does not need the engine.
2311
- // The whole module, not just `rowsToChartSpec`: the panel can now reach
2312
- // every chart type, and the ones that do not read a categories-by-series
2313
- // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
2314
- // from the same lazy chunk. Same `import()`, same boundary.
2315
- let chartEngine = $state<typeof import("./chart") | null>(null);
2316
- $effect(() => {
2317
- if (chartingEnabled && !chartEngine)
2318
- import("./chart").then((m) => (chartEngine = m));
2319
- });
2320
-
2321
2561
  /** Put the chart's numbers in the same locale as the grid's. A grid that set
2322
2562
  * `localization.locale` has already said which locale its data is in, and a
2323
2563
  * chart of that data reading in a different one is a bug nobody would think
@@ -2329,27 +2569,52 @@ export function createSvGridController<
2329
2569
  if (chartCfg?.currency) spec.currency = chartCfg.currency;
2330
2570
  };
2331
2571
 
2332
- const chartSpec = $derived.by<ChartSpec | null>(() => {
2572
+ const chartSpecLive = $derived.by<ChartSpec | null>(() => {
2333
2573
  if (!chartingEnabled || !chartCfg) return null;
2334
2574
  if (chartCfg.getAggregate) {
2335
- if (!chartServerBuckets) return null;
2336
- const spec = bucketsToSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
2575
+ if (!chartServerBuckets || !chartEngine) return null;
2576
+ const spec = chartEngine.bucketsToChartSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
2337
2577
  if (effectiveChartStacked || chartCfg.stacked100) spec.stacked = true;
2338
2578
  if (chartCfg.stacked100) spec.stacked100 = true;
2339
2579
  if (chartCfg.palette) spec.palette = chartCfg.palette;
2340
2580
  if (effectiveChartTimeAxis) spec.xType = "time";
2581
+ else if (chartBucket && chartDimensionIsDate) spec.xType = "ordinal-time";
2341
2582
  if (effectiveChartLogScale) spec.yScale = "log";
2342
2583
  if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
2343
2584
  applyChartLocale(spec);
2344
2585
  return spec;
2345
2586
  }
2346
2587
  if (chartCfg.buildSpec) return chartCfg.buildSpec(chartRows) ?? null;
2588
+ // Engine not loaded yet (lazy): render nothing until it resolves.
2589
+ if (!chartEngine) return null;
2590
+ // Shared by the direct and aggregated paths so the two cannot drift apart
2591
+ // again - which is how these came to be skipped for scatter, gauge and
2592
+ // boxplot in the first place. Declared here, at the derived's own scope:
2593
+ // a function declaration inside the `else` block below is block-scoped,
2594
+ // and the aggregated path calls it after that block has closed.
2595
+ function applyChartValueSettings<T extends {
2596
+ xType?: string; yScale?: string; valueFormat?: unknown;
2597
+ }>(spec: T): T {
2598
+ if (effectiveChartTimeAxis) spec.xType = "time";
2599
+ if (effectiveChartLogScale) spec.yScale = "log";
2600
+ if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
2601
+ applyChartLocale(spec as never);
2602
+ return spec;
2603
+ }
2347
2604
 
2605
+ // Pivot mode: the pivot on screen is the chart, rows as categories and
2606
+ // columns as series; the pickers have nothing to choose.
2607
+ let spec: ChartSpec;
2608
+ if (pivotResult) {
2609
+ spec = chartEngine.pivotResultToChartSpec(pivotResult, {
2610
+ type: chartType, stacked: effectiveChartStacked, stacked100: effectiveChartStacked100,
2611
+ measures: pivotConfig!.values,
2612
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2613
+ });
2614
+ } else {
2348
2615
  const category = fieldOf(effectiveChartDimensionId);
2349
2616
  const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f): f is string => !!f);
2350
- if (!category || !values.length) return null;
2351
- // Engine not loaded yet (lazy): render nothing until it resolves.
2352
- if (!chartEngine) return null;
2617
+ if (!chartIsOhlc && (!category || !values.length)) return null;
2353
2618
  const seriesField = fieldOf(effectiveChartSeriesId);
2354
2619
  const rowsForChart = chartRows as Array<Record<string, unknown>>;
2355
2620
 
@@ -2360,26 +2625,51 @@ export function createSvGridController<
2360
2625
  // base bundle, which every grid pays for whether or not it ever charts.
2361
2626
  // Everything else falls through to the aggregation below and is reshaped
2362
2627
  // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
2628
+ const ohlcOpts = chartIsOhlc
2629
+ ? chartEngine.ohlcDirectOptions(fieldOf, effectiveChartOhlcDateId, effectiveChartOhlc, { bucket: chartBucket, candleStyle: chartCandleStyle })
2630
+ : null;
2363
2631
  const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
2364
- category: category as string,
2632
+ ...(ohlcOpts ?? {}),
2633
+ category: ohlcOpts?.category ?? (category as string),
2365
2634
  ...(values[0] ? { value: values[0] } : {}),
2366
2635
  ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id)! } : {}),
2367
2636
  ...(seriesField ? { series: seriesField } : {}),
2368
2637
  reduce: chartReduce,
2638
+ ...(chartBins ? { bins: chartBins } : {}),
2369
2639
  ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2370
2640
  });
2371
- if (direct) return direct;
2641
+ // Number formatting and the scale/axis-type settings are about how values
2642
+ // READ, so they apply to a scatter or a gauge exactly as they do to a bar
2643
+ // chart. They used to live only on the aggregated path below, so setting
2644
+ // `valueFormat` or `logScale` on a direct type silently did nothing.
2645
+ //
2646
+ // The series-shaped settings (trend, averageLine, seriesTypes) stay on the
2647
+ // aggregated path: a direct spec does not have the series shape they read.
2648
+ if (direct) {
2649
+ if (chartIsOhlc) {
2650
+ // Overlays ride on the price series; the panes are composed by the
2651
+ // panel body from `chartIndicatorPanes`.
2652
+ const overlay = chartIndicatorSplit.overlay;
2653
+ if (overlay && direct.series[0]) direct.series[0].overlay = overlay;
2654
+ }
2655
+ return applyChartValueSettings(direct);
2656
+ }
2372
2657
  // A direct type that could not build (scatter with no Y picked yet, for
2373
2658
  // instance) must not fall through to the bar-chart path and draw the wrong
2374
2659
  // chart - it renders the panel's empty state instead.
2375
- if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot") return null;
2660
+ if (
2661
+ chartType === "scatter" || chartType === "gauge" || chartType === "boxplot" ||
2662
+ chartType === "histogram" || chartType === "range-bar" || chartType === "range-area" ||
2663
+ chartType === "dumbbell" || chartType === "bullet" || chartIsOhlc
2664
+ ) return null;
2376
2665
 
2377
- const spec = chartEngine.rowsToChartSpec<Record<string, unknown>>(rowsForChart, {
2666
+ spec = chartEngine.rowsToChartSpec<Record<string, unknown>>(rowsForChart, {
2378
2667
  type: chartType,
2379
2668
  category: category as string,
2380
2669
  value: values.length === 1 ? values[0]! : values,
2381
2670
  ...(seriesField ? { series: seriesField } : {}),
2382
2671
  reduce: chartReduce,
2672
+ ...(chartBucket && chartDimensionIsDate ? { bucket: chartBucket } : {}),
2383
2673
  stacked: effectiveChartStacked || effectiveChartStacked100,
2384
2674
  stacked100: effectiveChartStacked100,
2385
2675
  ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
@@ -2388,10 +2678,16 @@ export function createSvGridController<
2388
2678
  ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
2389
2679
  });
2390
2680
 
2681
+ }
2391
2682
  // Reshape the aggregated grid into whatever this type actually reads.
2392
2683
  // These run AFTER the aggregation so grouping, reduce, sort, topN and the
2393
2684
  // "Other" bucket apply to them exactly as they do to a bar chart.
2394
- if (chartType === "treemap") spec.treemap = chartEngine.specToTreemap(spec);
2685
+ if (chartType === "treemap" || chartType === "sunburst") spec.treemap = chartEngine.specToTreemap(spec);
2686
+ else if (chartType === "chord") {
2687
+ const flow = chartEngine.specToSankey(spec);
2688
+ spec.sankeyNodes = flow.nodes;
2689
+ spec.sankeyLinks = flow.links;
2690
+ }
2395
2691
  else if (chartType === "calendar") spec.calendarValues = chartEngine.specToCalendar(spec);
2396
2692
  else if (chartType === "sankey") {
2397
2693
  const flow = chartEngine.specToSankey(spec);
@@ -2400,10 +2696,7 @@ export function createSvGridController<
2400
2696
  }
2401
2697
 
2402
2698
  if (effectiveChartOrientation === "horizontal") spec.orientation = "horizontal";
2403
- if (effectiveChartTimeAxis) spec.xType = "time";
2404
- if (effectiveChartLogScale) spec.yScale = "log";
2405
- if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
2406
- applyChartLocale(spec);
2699
+ applyChartValueSettings(spec);
2407
2700
  if (chartType !== "pie" && spec.orientation !== "horizontal" && !spec.yAxisTitle && chartAutoYAxisTitle) {
2408
2701
  spec.yAxisTitle = chartAutoYAxisTitle;
2409
2702
  }
@@ -2413,6 +2706,11 @@ export function createSvGridController<
2413
2706
  }
2414
2707
  if (chartCfg.annotations) spec.annotations = chartCfg.annotations;
2415
2708
  if (chartCfg.patternFallback) spec.patternFallback = true;
2709
+ if (chartFunnelShape && chartType === "funnel") spec.funnelShape = chartFunnelShape;
2710
+ if (chartCandleStyle && (chartType === "candlestick" || chartType === "ohlc")) spec.candleStyle = chartCandleStyle;
2711
+ // A stream is a stacked area on a wiggle baseline; the picker's Stacked
2712
+ // box has nothing to add, so the offset is applied here outright.
2713
+ if (chartType === "stream") { spec.stacked = true; spec.stackOffset = "wiggle"; }
2416
2714
  const refLines = [...(chartCfg.referenceLines ?? [])];
2417
2715
  if (chartCfg.averageLine && spec.series[0]?.values.length) {
2418
2716
  const vals = spec.series[0].values;
@@ -2421,7 +2719,10 @@ export function createSvGridController<
2421
2719
  }
2422
2720
  if (refLines.length) spec.referenceLines = refLines;
2423
2721
  for (const s of spec.series) {
2424
- if (chartCfg.trend) s.overlay = chartCfg.trend === "linear" ? "linear" : `${chartCfg.trend}:7`;
2722
+ if (chartCfg.trend) {
2723
+ const t = chartCfg.trend;
2724
+ s.overlay = t === "sma" || t === "ema" ? `${t}:7` : t === "poly" ? "poly:2" : t;
2725
+ }
2425
2726
  if (chartCfg.smooth) s.smooth = true;
2426
2727
  if (chartCfg.seriesTypes?.[s.label]) s.type = chartCfg.seriesTypes[s.label];
2427
2728
  if (chartCfg.seriesAxes?.[s.label]) s.axis = chartCfg.seriesAxes[s.label];
@@ -2429,6 +2730,43 @@ export function createSvGridController<
2429
2730
  return spec;
2430
2731
  });
2431
2732
 
2733
+ /**
2734
+ * What the panel renders: an unlinked chart's frozen snapshot, else the live
2735
+ * derivation with the builder's format applied. The format runs through
2736
+ * the lazy engine so its code stays out of the base bundle.
2737
+ */
2738
+ const chartSpec = $derived.by<ChartSpec | null>(() => {
2739
+ if (activeChart.frozen) return activeChart.frozen.spec;
2740
+ const live = chartSpecLive;
2741
+ if (!live || !activeChart.format || !chartEngine) return live;
2742
+ return chartEngine.applyChartFormat(live, activeChart.format);
2743
+ });
2744
+ let announcedFirstChart = false;
2745
+ $effect(() => {
2746
+ if (!chartPanelOpen || announcedFirstChart || !chartCfg?.onChartCreated) return;
2747
+ announcedFirstChart = true;
2748
+ const c = charts[0]!;
2749
+ untrack(() => chartCfg?.onChartCreated?.({ index: 0, title: c.title, type: c.type, spec: null }));
2750
+ });
2751
+ // Lifecycle: one `onChartChanged` per burst of spec changes.
2752
+ let chartChangeTimer: ReturnType<typeof setTimeout> | null = null;
2753
+ $effect(() => {
2754
+ const cb = chartCfg?.onChartChanged;
2755
+ const spec = chartSpec;
2756
+ const index = activeChartIndex;
2757
+ if (!cb) return;
2758
+ untrack(() => {
2759
+ if (chartChangeTimer) clearTimeout(chartChangeTimer);
2760
+ chartChangeTimer = setTimeout(() => {
2761
+ chartChangeTimer = null;
2762
+ const c = charts[index] ?? activeChart;
2763
+ cb({ index, title: c.title, type: c.type, spec });
2764
+ }, 150);
2765
+ });
2766
+ return () => {
2767
+ if (chartChangeTimer) { clearTimeout(chartChangeTimer); chartChangeTimer = null; }
2768
+ };
2769
+ });
2432
2770
  /**
2433
2771
  * Can a click on this chart be turned back into a grid filter?
2434
2772
  *
@@ -2438,12 +2776,19 @@ export function createSvGridController<
2438
2776
  * at all), or a scatter (the mark carries a point label, not a group).
2439
2777
  */
2440
2778
  const chartCrossFilterable = $derived(
2441
- chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter",
2779
+ chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter" &&
2780
+ // A chord's arcs are both ends of a flow, a sunburst's are a hierarchy
2781
+ // and a histogram's are bins: none maps a click back to one dimension value.
2782
+ chartType !== "chord" && chartType !== "sunburst" && chartType !== "histogram",
2442
2783
  );
2443
2784
 
2785
+ /** The column a chart click filters: the picked dimension, or in pivot mode the innermost row dimension when it is a grid column. */
2786
+ const chartFilterDimId = $derived(
2787
+ pivotResult ? (chartEngine?.pivotFilterColumn(pivotConfig!.rows, chartableColumns.dims, fieldOf) ?? null) : effectiveChartDimensionId,
2788
+ );
2444
2789
  function applyChartCrossFilter(category: string) {
2445
2790
  if (!chartCrossFilterable) return;
2446
- const dimId = effectiveChartDimensionId;
2791
+ const dimId = chartFilterDimId;
2447
2792
  if (!dimId) return;
2448
2793
  const existing = valueFilters[dimId];
2449
2794
  const next = new Set(existing && existing.has(category) ? existing : []);
@@ -2452,7 +2797,7 @@ export function createSvGridController<
2452
2797
  valueFilters = { ...valueFilters, [dimId]: next };
2453
2798
  }
2454
2799
  function clearChartCrossFilter() {
2455
- const dimId = effectiveChartDimensionId;
2800
+ const dimId = chartFilterDimId;
2456
2801
  if (!dimId) return;
2457
2802
  const next = { ...valueFilters };
2458
2803
  delete next[dimId];
@@ -2600,6 +2945,55 @@ export function createSvGridController<
2600
2945
  Math.max(virtualRowTotalSize - virtualRowEnd, 0),
2601
2946
  );
2602
2947
 
2948
+ // A row model tells us when to re-read it. Subscribing here (rather than
2949
+ // polling, or asking the consumer to wrap the model in a rune) is what lets
2950
+ // `getRows()` be a plain method call on a plain object.
2951
+ $effect(() => {
2952
+ const model = rawProps.rowModel;
2953
+ if (!model) return;
2954
+ return model.subscribe(() => {
2955
+ rowModelVersion += 1;
2956
+ });
2957
+ });
2958
+
2959
+ // The index range on screen, for data sources that fetch by block.
2960
+ // `virtualRowStart` / `virtualRowEnd` above are PIXEL offsets; a fetcher
2961
+ // needs row numbers, and needs them whether or not virtualization is on.
2962
+ const visibleRowRange = $derived.by(function visibleRowRange_d() {
2963
+ if (!rowVirtualizationEnabled) {
2964
+ return { startIndex: 0, endIndex: Math.max(0, allRows.length - 1) };
2965
+ }
2966
+ const first = virtualRows[0];
2967
+ const last = virtualRows[virtualRows.length - 1];
2968
+ if (!first || !last) return { startIndex: 0, endIndex: 0 };
2969
+ return { startIndex: first.index, endIndex: last.index };
2970
+ });
2971
+ // Emitted from an effect rather than the derived so a consumer that
2972
+ // fetches (and therefore changes `data`) cannot re-enter the derivation.
2973
+ let lastReportedRange = "";
2974
+ $effect(() => {
2975
+ const report = props.onVisibleRangeChange;
2976
+ if (!report) return;
2977
+ const range = visibleRowRange;
2978
+ const key = range.startIndex + ":" + range.endIndex;
2979
+ if (key === lastReportedRange) return;
2980
+ lastReportedRange = key;
2981
+ report(range);
2982
+ });
2983
+
2984
+ // A paging row model that asks for `autoPageSize` gets as many rows as
2985
+ // the body shows without scrolling, re-measured as the viewport changes.
2986
+ $effect(() => {
2987
+ rowModelVersion;
2988
+ const paging = rawProps.rowModel?.pagination;
2989
+ if (!paging?.autoPageSize) return;
2990
+ const height = viewportHeight;
2991
+ if (height <= 0) return;
2992
+ const rowPx = rowSizePxOf(0) || 30;
2993
+ const fit = Math.max(1, Math.floor(height / rowPx));
2994
+ if (fit !== paging.pageSize) paging.setPageSize(fit);
2995
+ });
2996
+
2603
2997
  // --- Huge-list scroll scaling -----------------------------------------
2604
2998
  // Browsers cap how tall a single element may be, and mobile caps sit well
2605
2999
  // below desktop. Past a few hundred thousand rows the true content height
@@ -2647,9 +3041,46 @@ export function createSvGridController<
2647
3041
  const logical = virtualizer.getState().scrollOffset;
2648
3042
  return logical - rowScrollScaling.logicalToDom(logical);
2649
3043
  });
3044
+ /**
3045
+ * The first `frozenRows` rows are rendered ahead of the virtual window
3046
+ * and stick under the header. The virtualizer still counts them, so its
3047
+ * offsets stay in row-index space and nothing downstream has to translate;
3048
+ * the window skips them, and the top spacer gives back the height they
3049
+ * already occupy in the flow.
3050
+ */
3051
+ const frozenRowCount = $derived(
3052
+ Math.max(0, Math.min(Math.floor(props.frozenRows ?? 0), allRows.length)),
3053
+ );
3054
+ const frozenRowList = $derived(frozenRowCount ? allRows.slice(0, frozenRowCount) : []);
3055
+ /** The merged cells, indexed; null (the usual case) costs nothing. */
3056
+ const mergeIndex = $derived(buildMergeIndex(props.mergedCells));
3057
+ const frozenBandHeight = $derived.by(() => {
3058
+ if (!frozenRowCount) return 0;
3059
+ rowResizeVersion;
3060
+ autoRowHeightVersion;
3061
+ let total = 0;
3062
+ for (let i = 0; i < frozenRowCount; i += 1) total += rowSizePxOf(i);
3063
+ return total;
3064
+ });
3065
+ /** The declared or dragged height of one row; measured under auto height. */
3066
+ function rowSizePxOf(i: number): number {
3067
+ if (isRowCollapsed(i)) return 0;
3068
+ if (autoRowHeightOn) return measuredRowHeights.get(i) ?? autoRowHeightFallback;
3069
+ const own = rowResizeHeightPx(i);
3070
+ if (own != null) return own;
3071
+ const rh = props.rowHeight;
3072
+ return typeof rh === "function" ? rh(i) : (rh ?? 30);
3073
+ }
3074
+ /** Where frozen row i sticks: under the header and the frozen rows above it. */
3075
+ function frozenRowTop(i: number): number {
3076
+ let top = headerHeight;
3077
+ for (let k = 0; k < i; k += 1) top += rowSizePxOf(k);
3078
+ return top;
3079
+ }
2650
3080
  // Spacer heights in DOM space. With scaling inert these equal the original
2651
- // virtualRowStart / virtualRowBottomSpacer.
2652
- const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment, 0));
3081
+ // virtualRowStart / virtualRowBottomSpacer. The frozen band is in the flow
3082
+ // before the spacer whatever the window shows, so it comes off the spacer.
3083
+ const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment - frozenBandHeight, 0));
2653
3084
  const rowBottomSpacer = $derived(
2654
3085
  Math.max(rowDomTotalSize - (virtualRowEnd - rowOffsetAdjustment), 0),
2655
3086
  );
@@ -3003,35 +3434,104 @@ export function createSvGridController<
3003
3434
  );
3004
3435
 
3005
3436
  // ----- Interactive row resize (`rowResize` prop) -----
3006
- // Heights the user has dragged, by row index. A plain Map plus a version
3007
- // counter, exactly like `measuredRowHeights` above and for the same reason:
3008
- // the reset effect below has to read this collection to know whether there is
3009
- // anything to clear, and if the collection were reactive that read would make
3010
- // every resize re-trigger the reset and wipe the height it just stored.
3011
- const rowResizeHeights = new Map<number, number>();
3437
+ // Heights the user has dragged, or set through `api.setRowHeight`, keyed
3438
+ // by ROW ID so they belong to the row rather than to a slot: the height
3439
+ // follows its row through a sort or a filter and survives the data being
3440
+ // replaced. They used to be keyed by index and wiped on every data change,
3441
+ // which in a spreadsheet meant every keystroke, so a row dragged taller
3442
+ // snapped back the moment the next cell was typed. A plain Map plus a
3443
+ // version counter, like `measuredRowHeights` above: the readers touch the
3444
+ // version, and the Map itself stays out of the reactive graph.
3445
+ const rowResizeHeights = new Map<string, number>();
3012
3446
  let rowResizeVersion = $state(0);
3013
-
3014
- function setRowResizeHeight(index: number, height: number): void {
3015
- if (!rowResizeOn || !Number.isFinite(index) || height <= 0) return;
3016
- rowResizeHeights.set(index, Math.round(height));
3447
+ /**
3448
+ * Rows folded to nothing, the row-side twin of `collapsedColumns`: the
3449
+ * row keeps its index and its cells and takes no height. Keyed by row id
3450
+ * like the heights and announced through the same version counter, so
3451
+ * every reader of a row's size sees the change.
3452
+ */
3453
+ const collapsedRows = new Set<string>();
3454
+ function isRowCollapsed(index: number): boolean {
3455
+ rowResizeVersion;
3456
+ const id = allRows[index]?.id;
3457
+ return id != null && collapsedRows.has(id);
3458
+ }
3459
+ function setRowCollapsed(index: number, collapsed: boolean): void {
3460
+ const id = allRows[index]?.id;
3461
+ if (id == null) return;
3462
+ if (collapsed === collapsedRows.has(id)) return;
3463
+ if (collapsed) collapsedRows.add(id);
3464
+ else collapsedRows.delete(id);
3017
3465
  rowResizeVersion += 1;
3018
3466
  }
3019
- /** A user-dragged height for this row, or undefined when it has not been
3020
- * resized. Read by the view AND by the virtualizer's `estimateSize`. */
3021
- function rowResizeHeightPx(index: number): number | undefined {
3022
- return rowResizeOn ? rowResizeHeights.get(index) : undefined;
3467
+
3468
+ /**
3469
+ * A size the user changed by hand is one undo step, as it is in a
3470
+ * spreadsheet: Ctrl+Z after a column drag puts the width back. Recorded
3471
+ * only for the user's gestures; the api's own setters record nothing, so
3472
+ * a consumer restoring saved sizes does not fill the history.
3473
+ */
3474
+ function recordSizeUndo(undo: () => void, redo: () => void): void {
3475
+ pushHistory(ctx, [
3476
+ { rowId: "", columnId: "", field: "", before: undefined, after: undefined, custom: { undo, redo } },
3477
+ ]);
3478
+ }
3479
+ // The undo and redo of a size step report through the same callbacks as
3480
+ // the gesture did: a consumer keeping the sizes (a sheet saving its
3481
+ // document) would otherwise hold the width the user dragged to while the
3482
+ // grid showed the one Ctrl+Z put back.
3483
+ function resizeColumnByUser(columnId: string, width: number, before: number): void {
3484
+ const apply = (w: number) => {
3485
+ columnWidths = { ...columnWidths, [columnId]: w };
3486
+ props.onColumnResize?.({ columnId, width: w });
3487
+ };
3488
+ apply(width);
3489
+ if (before !== width) recordSizeUndo(() => apply(before), () => apply(width));
3490
+ }
3491
+ function autosizeColumnByUser(columnId: string): void {
3492
+ const before = getColumnWidth(columnId);
3493
+ autosizeColumn(columnId);
3494
+ const width = getColumnWidth(columnId);
3495
+ const apply = (w: number) => {
3496
+ columnWidths = { ...columnWidths, [columnId]: w };
3497
+ props.onColumnResize?.({ columnId, width: w });
3498
+ };
3499
+ if (before !== width) recordSizeUndo(() => apply(before), () => apply(width));
3500
+ else props.onColumnResize?.({ columnId, width });
3501
+ }
3502
+ function resizeRowByUser(index: number, height: number | null): void {
3503
+ const before = rowResizeHeightPx(index) ?? null;
3504
+ const apply = (h: number | null) => {
3505
+ setRowResizeHeight(index, h);
3506
+ props.onRowResize?.({ rowIndex: index, height: h });
3507
+ };
3508
+ apply(height);
3509
+ if (before !== height) recordSizeUndo(() => apply(before), () => apply(height));
3023
3510
  }
3024
3511
 
3025
- // A row index means a different row once the data changes, so keeping the
3026
- // heights would size new rows by the old ones - the same reasoning as the
3027
- // measured-height reset below.
3028
- $effect(() => {
3029
- void allRows.length;
3030
- void internalData;
3031
- if (rowResizeHeights.size === 0) return;
3032
- rowResizeHeights.clear();
3512
+ function setRowResizeHeight(index: number, height: number | null): void {
3513
+ const id = allRows[index]?.id;
3514
+ if (id == null) return;
3515
+ if (height == null) {
3516
+ if (!rowResizeHeights.delete(id)) return;
3517
+ } else {
3518
+ if (!Number.isFinite(height) || height <= 0) return;
3519
+ rowResizeHeights.set(id, Math.round(height));
3520
+ }
3033
3521
  rowResizeVersion += 1;
3034
- });
3522
+ }
3523
+ /** A row's own height, or undefined when it has none. Read by the view AND
3524
+ * by the virtualizer's `estimateSize`. Auto height wins over it: the
3525
+ * content decides the height there, and a manual one would be overwritten
3526
+ * by the next measurement. */
3527
+ function rowResizeHeightPx(index: number): number | undefined {
3528
+ const id = allRows[index]?.id;
3529
+ if (id == null) return undefined;
3530
+ // A collapsed row is 0 whatever else it has, under auto height too.
3531
+ if (collapsedRows.has(id)) return 0;
3532
+ if (autoRowHeightOn) return undefined;
3533
+ return rowResizeHeights.get(id);
3534
+ }
3035
3535
 
3036
3536
  /** Report a row's measured height. Called by the view's measuring action.
3037
3537
  * Sub-pixel churn is ignored so a fractional layout can't loop. */
@@ -3056,11 +3556,19 @@ export function createSvGridController<
3056
3556
  function measureRowHeight(node: HTMLElement, index: number) {
3057
3557
  let current = index;
3058
3558
  let stop: (() => void) | null = null;
3059
- const measure = () => reportRowHeight(current, node.getBoundingClientRect().height);
3559
+ // Read the rect only under autoRowHeight. `getBoundingClientRect` forces a
3560
+ // synchronous layout, and the virtualizer's slot keys hand every rendered
3561
+ // <tr> a new index on each one-row scroll, so measuring unconditionally
3562
+ // meant ~30 forced layouts per arrow key on a fixed-height grid whose
3563
+ // measurement `reportRowHeight` then threw away.
3564
+ const measure = () => {
3565
+ if (!autoRowHeightOn) return;
3566
+ reportRowHeight(current, node.getBoundingClientRect().height);
3567
+ };
3060
3568
  const attach = () => {
3061
3569
  stop?.();
3062
3570
  stop = autoRowHeightOn ? observeSizeRaf(node, measure) : null;
3063
- if (autoRowHeightOn) measure();
3571
+ measure();
3064
3572
  };
3065
3573
  attach();
3066
3574
  return {
@@ -3112,9 +3620,9 @@ export function createSvGridController<
3112
3620
  : typeof rh === "function"
3113
3621
  ? rh
3114
3622
  : (rh ?? 30);
3115
- const estimateSize = rowResizeOn
3623
+ const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0
3116
3624
  ? (index: number) =>
3117
- rowResizeHeights.get(index) ??
3625
+ rowResizeHeightPx(index) ??
3118
3626
  (typeof base === "function" ? base(index) : base)
3119
3627
  : base;
3120
3628
  virtualizer.setOptions({
@@ -3175,6 +3683,7 @@ export function createSvGridController<
3175
3683
  // closure each run; the virtualizer sees a new function reference and
3176
3684
  // re-derives its layout from the current per-column widths.
3177
3685
  columnWidths;
3686
+ collapsedColumns;
3178
3687
  columnVirtualizer.setOptions({
3179
3688
  count: allColumns.length,
3180
3689
  estimateSize: (index: number) => {
@@ -3243,9 +3752,18 @@ export function createSvGridController<
3243
3752
 
3244
3753
 
3245
3754
 
3755
+ // Depends on `allRows` and `rowSelectionState` only. It used to read the
3756
+ // catch-all `gridStateVersion` too, which made every arrow key walk all the
3757
+ // rows (10k iterations on the large-dataset demo, 100k on its top tier) to
3758
+ // recount a selection that had not changed.
3246
3759
  const headerSelectionState = $derived.by(function headerSelectionState_d() {
3247
- gridStateVersion;
3248
- const selectable = allRows.filter((row) => !isGroupRow(row));
3760
+ // An external model knows about rows the grid has never seen, so it is
3761
+ // the only thing that can answer honestly once the data is a window
3762
+ // onto a server.
3763
+ if (props.rowSelectionModel) return props.rowSelectionModel.headerState();
3764
+ const selectable = allRows.filter(
3765
+ (row) => !isGroupRow(row) && !placeholderStateOf(row),
3766
+ );
3249
3767
  if (!selectable.length) return "none";
3250
3768
  let selected = 0;
3251
3769
  for (const row of selectable) if (rowSelectionState[row.id]) selected += 1;
@@ -3261,6 +3779,10 @@ export function createSvGridController<
3261
3779
  * from a user-focused (0,0). The fill handle keys off this flag so it
3262
3780
  * stays hidden until the user actually selects something. */
3263
3781
  let userHasActivatedCell = $state(false);
3782
+ /** The column a run of Tabs began in, so the Enter that ends the run goes
3783
+ * down from there (`getEntryStep`). Plain, not state: nothing renders
3784
+ * it. `setActiveCell` clears it; the Tab and Enter paths set it back. */
3785
+ let tabRunOrigin: number | null = null;
3264
3786
 
3265
3787
 
3266
3788
 
@@ -3286,7 +3808,7 @@ export function createSvGridController<
3286
3808
  if (!props.fitColumns || isNarrowResponsive) return null;
3287
3809
  // Exclude columns hidden by a collapsed group too (#57) - otherwise their
3288
3810
  // widths stay in the fit distribution and leave a blank gap at the edge.
3289
- const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id]);
3811
+ const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id] && !collapsedColumns[c.id]);
3290
3812
  if (!cols.length) return null;
3291
3813
  const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
3292
3814
  const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
@@ -3465,7 +3987,12 @@ export function createSvGridController<
3465
3987
  for (const column of allColumns) {
3466
3988
  const meta = isBucketableColumn(column);
3467
3989
  if (!meta) continue;
3468
- const buckets = buildBuckets(column, meta.isDate, props.data, getColumnAccessorValue);
3990
+ const buckets = buildBuckets(
3991
+ column,
3992
+ meta.isDate,
3993
+ props.data ?? EMPTY_DATA,
3994
+ getColumnAccessorValue,
3995
+ );
3469
3996
  if (buckets) map.set(column.id, buckets);
3470
3997
  }
3471
3998
  return map;
@@ -3712,12 +4239,22 @@ export function createSvGridController<
3712
4239
  // state mutation inside the callback (e.g. `api.setGroupBy(...)`) created
3713
4240
  // an infinite update loop. Now it's a true mount-once notification.
3714
4241
  let apiNotified = false;
4242
+ // Memoized: `buildApi()` builds a fresh object each call, and a feature pack
4243
+ // that keys per-grid state on the api (a WeakMap in @svgrid/enterprise) needs
4244
+ // the object the consumer received and the object a command handler sees to
4245
+ // be the same one. Every method reads through `ctx`, so one instance stays
4246
+ // correct for the life of the grid.
4247
+ let apiInstance: ReturnType<typeof buildApi> | null = null;
4248
+ function api() {
4249
+ if (!apiInstance) apiInstance = buildApi();
4250
+ return apiInstance;
4251
+ }
3715
4252
  $effect(() => {
3716
4253
  if (apiNotified) return;
3717
4254
  const cb = props.onApiReady;
3718
4255
  if (!cb) return;
3719
4256
  apiNotified = true;
3720
- cb(buildApi());
4257
+ cb(api());
3721
4258
  });
3722
4259
 
3723
4260
  const ctx = {
@@ -3780,6 +4317,8 @@ export function createSvGridController<
3780
4317
  set history(v) { history = v as never; },
3781
4318
  get historyPtr() { return historyPtr; },
3782
4319
  set historyPtr(v) { historyPtr = v as never; },
4320
+ get historyGroupId() { return historyGroupId; },
4321
+ set historyGroupId(v) { historyGroupId = v as never; },
3783
4322
  get historyVersion() { return historyVersion; },
3784
4323
  set historyVersion(v) { historyVersion = v as never; },
3785
4324
  get tooltip() { return tooltip; },
@@ -3802,6 +4341,9 @@ export function createSvGridController<
3802
4341
  get editorSelectAll() { return editorSelectAll; },
3803
4342
  set editorSelectAll(v) { editorSelectAll = v as never; },
3804
4343
  get columnWidths() { return columnWidths; },
4344
+ get collapsedColumns() { return collapsedColumns; },
4345
+ get mergeIndex() { return mergeIndex; },
4346
+ get setColumnCollapsed() { return setColumnCollapsed; },
3805
4347
  set columnWidths(v) { columnWidths = v as never; },
3806
4348
  get MIN_COLUMN_WIDTH() { return MIN_COLUMN_WIDTH; },
3807
4349
  get columnPinning() { return columnPinning; },
@@ -3873,6 +4415,7 @@ export function createSvGridController<
3873
4415
  get moveCellsEffective() { return moveCellsEffective; },
3874
4416
  get flushScheduledScrollSync() { return flushScheduledScrollSync; },
3875
4417
  get scheduleScrollSync() { return scheduleScrollSync; },
4418
+ get syncScrollNow() { return syncScrollNow; },
3876
4419
  get internalData() { return internalData; },
3877
4420
  set internalData(v) { internalData = v as never; },
3878
4421
  get internalColumns() { return internalColumns; },
@@ -3958,6 +4501,8 @@ export function createSvGridController<
3958
4501
  get selectionBarMaxVisible() { return selectionBarMaxVisible; },
3959
4502
  get selectionBarHideClear() { return selectionBarHideClear; },
3960
4503
  get selectionBarTarget() { return selectionBarTarget; },
4504
+ get selectionBarCount() { return selectionBarCount; },
4505
+ get selectionBarVisible() { return selectionBarVisible; },
3961
4506
  get lastSelectionSerialized() { return lastSelectionSerialized; },
3962
4507
  set lastSelectionSerialized(v) { lastSelectionSerialized = v as never; },
3963
4508
  get lastCellRangeSerialized() { return lastCellRangeSerialized; },
@@ -3976,6 +4521,8 @@ export function createSvGridController<
3976
4521
  get moveColumnInPanel() { return moveColumnInPanel; },
3977
4522
  // ---- Localized chrome strings ----
3978
4523
  get messages() { return gridMessages; },
4524
+ /** The raw `localization.text` map, for the lazy chart panel's own strings. */
4525
+ get localizationText() { return props.localization?.text; },
3979
4526
  // ---- In-grid pivot mode ----
3980
4527
  get pivotConfig() { return pivotConfig; },
3981
4528
  get pivotActive() { return pivotActive; },
@@ -4008,6 +4555,32 @@ export function createSvGridController<
4008
4555
  set chartLogScale(v) { activeChart.logScale = v as never; },
4009
4556
  get chartTimeAxis() { return effectiveChartTimeAxis; },
4010
4557
  set chartTimeAxis(v) { activeChart.timeAxis = v as never; },
4558
+ get chartBucket() { return chartBucket; },
4559
+ set chartBucket(v) { activeChart.bucket = v as never; },
4560
+ get chartBins() { return chartBins; },
4561
+ set chartBins(v) { activeChart.bins = v as never; },
4562
+ get chartFunnelShape() { return chartFunnelShape; },
4563
+ set chartFunnelShape(v) { activeChart.funnelShape = v as never; },
4564
+ get chartCandleStyle() { return chartCandleStyle; },
4565
+ set chartCandleStyle(v) { activeChart.candleStyle = v as never; },
4566
+ get chartFormat() { return activeChart.format; },
4567
+ set chartFormat(v) { activeChart.format = v as never; },
4568
+ get chartFrozen() { return activeChart.frozen; },
4569
+ get chartIsOhlc() { return chartIsOhlc; },
4570
+ get chartOhlc() { return effectiveChartOhlc; },
4571
+ set chartOhlc(v) { activeChart.ohlc = v as never; },
4572
+ get chartOhlcDateId() { return effectiveChartOhlcDateId; },
4573
+ get chartIndicators() { return chartIndicators; },
4574
+ set chartIndicators(v) { activeChart.indicators = v as never; },
4575
+ get chartIndicatorPanes() { return chartIndicatorPanes; },
4576
+ get chartZoom() { return activeChart.zoom; },
4577
+ // The panel keeps one chart component across tabs, so "tabs share a
4578
+ // window" is simply every tab carrying the same window: a zoom on one is
4579
+ // written to all, and switching tabs hands the component the same value.
4580
+ set chartZoom(v) {
4581
+ if (chartCfg?.syncTabs) for (const c of charts) c.zoom = v as never;
4582
+ else activeChart.zoom = v as never;
4583
+ },
4011
4584
  get chartValueFormat() { return effectiveChartValueFormat ?? "number"; },
4012
4585
  set chartValueFormat(v) { activeChart.valueFormat = v as never; },
4013
4586
  get chartDimensionIsDate() { return chartDimensionIsDate; },
@@ -4028,8 +4601,21 @@ export function createSvGridController<
4028
4601
  get activeChartIndex() { return Math.min(activeChartIndex, charts.length - 1); },
4029
4602
  set activeChartIndex(v) { activeChartIndex = Math.max(0, Math.min(charts.length - 1, v as number)); },
4030
4603
  addChart() {
4031
- charts = [...charts, makeChart(`Chart ${charts.length + 1}`)];
4604
+ const next = makeChart(`Chart ${charts.length + 1}`);
4605
+ if (chartCfg?.syncTabs) next.zoom = activeChart.zoom;
4606
+ charts = [...charts, next];
4032
4607
  activeChartIndex = charts.length - 1;
4608
+ chartCfg?.onChartCreated?.({ index: activeChartIndex, title: next.title, type: next.type, spec: null });
4609
+ },
4610
+ /** Unlink the active chart from the grid: it keeps the spec it has now. */
4611
+ freezeChart() {
4612
+ const spec = chartSpec;
4613
+ if (!spec) return;
4614
+ activeChart.frozen = { spec: JSON.parse(JSON.stringify(spec)) as ChartSpec, at: new Date().toISOString() };
4615
+ },
4616
+ /** Link the active chart back to the grid's rows. */
4617
+ unfreezeChart() {
4618
+ activeChart.frozen = null;
4033
4619
  },
4034
4620
  removeChart(index?: number) {
4035
4621
  if (charts.length <= 1) return;
@@ -4042,48 +4628,39 @@ export function createSvGridController<
4042
4628
  if (c) c.title = title;
4043
4629
  },
4044
4630
  getChartsState() {
4045
- return charts.map((c) => ({
4046
- title: c.title,
4047
- type: c.type,
4048
- dimension: c.dimensionId,
4049
- series: c.seriesId ?? null,
4050
- measure: c.measureId,
4051
- measure2: c.measure2Id,
4052
- orientation: c.orientation,
4053
- stacked100: c.stacked100,
4054
- donut: c.donut,
4055
- palette: c.palette,
4056
- reduce: c.reduce,
4057
- stacked: c.stacked,
4058
- dataLabels: c.dataLabels,
4059
- logScale: c.logScale,
4060
- timeAxis: c.timeAxis,
4061
- valueFormat: c.valueFormat,
4062
- }));
4631
+ return charts.map(tabToState);
4063
4632
  },
4064
4633
  applyChartsState(list: ReadonlyArray<Record<string, unknown>>, active?: number) {
4065
- const restored = list.map((c, i) => ({
4066
- id: `chart-${chartSeq++}`,
4067
- title: typeof c.title === "string" ? c.title : `Chart ${i + 1}`,
4068
- type: (c.type as ChartType) ?? "bar",
4069
- reduce: (c.reduce as "sum" | "avg" | "count") ?? "sum",
4070
- dimensionId: (c.dimension as string | null) ?? null,
4071
- measureId: (c.measure as string | null) ?? null,
4072
- seriesId: c.series as string | null | undefined,
4073
- measure2Id: (c.measure2 as string | null) ?? null,
4074
- orientation: (c.orientation as "vertical" | "horizontal" | null) ?? null,
4075
- stacked100: (c.stacked100 as boolean | null) ?? null,
4076
- donut: (c.donut as boolean | null) ?? null,
4077
- palette: (c.palette as string[] | null) ?? null,
4078
- stacked: (c.stacked as boolean | null) ?? null,
4079
- dataLabels: (c.dataLabels as boolean | null) ?? null,
4080
- logScale: (c.logScale as boolean | null) ?? null,
4081
- timeAxis: (c.timeAxis as boolean | null) ?? null,
4082
- valueFormat: (c.valueFormat as ChartValueFormat | null) ?? null,
4083
- }));
4634
+ const restored = list.map(tabFromState);
4084
4635
  charts = restored.length ? restored : [makeChart("Chart 1")];
4085
4636
  activeChartIndex = Math.max(0, Math.min(charts.length - 1, active ?? 0));
4086
4637
  },
4638
+ // ---- Saved charts ----
4639
+ get savedCharts() { return savedCharts; },
4640
+ /** Keep the active tab's configuration under a name (replacing a same-named one). */
4641
+ saveChart(name: string) {
4642
+ const entry: SavedChartEntry = { name, savedAt: new Date().toISOString(), tab: tabToState(activeChart) };
4643
+ savedCharts = [...savedCharts.filter((s) => s.name !== name), entry];
4644
+ },
4645
+ /** Apply a saved chart to the active tab; the tab keeps its id and title. */
4646
+ applySavedChart(name: string): boolean {
4647
+ const entry = savedCharts.find((s) => s.name === name);
4648
+ if (!entry) return false;
4649
+ const { id: _id, title: _title, ...rest } = tabFromState(entry.tab, activeChartIndex);
4650
+ Object.assign(activeChart, rest);
4651
+ return true;
4652
+ },
4653
+ removeSavedChart(name: string) {
4654
+ savedCharts = savedCharts.filter((s) => s.name !== name);
4655
+ },
4656
+ getSavedCharts() {
4657
+ return savedCharts.map((s) => ({ ...s, tab: { ...s.tab } }));
4658
+ },
4659
+ applySavedCharts(list: ReadonlyArray<Record<string, unknown>>) {
4660
+ savedCharts = list
4661
+ .filter((s) => typeof s?.name === "string" && s.tab && typeof s.tab === "object")
4662
+ .map((s) => ({ name: s.name as string, savedAt: String(s.savedAt ?? ""), tab: s.tab as Record<string, unknown> }));
4663
+ },
4087
4664
  get chartCrossFilterable() { return chartCrossFilterable; },
4088
4665
  get chartOrientation() { return effectiveChartOrientation; },
4089
4666
  set chartOrientation(v) { activeChart.orientation = v as never; },
@@ -4098,19 +4675,40 @@ export function createSvGridController<
4098
4675
  get effectiveChartMeasure2Id() { return effectiveChartMeasure2Id; },
4099
4676
  get chartAiHandler() { return chartAiHandler; },
4100
4677
  set chartAiHandler(v) { chartAiHandler = v as never; },
4678
+ get chartExplainHandler() { return chartExplainHandler; },
4679
+ set chartExplainHandler(v) { chartExplainHandler = v as never; },
4101
4680
  applyChartConfig(config: {
4102
4681
  open?: boolean;
4103
4682
  type?: ChartType;
4104
4683
  dimension?: string | null;
4105
4684
  series?: string | null;
4106
4685
  measure?: string | null;
4107
- reduce?: "sum" | "avg" | "count";
4686
+ reduce?: ChartReducer;
4108
4687
  stacked?: boolean;
4109
4688
  dataLabels?: boolean;
4110
4689
  logScale?: boolean;
4111
4690
  timeAxis?: boolean;
4112
4691
  valueFormat?: ChartValueFormat;
4692
+ bucket?: ChartTimeBucket | null;
4693
+ bins?: number | null;
4694
+ funnelShape?: "trapezoid" | "pyramid" | "cone" | null;
4695
+ candleStyle?: "classic" | "hollow" | "heikin-ashi" | null;
4696
+ /** Second measure: scatter Y, range high end, bullet target. */
4697
+ measure2?: string | null;
4698
+ /** Candlestick / OHLC price columns (ids or fields). */
4699
+ ohlc?: { open?: string | null; high?: string | null; low?: string | null; close?: string | null; volume?: string | null } | null;
4700
+ /** Indicators: panes (volume, rsi, macd, stochastic, atr, obv) and
4701
+ * overlays (sma, ema, bb, vwap). */
4702
+ indicators?: ChartPanelIndicator[];
4703
+ /** The builder's format (titles, axes, legend, series style); `null` resets it. */
4704
+ format?: ChartFormatState | null;
4705
+ /** Unlink the chart from the grid (true) or link it back (false). */
4706
+ frozen?: boolean;
4707
+ /** A saved chart's name: applied to the active tab first, then the
4708
+ * other keys on top. */
4709
+ saved?: string;
4113
4710
  }) {
4711
+ if (config.saved) this.applySavedChart(config.saved);
4114
4712
  const cols = [...chartableColumns.dims, ...chartableColumns.measures];
4115
4713
  const resolve = (v: string | null | undefined): string | null => {
4116
4714
  if (v == null) return null;
@@ -4130,6 +4728,24 @@ export function createSvGridController<
4130
4728
  if (typeof config.logScale === "boolean") activeChart.logScale = config.logScale;
4131
4729
  if (typeof config.timeAxis === "boolean") activeChart.timeAxis = config.timeAxis;
4132
4730
  if (config.valueFormat) activeChart.valueFormat = config.valueFormat;
4731
+ if ("bucket" in config) activeChart.bucket = config.bucket ?? null;
4732
+ if ("bins" in config) activeChart.bins = config.bins ?? null;
4733
+ if ("funnelShape" in config) activeChart.funnelShape = config.funnelShape ?? null;
4734
+ if ("candleStyle" in config) activeChart.candleStyle = config.candleStyle ?? null;
4735
+ if ("measure2" in config) activeChart.measure2Id = resolve(config.measure2);
4736
+ if ("ohlc" in config) {
4737
+ const o = config.ohlc;
4738
+ activeChart.ohlc = o
4739
+ ? { open: resolve(o.open), high: resolve(o.high), low: resolve(o.low), close: resolve(o.close), volume: resolve(o.volume) }
4740
+ : null;
4741
+ }
4742
+ if (config.indicators) activeChart.indicators = [...config.indicators];
4743
+ if ("format" in config) activeChart.format = config.format ?? null;
4744
+ if (config.frozen === false) activeChart.frozen = null;
4745
+ else if (config.frozen === true && !activeChart.frozen) {
4746
+ const spec = chartSpec;
4747
+ if (spec) activeChart.frozen = { spec: JSON.parse(JSON.stringify(spec)) as ChartSpec, at: new Date().toISOString() };
4748
+ }
4133
4749
  },
4134
4750
  get toggleGroupInPanel() { return toggleGroupInPanel; },
4135
4751
  get lastSortingSerialized() { return lastSortingSerialized; },
@@ -4152,8 +4768,17 @@ export function createSvGridController<
4152
4768
  },
4153
4769
  /** True when the `rowResize` prop is on and nothing overrides it. */
4154
4770
  get rowResizeOn() { return rowResizeOn; },
4771
+ get frozenRowCount() { return frozenRowCount; },
4772
+ get frozenRowList() { return frozenRowList; },
4773
+ get frozenBandHeight() { return frozenBandHeight; },
4774
+ get frozenRowTop() { return frozenRowTop; },
4155
4775
  /** Record the height the user dragged a row to. */
4156
4776
  get setRowResizeHeight() { return setRowResizeHeight; },
4777
+ get isRowCollapsed() { return isRowCollapsed; },
4778
+ get setRowCollapsed() { return setRowCollapsed; },
4779
+ get resizeRowByUser() { return resizeRowByUser; },
4780
+ get resizeColumnByUser() { return resizeColumnByUser; },
4781
+ get autosizeColumnByUser() { return autosizeColumnByUser; },
4157
4782
  /** A row's dragged height, or undefined when it has not been resized. */
4158
4783
  get rowResizeHeightPx() {
4159
4784
  rowResizeVersion;
@@ -4213,9 +4838,18 @@ export function createSvGridController<
4213
4838
  get isRowSelected() { return isRowSelected; },
4214
4839
  get toggleRowSelectionById() { return toggleRowSelectionById; },
4215
4840
  get headerSelectionState() { return headerSelectionState; },
4841
+ get visibleRowRange() { return visibleRowRange; },
4842
+ placeholderStateOf,
4843
+ /** Retry a failed placeholder row. No-op without `onRetryRow`. */
4844
+ retryRow(row: Row<TData>) {
4845
+ props.onRetryRow?.(row.original, row.index);
4846
+ },
4216
4847
  get toggleSelectAllRows() { return toggleSelectAllRows; },
4848
+ get setSelectAllRows() { return setSelectAllRows; },
4217
4849
  get userHasActivatedCell() { return userHasActivatedCell; },
4218
4850
  set userHasActivatedCell(v) { userHasActivatedCell = v as never; },
4851
+ get tabRunOrigin() { return tabRunOrigin; },
4852
+ set tabRunOrigin(v) { tabRunOrigin = v; },
4219
4853
  get setActiveCell() { return setActiveCell; },
4220
4854
  get scrollActiveCellIntoView() { return scrollActiveCellIntoView; },
4221
4855
  get getColumnBaseWidth() { return getColumnBaseWidth; },
@@ -4225,6 +4859,8 @@ export function createSvGridController<
4225
4859
  get extendSelection() { return extendSelection; },
4226
4860
  get isCellInSelectedRange() { return isCellInSelectedRange; },
4227
4861
  get getCellRangeEdges() { return getCellRangeEdges; },
4862
+ get activeRangeRect() { return activeRangeRect; },
4863
+ get cellSelectionState() { return cellSelectionState; },
4228
4864
  get getSelectionRects() { return getSelectionRects; },
4229
4865
  get fillHandleCell() { return fillHandleCell; },
4230
4866
  get isInFillPreview() { return isInFillPreview; },
@@ -4247,6 +4883,7 @@ export function createSvGridController<
4247
4883
  get startFillDrag() { return startFillDrag; },
4248
4884
  get onFillPointerMove() { return onFillPointerMove; },
4249
4885
  get onFillPointerUp() { return onFillPointerUp; },
4886
+ get fillDownToNeighbour() { return fillDownToNeighbour; },
4250
4887
  get isOnMoveGrabStrip() { return isOnMoveGrabStrip; },
4251
4888
  get startMoveDrag() { return startMoveDrag; },
4252
4889
  get onMovePointerMove() { return onMovePointerMove; },
@@ -4266,6 +4903,7 @@ export function createSvGridController<
4266
4903
  get cutSelectionToClipboard() { return cutSelectionToClipboard; },
4267
4904
  get pasteFromClipboard() { return pasteFromClipboard; },
4268
4905
  get onGridPaste() { return onGridPaste; },
4906
+ get armPasteFallback() { return armPasteFallback; },
4269
4907
  get clearSelectedCells() { return clearSelectedCells; },
4270
4908
  get onCellDoubleClick() { return onCellDoubleClick; },
4271
4909
  get startEditingWithChar() { return startEditingWithChar; },
@@ -4342,12 +4980,12 @@ export function createSvGridController<
4342
4980
  get clearColumnFilter() { return clearColumnFilter; },
4343
4981
  get onWindowKeydown() { return onWindowKeydown; },
4344
4982
  get columnDefMatchesId() { return columnDefMatchesId; },
4345
- get buildApi() { return buildApi; },
4983
+ get buildApi() { return api; },
4346
4984
  get apiNotified() { return apiNotified; },
4347
4985
  set apiNotified(v) { apiNotified = v as never; },
4348
4986
  };
4349
4987
  const { resolveEffectiveFeatures } = createFeatures<TFeatures, TData>(ctx);
4350
- const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, onBodyScroll } = createScrollSync<TFeatures, TData>(ctx);
4988
+ const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, syncScrollNow, onBodyScroll } = createScrollSync<TFeatures, TData>(ctx);
4351
4989
  const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard<TFeatures, TData>(ctx);
4352
4990
  const { computeSummaries, hasRenderedColumn } = createSummaries<TFeatures, TData>(ctx);
4353
4991
  // Hand the aggregator to `eagerSummaries` (declared above `ctx`, which this
@@ -4356,13 +4994,13 @@ export function createSvGridController<
4356
4994
 
4357
4995
  const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
4358
4996
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
4359
- const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
4360
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
4997
+ const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste, armPasteFallback } = createEditing<TFeatures, TData>(ctx);
4998
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, activeRangeRect, cellSelectionState, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
4361
4999
  const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns<TFeatures, TData>(ctx);
4362
5000
  const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, onRowPointerDown, destroyRowDrag } = createRowDrag<TFeatures, TData>(ctx);
4363
5001
  const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids<TFeatures, TData>(ctx);
4364
5002
  const { buildApi } = createGridApi<TFeatures, TData>(ctx);
4365
- const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
5003
+ const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, fillDownToNeighbour, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
4366
5004
 
4367
5005
  // Dev-time configuration checks. Silent misconfiguration was the grid's
4368
5006
  // biggest usability gap - a misspelled `field` rendered a column of blank
@@ -4397,6 +5035,7 @@ export function createSvGridController<
4397
5035
  pageSize: props.pageSize,
4398
5036
  groupBy: props.groupBy,
4399
5037
  treeData: props.treeData,
5038
+ serverGroup: props.serverGroup,
4400
5039
  initialColumnPinning: props.initialColumnPinning,
4401
5040
  columnVirtualization: props.columnVirtualization,
4402
5041
  externalPagination: props.externalPagination,