@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
@@ -6,6 +6,8 @@ import "./sv-grid-scrollbar.js";
6
6
  import { computeColumnStat, formatsNeedingStats, formatNeedsStats, } from "./conditional-formatting.js";
7
7
  import { rawToNumber, } from "./SvGrid.helpers.js";
8
8
  import { createFeatures } from "./features.js";
9
+ import { pushHistory } from "./history.js";
10
+ import { buildMergeIndex } from "./merges.js";
9
11
  import { createScrollSync, } from "./scroll-sync.js";
10
12
  import { createKeyboard, } from "./keyboard-handlers.js";
11
13
  import { createSummaries, } from "./summaries.js";
@@ -169,6 +171,60 @@ export function createSvGridController(rawProps, domIdBase) {
169
171
  // preserves reference identity for non-overridden keys, which the columns-identity
170
172
  // column cache and the `data`/`columns` sync effects rely on.
171
173
  let optionOverrides = $state({});
174
+ /**
175
+ * A `rowModel` answers for the props it can, so a server-backed grid is
176
+ * one prop rather than a dozen.
177
+ *
178
+ * Bumped by the model itself through `subscribe` (see the effect below),
179
+ * which is what makes `getRows()` and `isLoading()` - plain method calls,
180
+ * invisible to the reactive system - re-read when the model changes.
181
+ */
182
+ let rowModelVersion = $state(0);
183
+ const rowModelProps = $derived.by(function rowModelProps_d() {
184
+ const model = rawProps.rowModel;
185
+ if (!model)
186
+ return null;
187
+ rowModelVersion; // read, so a notification re-derives
188
+ const paging = model.pagination;
189
+ return {
190
+ data: model.getRows(),
191
+ loading: model.isLoading(),
192
+ loadingOverlay: true,
193
+ getRowId: model.getRowId,
194
+ // A model that sorts or filters server-side is telling the grid not
195
+ // to do it locally; one that does neither leaves the grid in charge.
196
+ externalSort: model.setSort ? true : undefined,
197
+ externalFilter: model.setFilter ? true : undefined,
198
+ onSortingChange: model.setSort ? (s) => model.setSort(s) : undefined,
199
+ onFiltersChange: model.setFilter ? (f) => model.setFilter(f) : undefined,
200
+ onVisibleRangeChange: model.setViewport
201
+ ? (r) => model.setViewport(r.startIndex, r.endIndex)
202
+ : undefined,
203
+ rowPlaceholder: model.rowPlaceholder,
204
+ onRetryRow: model.retryRow,
205
+ serverGroup: model.group,
206
+ rowSelectionModel: model.selection,
207
+ serverFilterValues: model.filterValues
208
+ ? (columnId) => model.filterValues(columnId)
209
+ : undefined,
210
+ pinnedTopRows: model.pinnedTopRows,
211
+ pinnedBottomRows: model.pinnedBottomRows,
212
+ pivotResultColumns: model.pivotResultColumns ?? undefined,
213
+ externalPagination: paging ? true : undefined,
214
+ rowCount: paging?.rowCount,
215
+ pageIndex: paging?.pageIndex,
216
+ pageSize: paging?.pageSize,
217
+ pageSizeOptions: paging?.pageSizes,
218
+ onPaginationChange: paging
219
+ ? (p) => {
220
+ if (p.pageSize !== paging.pageSize)
221
+ paging.setPageSize(p.pageSize);
222
+ else
223
+ paging.setPage(p.pageIndex);
224
+ }
225
+ : undefined,
226
+ };
227
+ });
172
228
  const props = new Proxy(rawProps, {
173
229
  // Read the override as a PROPERTY (tracked by `$state`), not via `in` (the `has`
174
230
  // trap is not tracked) - so a reactive read of `props.X` re-runs when an override
@@ -177,7 +233,23 @@ export function createSvGridController(rawProps, domIdBase) {
177
233
  // undefined too. Whole-object reassignment on write makes the signal fire.
178
234
  get: (t, k) => {
179
235
  const v = typeof k === "string" ? optionOverrides[k] : undefined;
180
- return v !== undefined ? v : Reflect.get(t, k);
236
+ if (v !== undefined)
237
+ return v;
238
+ // Pivot mode swaps the column list: `pivotResultColumns` (the prop, or
239
+ // the row model's) stands in for `columns` while it is set, and
240
+ // `columns` itself stays the app's own list for when pivot ends.
241
+ if (k === "columns") {
242
+ const pivot = Reflect.get(t, "pivotResultColumns") ??
243
+ rowModelProps?.pivotResultColumns;
244
+ if (pivot)
245
+ return pivot;
246
+ }
247
+ const own = Reflect.get(t, k);
248
+ // A prop written on the grid always beats the row model, so adopting
249
+ // `rowModel` is never all-or-nothing.
250
+ if (own !== undefined || typeof k !== "string")
251
+ return own;
252
+ return rowModelProps?.[k];
181
253
  },
182
254
  });
183
255
  // Resolved capability gates. Capabilities are OFF by default - a bare
@@ -215,7 +287,12 @@ export function createSvGridController(rawProps, domIdBase) {
215
287
  let pendingScrollTop = 0;
216
288
  let pendingScrollLeft = 0;
217
289
  let scrollSyncRaf = null;
218
- let selectionRange = $state({ anchor: null, focus: null });
290
+ // `$state.raw`: every writer replaces the whole object, and every rendered
291
+ // cell's selection derived reads it. Under a deep proxy each of those ~300
292
+ // deriveds tracked the nested anchor/focus row/col signals separately (six
293
+ // dependencies per cell to mark and re-check on every arrow key) where one
294
+ // will do.
295
+ let selectionRange = $state.raw({ anchor: null, focus: null });
219
296
  // Extra committed ranges for multi-range (Ctrl+drag) selection. The
220
297
  // `selectionRange` above is always the ACTIVE range being manipulated; these
221
298
  // are the finished ones. Full selection = these + the active range.
@@ -243,7 +320,12 @@ export function createSvGridController(rawProps, domIdBase) {
243
320
  // Full-row editing: the row currently in whole-row edit + its per-column
244
321
  // draft (keyed by column id). Null when not in full-row mode.
245
322
  let fullRowEdit = $state(null);
246
- let editedCellValues = $state({});
323
+ // `$state.raw`, not `$state`: every writer replaces the whole object, and
324
+ // the render path asks `key in editedCellValues` once per rendered cell. On
325
+ // a deep proxy that `in` check, run inside an effect, creates and keeps a
326
+ // per-key signal for every cell that ever scrolls into view - a leak that
327
+ // grows with rows x columns and a Map lookup per cell per render.
328
+ let editedCellValues = $state.raw({});
247
329
  const UNDO_LIMIT = 200;
248
330
  let history = $state([]);
249
331
  /** Index in `history` of the LAST applied step. -1 means "nothing applied".
@@ -253,6 +335,11 @@ export function createSvGridController(rawProps, domIdBase) {
253
335
  /** Bumps on every undo / redo / record so $derived consumers can
254
336
  * observe via the api without subscribing to history directly. */
255
337
  let historyVersion = $state(0);
338
+ /** Set while a `runHistoryGroup` call is on the stack, so every step pushed
339
+ * inside one action shares a group id and undoes together. Deliberately not
340
+ * $state: it is written and read synchronously inside a single call and
341
+ * nothing renders from it. */
342
+ let historyGroupId = undefined;
256
343
  let tooltip = $state(null);
257
344
  let tooltipTimer = null;
258
345
  // ---- Find-in-grid ----------------------------------------------------
@@ -288,6 +375,26 @@ export function createSvGridController(rawProps, domIdBase) {
288
375
  let editorSelectAll = true;
289
376
  /** Per-column width overrides set by the resize handles. */
290
377
  let columnWidths = $state({});
378
+ /**
379
+ * Columns folded to nothing, the way a sheet hides a column: the column
380
+ * keeps its index, its cells and its width for later, and takes no room.
381
+ * Distinct from `hiddenColumns`, which takes a column out of the model
382
+ * and shifts every index after it; a spreadsheet's formulas and formats
383
+ * are keyed by index and could not follow that.
384
+ */
385
+ let collapsedColumns = $state({});
386
+ function setColumnCollapsed(columnId, collapsed) {
387
+ if (!!collapsedColumns[columnId] === collapsed)
388
+ return;
389
+ if (collapsed) {
390
+ collapsedColumns = { ...collapsedColumns, [columnId]: true };
391
+ }
392
+ else {
393
+ const next = { ...collapsedColumns };
394
+ delete next[columnId];
395
+ collapsedColumns = next;
396
+ }
397
+ }
291
398
  const MIN_COLUMN_WIDTH = 40;
292
399
  /** Columns pinned to the left or right edge of the grid (sticky positioning).
293
400
  * Seeded from `props.initialColumnPinning` so demos / tests can show the
@@ -373,26 +480,53 @@ export function createSvGridController(rawProps, domIdBase) {
373
480
  const hb = column.columnDef?.hideBelow;
374
481
  return hb != null && viewportWidth > 0 && viewportWidth < hb;
375
482
  }
376
- const scrollMetrics = $derived.by(function scrollMetrics_d() {
483
+ /**
484
+ * Live scroll geometry of the scroll container (feeds the custom
485
+ * scrollbars and `hasVerticalOverflow`).
486
+ *
487
+ * Measured from a plain `$effect`, NOT inside a `$derived`, on purpose.
488
+ * `scrollTop` / `scrollHeight` / `scrollWidth` reads force a synchronous
489
+ * layout whenever the tree is dirty. As a derived this was pulled by the
490
+ * shell's `class:sv-grid-has-vscroll` at the very start of every scroll
491
+ * flush - before the recycled rows had been rewritten - so it forced a
492
+ * full table layout that the row writes then invalidated again, and the
493
+ * browser laid the table out a second time before painting. Two table
494
+ * layouts per arrow key on a large grid. An effect runs after the flush
495
+ * has written the DOM, so the reads below land on a tree the browser has
496
+ * to lay out anyway, and the pre-paint pass is incremental.
497
+ *
498
+ * The version reads keep the same triggers the derived had: a scroll
499
+ * tick, a shell resize, and either virtualizer's window/size change (data
500
+ * load, row / column count change) - without those the scrollbar kept a
501
+ * stale `content-size` ≈ 0, its hidden-check tripped, it set
502
+ * `pointer-events: none`, and the user could not drag it.
503
+ */
504
+ let scrollMetrics = $state.raw({
505
+ scrollTop: 0,
506
+ scrollLeft: 0,
507
+ scrollHeight: 0,
508
+ scrollWidth: 0,
509
+ });
510
+ $effect(function scrollMetrics_e() {
377
511
  scrollVersion;
378
512
  viewportVersion;
379
- // Track the virtualizers' versions too so when data loads or row /
380
- // column counts change, scrollMetrics re-reads the DOM's grown
381
- // scrollHeight / scrollWidth. Without these deps the scrollbar
382
- // receives a stale `content-size` ≈ 0, its hidden-check trips, it
383
- // sets `pointer-events: none`, and the user can't drag it. The
384
- // identifiers below are declared further down - derived callbacks
385
- // run lazily, so by the time this fires they're in scope.
386
513
  virtualizer.version;
387
514
  columnVirtualizerVersion;
388
- return {
389
- scrollTop: scrollContainer?.scrollTop ?? 0,
390
- scrollLeft: scrollContainer?.scrollLeft ?? 0,
391
- clientHeight: scrollContainer?.clientHeight ?? 0,
392
- clientWidth: scrollContainer?.clientWidth ?? 0,
393
- scrollHeight: scrollContainer?.scrollHeight ?? 0,
394
- scrollWidth: scrollContainer?.scrollWidth ?? 0,
395
- };
515
+ const el = scrollContainer;
516
+ if (!el)
517
+ return;
518
+ const scrollTop = el.scrollTop;
519
+ const scrollLeft = el.scrollLeft;
520
+ const scrollHeight = el.scrollHeight;
521
+ const scrollWidth = el.scrollWidth;
522
+ const prev = untrack(() => scrollMetrics);
523
+ if (prev.scrollTop === scrollTop &&
524
+ prev.scrollLeft === scrollLeft &&
525
+ prev.scrollHeight === scrollHeight &&
526
+ prev.scrollWidth === scrollWidth) {
527
+ return;
528
+ }
529
+ scrollMetrics = { scrollTop, scrollLeft, scrollHeight, scrollWidth };
396
530
  });
397
531
  /** Vertical overflow from the virtualizer's authoritative total size,
398
532
  * NOT from `scrollMetrics.scrollHeight` alone. Reading DOM dimensions
@@ -454,7 +588,10 @@ export function createSvGridController(rawProps, domIdBase) {
454
588
  // re-synced whenever the parent passes a new array; the imperative API
455
589
  // mutates these so add/remove operations don't need a callback round-trip.
456
590
  // svelte-ignore state_referenced_locally
457
- let internalData = $state.raw(props.data);
591
+ // `data` is optional on the type (a `rowModel` may supply the rows
592
+ // instead), so every read falls back to empty rather than to undefined.
593
+ const EMPTY_DATA = [];
594
+ let internalData = $state.raw(props.data ?? EMPTY_DATA);
458
595
  // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
459
596
  // format defaults once, up front, so every downstream reader sees a normal
460
597
  // column. Explicit fields on the ColumnDef always win.
@@ -499,7 +636,7 @@ export function createSvGridController(rawProps, domIdBase) {
499
636
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
500
637
  // would keep returning the old edited values from `editedCellValues`
501
638
  // even though the underlying data has been replaced.
502
- internalData = props.data;
639
+ internalData = props.data ?? EMPTY_DATA;
503
640
  editedCellValues = {};
504
641
  });
505
642
  $effect(() => {
@@ -732,6 +869,20 @@ export function createSvGridController(rawProps, domIdBase) {
732
869
  *
733
870
  * Returns null when the cell should render nothing.
734
871
  */
872
+ /**
873
+ * Why a row has no data yet, or null when it does.
874
+ *
875
+ * Group rows are never placeholders, so the check is skipped for them:
876
+ * their `original` is a synthetic bucket, not a row the consumer handed
877
+ * us, and passing it to `rowPlaceholder` would ask a question about a row
878
+ * that does not exist.
879
+ */
880
+ function placeholderStateOf(row) {
881
+ const ask = props.rowPlaceholder;
882
+ if (!ask || isGroupRow(row))
883
+ return null;
884
+ return ask(row.original, row.index);
885
+ }
735
886
  function autoGroupCell(row, columnId) {
736
887
  if (!isGroupRow(row))
737
888
  return null;
@@ -1355,8 +1506,14 @@ export function createSvGridController(rawProps, domIdBase) {
1355
1506
  const paginationTotalRows = $derived(externalPaginationEnabled ? (props.rowCount ?? 0) : paginatedRowTotal);
1356
1507
  const paginationPageIndex = $derived(externalPaginationEnabled ? (props.pageIndex ?? 0) : paginationState.pageIndex);
1357
1508
  const paginationPageSize = $derived(externalPaginationEnabled ? (props.pageSize ?? 10) : paginationState.pageSize);
1509
+ // Tracks `dataStateVersion`, which the store subscription bumps when the
1510
+ // rowSelection slice (among others) changes - NOT the catch-all
1511
+ // `gridStateVersion`, which also bumps on every active-cell move. Every
1512
+ // rendered row and cell reads this (`sv-grid-row-selected`, `aria-selected`),
1513
+ // so on the catch-all each arrow key re-marked and re-checked ~400 effects
1514
+ // to find the same object.
1358
1515
  const rowSelectionState = $derived.by(function rowSelectionState_d() {
1359
- gridStateVersion;
1516
+ dataStateVersion;
1360
1517
  return grid.getState().rowSelection ?? {};
1361
1518
  });
1362
1519
  // ---- Selection bar ----------------------------------------------------
@@ -1388,16 +1545,44 @@ export function createSvGridController(rawProps, domIdBase) {
1388
1545
  const ids = [];
1389
1546
  if (!selectionBarOn)
1390
1547
  return { rows, ids };
1548
+ // Under an external selection model the loaded rows are the ones the
1549
+ // model says are selected; rows that never loaded are not in `allRows`
1550
+ // and are counted by `selectionBarCount` below instead. This walks
1551
+ // every loaded row, so it runs on every block a row model delivers:
1552
+ // a model that can say "nothing is selected" spares the walk, and the
1553
+ // props it needs are read once, not through the proxy per row.
1554
+ const model = props.rowSelectionModel;
1555
+ if (model && model.selectedCount?.() === 0)
1556
+ return { rows, ids };
1557
+ const ask = props.rowPlaceholder;
1558
+ const isPlaceholder = ask
1559
+ ? (row) => !isGroupRow(row) && !!ask(row.original, row.index)
1560
+ : () => false;
1391
1561
  for (const row of allRows) {
1392
- if (isGroupRow(row))
1562
+ if (isGroupRow(row) || isPlaceholder(row))
1393
1563
  continue;
1394
- if (rowSelectionState[row.id]) {
1564
+ const selected = model
1565
+ ? model.isSelected(row.id, row.original)
1566
+ : !!rowSelectionState[row.id];
1567
+ if (selected) {
1395
1568
  rows.push(row.original);
1396
1569
  ids.push(row.id);
1397
1570
  }
1398
1571
  }
1399
1572
  return { rows, ids };
1400
1573
  });
1574
+ /**
1575
+ * What the selection bar's count chip says. A model that stores the rule
1576
+ * ("everything except these") knows about rows the grid never loaded, so
1577
+ * its number wins over counting ticks on screen; `null` from it means it
1578
+ * cannot say, and the bar falls back to the loaded count.
1579
+ */
1580
+ const selectionBarCount = $derived.by(function selectionBarCount_d() {
1581
+ const fromModel = props.rowSelectionModel?.selectedCount?.();
1582
+ return fromModel ?? selectionBarTarget.ids.length;
1583
+ });
1584
+ /** Whether the bar has anything to show: the model's count when it has one, else the loaded ticks. */
1585
+ const selectionBarVisible = $derived(selectionBarOn && selectionBarCount > 0);
1401
1586
  // Forward selection changes to the consumer. Skips the very first invocation
1402
1587
  // (the initial empty state) so consumers don't get a spurious callback on mount.
1403
1588
  let lastSelectionSerialized = "";
@@ -1652,7 +1837,9 @@ export function createSvGridController(rawProps, domIdBase) {
1652
1837
  let chartMaximized = $state(false);
1653
1838
  let chartFloatRect = $state.raw(null);
1654
1839
  let chartAiHandler = $state(null);
1840
+ let chartExplainHandler = $state(null);
1655
1841
  let chartSeq = 0;
1842
+ const isZoomWindow = (v) => !!v && typeof v === "object" && Number.isInteger(v.i0) && Number.isInteger(v.i1);
1656
1843
  const makeChart = (title) => ({
1657
1844
  id: `chart-${chartSeq++}`,
1658
1845
  title,
@@ -1671,10 +1858,50 @@ export function createSvGridController(rawProps, domIdBase) {
1671
1858
  logScale: null,
1672
1859
  timeAxis: null,
1673
1860
  valueFormat: null,
1861
+ bucket: null,
1862
+ bins: null,
1863
+ funnelShape: null,
1864
+ candleStyle: null,
1865
+ zoom: null,
1866
+ ohlc: null,
1867
+ indicators: [],
1868
+ format: null,
1869
+ frozen: null,
1674
1870
  });
1871
+ // The saved view (`GridState.charts[i]`) names four fields differently
1872
+ // from the tab; everything else is the tab's own key.
1873
+ const TAB_ALIASES = { dimensionId: "dimension", measureId: "measure", seriesId: "series", measure2Id: "measure2" };
1874
+ /** A tab as the saved view carries it, without the runtime id. */
1875
+ const tabToState = (c) => {
1876
+ const out = {};
1877
+ for (const k in c)
1878
+ if (k !== "id")
1879
+ out[TAB_ALIASES[k] ?? k] = k === "seriesId" ? (c.seriesId ?? null) : c[k];
1880
+ return out;
1881
+ };
1882
+ /** A tab from a saved-view snapshot: a fresh tab with every known field
1883
+ * the snapshot carries, and the shape-checked ones dropped when malformed. */
1884
+ const tabFromState = (c, i) => {
1885
+ const t = makeChart(typeof c.title === "string" ? c.title : `Chart ${i + 1}`);
1886
+ for (const k in t) {
1887
+ const v = c[TAB_ALIASES[k] ?? k];
1888
+ if (k !== "id" && k !== "title" && v !== undefined)
1889
+ t[k] = v;
1890
+ }
1891
+ if (!isZoomWindow(t.zoom))
1892
+ t.zoom = null;
1893
+ if (!Array.isArray(t.indicators))
1894
+ t.indicators = [];
1895
+ if (typeof t.bins !== "number")
1896
+ t.bins = null;
1897
+ if (!t.frozen || typeof t.frozen !== "object" || !t.frozen.spec)
1898
+ t.frozen = null;
1899
+ return t;
1900
+ };
1675
1901
  // svelte-ignore state_referenced_locally
1676
1902
  let charts = $state([makeChart("Chart 1")]);
1677
1903
  let activeChartIndex = $state(0);
1904
+ let savedCharts = $state([]);
1678
1905
  const activeChart = $derived((charts[Math.min(activeChartIndex, charts.length - 1)] ?? charts[0]));
1679
1906
  const chartType = $derived(activeChart.type);
1680
1907
  const chartReduce = $derived(activeChart.reduce);
@@ -1685,6 +1912,10 @@ export function createSvGridController(rawProps, domIdBase) {
1685
1912
  const chartDataLabels = $derived(activeChart.dataLabels);
1686
1913
  const chartLogScale = $derived(activeChart.logScale);
1687
1914
  const chartTimeAxis = $derived(activeChart.timeAxis);
1915
+ const chartBucket = $derived(activeChart.bucket);
1916
+ const chartBins = $derived(activeChart.bins);
1917
+ const chartFunnelShape = $derived(activeChart.funnelShape);
1918
+ const chartCandleStyle = $derived(activeChart.candleStyle);
1688
1919
  const chartValueFormat = $derived(activeChart.valueFormat);
1689
1920
  const asMeasureList = (m) => m == null ? [] : Array.isArray(m) ? m : [m];
1690
1921
  const isNumericColumn = (col) => {
@@ -1802,9 +2033,14 @@ export function createSvGridController(rawProps, domIdBase) {
1802
2033
  const label = col ? columnLabel(col) : "";
1803
2034
  if (!label)
1804
2035
  return undefined;
1805
- return chartReduce === "count"
1806
- ? "Count"
1807
- : `${chartReduce === "avg" ? "Average" : "Sum"} of ${label}`;
2036
+ const word = {
2037
+ sum: "Sum", avg: "Average", min: "Min", max: "Max", median: "Median",
2038
+ first: "First", last: "Last", countDistinct: "Distinct",
2039
+ };
2040
+ if (chartReduce === "count")
2041
+ return "Count";
2042
+ const w = word[chartReduce] ?? (chartReduce.startsWith("p") ? `P${chartReduce.slice(1)}` : "Sum");
2043
+ return `${w} of ${label}`;
1808
2044
  });
1809
2045
  /**
1810
2046
  * Does this column hold dates? Declared type first, then a probe of the
@@ -1830,6 +2066,34 @@ export function createSvGridController(rawProps, domIdBase) {
1830
2066
  return false;
1831
2067
  }
1832
2068
  const chartDimensionIsDate = $derived.by(() => columnIsDate(allColumns.find((c) => c.id === effectiveChartDimensionId)));
2069
+ // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
2070
+ // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
2071
+ // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
2072
+ // null until it resolves, a tick after the panel itself starts loading (which it
2073
+ // already does). The `getAggregate` server path uses the local bucketsToSpec and
2074
+ // does not need the engine.
2075
+ // The whole module, not just `rowsToChartSpec`: the panel can now reach
2076
+ // every chart type, and the ones that do not read a categories-by-series
2077
+ // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
2078
+ // from the same lazy chunk. Same `import()`, same boundary.
2079
+ let chartEngine = $state(null);
2080
+ $effect(() => {
2081
+ if (chartingEnabled && !chartEngine)
2082
+ import("./chart.js").then((m) => (chartEngine = m));
2083
+ });
2084
+ const chartIsOhlc = $derived(chartType === "candlestick" || chartType === "ohlc");
2085
+ /** The price columns: what the reader picked, else the engine's guess
2086
+ * from the column names (nothing until the engine chunk has loaded). */
2087
+ const effectiveChartOhlc = $derived(chartEngine
2088
+ ? chartEngine.guessOhlcColumns(chartableColumns.measures, activeChart.ohlc)
2089
+ : { open: null, high: null, low: null, close: null, volume: null, ...activeChart.ohlc });
2090
+ /** The date column a candlestick reads: the picked dimension when it is a
2091
+ * date, else the first date column. */
2092
+ const effectiveChartOhlcDateId = $derived(chartDimensionIsDate ? effectiveChartDimensionId : (chartableColumns.dates[0]?.id ?? null));
2093
+ const chartIndicators = $derived(activeChart.indicators);
2094
+ /** The panes under a price chart and the overlay on it, split by the engine. */
2095
+ const chartIndicatorSplit = $derived(chartIsOhlc && chartEngine ? chartEngine.splitPanelIndicators(chartIndicators) : { panes: [], overlay: null });
2096
+ const chartIndicatorPanes = $derived(chartIndicatorSplit.panes);
1833
2097
  const chartRows = $derived.by(() => {
1834
2098
  const rects = getSelectionRects();
1835
2099
  const pushLeaf = (out, row) => {
@@ -1883,53 +2147,6 @@ export function createSvGridController(rawProps, domIdBase) {
1883
2147
  chartServerBuckets = [];
1884
2148
  });
1885
2149
  });
1886
- const bucketsToSpec = (buckets, type, hasSeries) => {
1887
- const cats = [];
1888
- const catIdx = new Map();
1889
- const ensureCat = (c) => {
1890
- let i = catIdx.get(c);
1891
- if (i === undefined) {
1892
- i = cats.length;
1893
- catIdx.set(c, i);
1894
- cats.push(c);
1895
- }
1896
- return i;
1897
- };
1898
- const seriesMap = new Map();
1899
- for (const b of buckets) {
1900
- const ci = ensureCat(b.category);
1901
- const key = hasSeries ? (b.series ?? "") : "value";
1902
- let arr = seriesMap.get(key);
1903
- if (!arr) {
1904
- arr = [];
1905
- seriesMap.set(key, arr);
1906
- }
1907
- arr[ci] = (arr[ci] ?? 0) + b.value;
1908
- }
1909
- return {
1910
- type,
1911
- categories: cats,
1912
- series: [...seriesMap.entries()].map(([label, values]) => ({
1913
- label,
1914
- values: cats.map((_, i) => values[i] ?? 0),
1915
- })),
1916
- };
1917
- };
1918
- // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
1919
- // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
1920
- // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
1921
- // null until it resolves, a tick after the panel itself starts loading (which it
1922
- // already does). The `getAggregate` server path uses the local bucketsToSpec and
1923
- // does not need the engine.
1924
- // The whole module, not just `rowsToChartSpec`: the panel can now reach
1925
- // every chart type, and the ones that do not read a categories-by-series
1926
- // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
1927
- // from the same lazy chunk. Same `import()`, same boundary.
1928
- let chartEngine = $state(null);
1929
- $effect(() => {
1930
- if (chartingEnabled && !chartEngine)
1931
- import("./chart.js").then((m) => (chartEngine = m));
1932
- });
1933
2150
  /** Put the chart's numbers in the same locale as the grid's. A grid that set
1934
2151
  * `localization.locale` has already said which locale its data is in, and a
1935
2152
  * chart of that data reading in a different one is a bug nobody would think
@@ -1942,13 +2159,13 @@ export function createSvGridController(rawProps, domIdBase) {
1942
2159
  if (chartCfg?.currency)
1943
2160
  spec.currency = chartCfg.currency;
1944
2161
  };
1945
- const chartSpec = $derived.by(() => {
2162
+ const chartSpecLive = $derived.by(() => {
1946
2163
  if (!chartingEnabled || !chartCfg)
1947
2164
  return null;
1948
2165
  if (chartCfg.getAggregate) {
1949
- if (!chartServerBuckets)
2166
+ if (!chartServerBuckets || !chartEngine)
1950
2167
  return null;
1951
- const spec = bucketsToSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
2168
+ const spec = chartEngine.bucketsToChartSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
1952
2169
  if (effectiveChartStacked || chartCfg.stacked100)
1953
2170
  spec.stacked = true;
1954
2171
  if (chartCfg.stacked100)
@@ -1957,6 +2174,8 @@ export function createSvGridController(rawProps, domIdBase) {
1957
2174
  spec.palette = chartCfg.palette;
1958
2175
  if (effectiveChartTimeAxis)
1959
2176
  spec.xType = "time";
2177
+ else if (chartBucket && chartDimensionIsDate)
2178
+ spec.xType = "ordinal-time";
1960
2179
  if (effectiveChartLogScale)
1961
2180
  spec.yScale = "log";
1962
2181
  if (effectiveChartValueFormat)
@@ -1966,55 +2185,110 @@ export function createSvGridController(rawProps, domIdBase) {
1966
2185
  }
1967
2186
  if (chartCfg.buildSpec)
1968
2187
  return chartCfg.buildSpec(chartRows) ?? null;
1969
- const category = fieldOf(effectiveChartDimensionId);
1970
- const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f) => !!f);
1971
- if (!category || !values.length)
1972
- return null;
1973
2188
  // Engine not loaded yet (lazy): render nothing until it resolves.
1974
2189
  if (!chartEngine)
1975
2190
  return null;
1976
- const seriesField = fieldOf(effectiveChartSeriesId);
1977
- const rowsForChart = chartRows;
1978
- // Some types read the rows directly rather than a grouped grid: a scatter
1979
- // point is one row, a gauge has no category axis at all, and a box plot
1980
- // needs the whole sample per group rather than one reduced number. The
1981
- // engine owns that dispatch - naming each type here would put them in the
1982
- // base bundle, which every grid pays for whether or not it ever charts.
1983
- // Everything else falls through to the aggregation below and is reshaped
1984
- // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
1985
- const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
1986
- category: category,
1987
- ...(values[0] ? { value: values[0] } : {}),
1988
- ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id) } : {}),
1989
- ...(seriesField ? { series: seriesField } : {}),
1990
- reduce: chartReduce,
1991
- ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
1992
- });
1993
- if (direct)
1994
- return direct;
1995
- // A direct type that could not build (scatter with no Y picked yet, for
1996
- // instance) must not fall through to the bar-chart path and draw the wrong
1997
- // chart - it renders the panel's empty state instead.
1998
- if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot")
1999
- return null;
2000
- const spec = chartEngine.rowsToChartSpec(rowsForChart, {
2001
- type: chartType,
2002
- category: category,
2003
- value: values.length === 1 ? values[0] : values,
2004
- ...(seriesField ? { series: seriesField } : {}),
2005
- reduce: chartReduce,
2006
- stacked: effectiveChartStacked || effectiveChartStacked100,
2007
- stacked100: effectiveChartStacked100,
2008
- ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2009
- ...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
2010
- ...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
2011
- ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
2012
- });
2191
+ // Shared by the direct and aggregated paths so the two cannot drift apart
2192
+ // again - which is how these came to be skipped for scatter, gauge and
2193
+ // boxplot in the first place. Declared here, at the derived's own scope:
2194
+ // a function declaration inside the `else` block below is block-scoped,
2195
+ // and the aggregated path calls it after that block has closed.
2196
+ function applyChartValueSettings(spec) {
2197
+ if (effectiveChartTimeAxis)
2198
+ spec.xType = "time";
2199
+ if (effectiveChartLogScale)
2200
+ spec.yScale = "log";
2201
+ if (effectiveChartValueFormat)
2202
+ spec.valueFormat = effectiveChartValueFormat;
2203
+ applyChartLocale(spec);
2204
+ return spec;
2205
+ }
2206
+ // Pivot mode: the pivot on screen is the chart, rows as categories and
2207
+ // columns as series; the pickers have nothing to choose.
2208
+ let spec;
2209
+ if (pivotResult) {
2210
+ spec = chartEngine.pivotResultToChartSpec(pivotResult, {
2211
+ type: chartType, stacked: effectiveChartStacked, stacked100: effectiveChartStacked100,
2212
+ measures: pivotConfig.values,
2213
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2214
+ });
2215
+ }
2216
+ else {
2217
+ const category = fieldOf(effectiveChartDimensionId);
2218
+ const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f) => !!f);
2219
+ if (!chartIsOhlc && (!category || !values.length))
2220
+ return null;
2221
+ const seriesField = fieldOf(effectiveChartSeriesId);
2222
+ const rowsForChart = chartRows;
2223
+ // Some types read the rows directly rather than a grouped grid: a scatter
2224
+ // point is one row, a gauge has no category axis at all, and a box plot
2225
+ // needs the whole sample per group rather than one reduced number. The
2226
+ // engine owns that dispatch - naming each type here would put them in the
2227
+ // base bundle, which every grid pays for whether or not it ever charts.
2228
+ // Everything else falls through to the aggregation below and is reshaped
2229
+ // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
2230
+ const ohlcOpts = chartIsOhlc
2231
+ ? chartEngine.ohlcDirectOptions(fieldOf, effectiveChartOhlcDateId, effectiveChartOhlc, { bucket: chartBucket, candleStyle: chartCandleStyle })
2232
+ : null;
2233
+ const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
2234
+ ...(ohlcOpts ?? {}),
2235
+ category: ohlcOpts?.category ?? category,
2236
+ ...(values[0] ? { value: values[0] } : {}),
2237
+ ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id) } : {}),
2238
+ ...(seriesField ? { series: seriesField } : {}),
2239
+ reduce: chartReduce,
2240
+ ...(chartBins ? { bins: chartBins } : {}),
2241
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2242
+ });
2243
+ // Number formatting and the scale/axis-type settings are about how values
2244
+ // READ, so they apply to a scatter or a gauge exactly as they do to a bar
2245
+ // chart. They used to live only on the aggregated path below, so setting
2246
+ // `valueFormat` or `logScale` on a direct type silently did nothing.
2247
+ //
2248
+ // The series-shaped settings (trend, averageLine, seriesTypes) stay on the
2249
+ // aggregated path: a direct spec does not have the series shape they read.
2250
+ if (direct) {
2251
+ if (chartIsOhlc) {
2252
+ // Overlays ride on the price series; the panes are composed by the
2253
+ // panel body from `chartIndicatorPanes`.
2254
+ const overlay = chartIndicatorSplit.overlay;
2255
+ if (overlay && direct.series[0])
2256
+ direct.series[0].overlay = overlay;
2257
+ }
2258
+ return applyChartValueSettings(direct);
2259
+ }
2260
+ // A direct type that could not build (scatter with no Y picked yet, for
2261
+ // instance) must not fall through to the bar-chart path and draw the wrong
2262
+ // chart - it renders the panel's empty state instead.
2263
+ if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot" ||
2264
+ chartType === "histogram" || chartType === "range-bar" || chartType === "range-area" ||
2265
+ chartType === "dumbbell" || chartType === "bullet" || chartIsOhlc)
2266
+ return null;
2267
+ spec = chartEngine.rowsToChartSpec(rowsForChart, {
2268
+ type: chartType,
2269
+ category: category,
2270
+ value: values.length === 1 ? values[0] : values,
2271
+ ...(seriesField ? { series: seriesField } : {}),
2272
+ reduce: chartReduce,
2273
+ ...(chartBucket && chartDimensionIsDate ? { bucket: chartBucket } : {}),
2274
+ stacked: effectiveChartStacked || effectiveChartStacked100,
2275
+ stacked100: effectiveChartStacked100,
2276
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2277
+ ...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
2278
+ ...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
2279
+ ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
2280
+ });
2281
+ }
2013
2282
  // Reshape the aggregated grid into whatever this type actually reads.
2014
2283
  // These run AFTER the aggregation so grouping, reduce, sort, topN and the
2015
2284
  // "Other" bucket apply to them exactly as they do to a bar chart.
2016
- if (chartType === "treemap")
2285
+ if (chartType === "treemap" || chartType === "sunburst")
2017
2286
  spec.treemap = chartEngine.specToTreemap(spec);
2287
+ else if (chartType === "chord") {
2288
+ const flow = chartEngine.specToSankey(spec);
2289
+ spec.sankeyNodes = flow.nodes;
2290
+ spec.sankeyLinks = flow.links;
2291
+ }
2018
2292
  else if (chartType === "calendar")
2019
2293
  spec.calendarValues = chartEngine.specToCalendar(spec);
2020
2294
  else if (chartType === "sankey") {
@@ -2024,13 +2298,7 @@ export function createSvGridController(rawProps, domIdBase) {
2024
2298
  }
2025
2299
  if (effectiveChartOrientation === "horizontal")
2026
2300
  spec.orientation = "horizontal";
2027
- if (effectiveChartTimeAxis)
2028
- spec.xType = "time";
2029
- if (effectiveChartLogScale)
2030
- spec.yScale = "log";
2031
- if (effectiveChartValueFormat)
2032
- spec.valueFormat = effectiveChartValueFormat;
2033
- applyChartLocale(spec);
2301
+ applyChartValueSettings(spec);
2034
2302
  if (chartType !== "pie" && spec.orientation !== "horizontal" && !spec.yAxisTitle && chartAutoYAxisTitle) {
2035
2303
  spec.yAxisTitle = chartAutoYAxisTitle;
2036
2304
  }
@@ -2043,6 +2311,16 @@ export function createSvGridController(rawProps, domIdBase) {
2043
2311
  spec.annotations = chartCfg.annotations;
2044
2312
  if (chartCfg.patternFallback)
2045
2313
  spec.patternFallback = true;
2314
+ if (chartFunnelShape && chartType === "funnel")
2315
+ spec.funnelShape = chartFunnelShape;
2316
+ if (chartCandleStyle && (chartType === "candlestick" || chartType === "ohlc"))
2317
+ spec.candleStyle = chartCandleStyle;
2318
+ // A stream is a stacked area on a wiggle baseline; the picker's Stacked
2319
+ // box has nothing to add, so the offset is applied here outright.
2320
+ if (chartType === "stream") {
2321
+ spec.stacked = true;
2322
+ spec.stackOffset = "wiggle";
2323
+ }
2046
2324
  const refLines = [...(chartCfg.referenceLines ?? [])];
2047
2325
  if (chartCfg.averageLine && spec.series[0]?.values.length) {
2048
2326
  const vals = spec.series[0].values;
@@ -2052,8 +2330,10 @@ export function createSvGridController(rawProps, domIdBase) {
2052
2330
  if (refLines.length)
2053
2331
  spec.referenceLines = refLines;
2054
2332
  for (const s of spec.series) {
2055
- if (chartCfg.trend)
2056
- s.overlay = chartCfg.trend === "linear" ? "linear" : `${chartCfg.trend}:7`;
2333
+ if (chartCfg.trend) {
2334
+ const t = chartCfg.trend;
2335
+ s.overlay = t === "sma" || t === "ema" ? `${t}:7` : t === "poly" ? "poly:2" : t;
2336
+ }
2057
2337
  if (chartCfg.smooth)
2058
2338
  s.smooth = true;
2059
2339
  if (chartCfg.seriesTypes?.[s.label])
@@ -2063,6 +2343,51 @@ export function createSvGridController(rawProps, domIdBase) {
2063
2343
  }
2064
2344
  return spec;
2065
2345
  });
2346
+ /**
2347
+ * What the panel renders: an unlinked chart's frozen snapshot, else the live
2348
+ * derivation with the builder's format applied. The format runs through
2349
+ * the lazy engine so its code stays out of the base bundle.
2350
+ */
2351
+ const chartSpec = $derived.by(() => {
2352
+ if (activeChart.frozen)
2353
+ return activeChart.frozen.spec;
2354
+ const live = chartSpecLive;
2355
+ if (!live || !activeChart.format || !chartEngine)
2356
+ return live;
2357
+ return chartEngine.applyChartFormat(live, activeChart.format);
2358
+ });
2359
+ let announcedFirstChart = false;
2360
+ $effect(() => {
2361
+ if (!chartPanelOpen || announcedFirstChart || !chartCfg?.onChartCreated)
2362
+ return;
2363
+ announcedFirstChart = true;
2364
+ const c = charts[0];
2365
+ untrack(() => chartCfg?.onChartCreated?.({ index: 0, title: c.title, type: c.type, spec: null }));
2366
+ });
2367
+ // Lifecycle: one `onChartChanged` per burst of spec changes.
2368
+ let chartChangeTimer = null;
2369
+ $effect(() => {
2370
+ const cb = chartCfg?.onChartChanged;
2371
+ const spec = chartSpec;
2372
+ const index = activeChartIndex;
2373
+ if (!cb)
2374
+ return;
2375
+ untrack(() => {
2376
+ if (chartChangeTimer)
2377
+ clearTimeout(chartChangeTimer);
2378
+ chartChangeTimer = setTimeout(() => {
2379
+ chartChangeTimer = null;
2380
+ const c = charts[index] ?? activeChart;
2381
+ cb({ index, title: c.title, type: c.type, spec });
2382
+ }, 150);
2383
+ });
2384
+ return () => {
2385
+ if (chartChangeTimer) {
2386
+ clearTimeout(chartChangeTimer);
2387
+ chartChangeTimer = null;
2388
+ }
2389
+ };
2390
+ });
2066
2391
  /**
2067
2392
  * Can a click on this chart be turned back into a grid filter?
2068
2393
  *
@@ -2071,11 +2396,16 @@ export function createSvGridController(rawProps, domIdBase) {
2071
2396
  * SOURCE column by a target value and empty the grid), a gauge (no category
2072
2397
  * at all), or a scatter (the mark carries a point label, not a group).
2073
2398
  */
2074
- const chartCrossFilterable = $derived(chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter");
2399
+ const chartCrossFilterable = $derived(chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter" &&
2400
+ // A chord's arcs are both ends of a flow, a sunburst's are a hierarchy
2401
+ // and a histogram's are bins: none maps a click back to one dimension value.
2402
+ chartType !== "chord" && chartType !== "sunburst" && chartType !== "histogram");
2403
+ /** The column a chart click filters: the picked dimension, or in pivot mode the innermost row dimension when it is a grid column. */
2404
+ const chartFilterDimId = $derived(pivotResult ? (chartEngine?.pivotFilterColumn(pivotConfig.rows, chartableColumns.dims, fieldOf) ?? null) : effectiveChartDimensionId);
2075
2405
  function applyChartCrossFilter(category) {
2076
2406
  if (!chartCrossFilterable)
2077
2407
  return;
2078
- const dimId = effectiveChartDimensionId;
2408
+ const dimId = chartFilterDimId;
2079
2409
  if (!dimId)
2080
2410
  return;
2081
2411
  const existing = valueFilters[dimId];
@@ -2087,7 +2417,7 @@ export function createSvGridController(rawProps, domIdBase) {
2087
2417
  valueFilters = { ...valueFilters, [dimId]: next };
2088
2418
  }
2089
2419
  function clearChartCrossFilter() {
2090
- const dimId = effectiveChartDimensionId;
2420
+ const dimId = chartFilterDimId;
2091
2421
  if (!dimId)
2092
2422
  return;
2093
2423
  const next = { ...valueFilters };
@@ -2204,6 +2534,59 @@ export function createSvGridController(rawProps, domIdBase) {
2204
2534
  const virtualRowStart = $derived.by(() => virtualRows[0]?.start ?? 0);
2205
2535
  const virtualRowEnd = $derived.by(() => virtualRows[virtualRows.length - 1]?.end ?? 0);
2206
2536
  const virtualRowBottomSpacer = $derived.by(() => Math.max(virtualRowTotalSize - virtualRowEnd, 0));
2537
+ // A row model tells us when to re-read it. Subscribing here (rather than
2538
+ // polling, or asking the consumer to wrap the model in a rune) is what lets
2539
+ // `getRows()` be a plain method call on a plain object.
2540
+ $effect(() => {
2541
+ const model = rawProps.rowModel;
2542
+ if (!model)
2543
+ return;
2544
+ return model.subscribe(() => {
2545
+ rowModelVersion += 1;
2546
+ });
2547
+ });
2548
+ // The index range on screen, for data sources that fetch by block.
2549
+ // `virtualRowStart` / `virtualRowEnd` above are PIXEL offsets; a fetcher
2550
+ // needs row numbers, and needs them whether or not virtualization is on.
2551
+ const visibleRowRange = $derived.by(function visibleRowRange_d() {
2552
+ if (!rowVirtualizationEnabled) {
2553
+ return { startIndex: 0, endIndex: Math.max(0, allRows.length - 1) };
2554
+ }
2555
+ const first = virtualRows[0];
2556
+ const last = virtualRows[virtualRows.length - 1];
2557
+ if (!first || !last)
2558
+ return { startIndex: 0, endIndex: 0 };
2559
+ return { startIndex: first.index, endIndex: last.index };
2560
+ });
2561
+ // Emitted from an effect rather than the derived so a consumer that
2562
+ // fetches (and therefore changes `data`) cannot re-enter the derivation.
2563
+ let lastReportedRange = "";
2564
+ $effect(() => {
2565
+ const report = props.onVisibleRangeChange;
2566
+ if (!report)
2567
+ return;
2568
+ const range = visibleRowRange;
2569
+ const key = range.startIndex + ":" + range.endIndex;
2570
+ if (key === lastReportedRange)
2571
+ return;
2572
+ lastReportedRange = key;
2573
+ report(range);
2574
+ });
2575
+ // A paging row model that asks for `autoPageSize` gets as many rows as
2576
+ // the body shows without scrolling, re-measured as the viewport changes.
2577
+ $effect(() => {
2578
+ rowModelVersion;
2579
+ const paging = rawProps.rowModel?.pagination;
2580
+ if (!paging?.autoPageSize)
2581
+ return;
2582
+ const height = viewportHeight;
2583
+ if (height <= 0)
2584
+ return;
2585
+ const rowPx = rowSizePxOf(0) || 30;
2586
+ const fit = Math.max(1, Math.floor(height / rowPx));
2587
+ if (fit !== paging.pageSize)
2588
+ paging.setPageSize(fit);
2589
+ });
2207
2590
  // --- Huge-list scroll scaling -----------------------------------------
2208
2591
  // Browsers cap how tall a single element may be, and mobile caps sit well
2209
2592
  // below desktop. Past a few hundred thousand rows the true content height
@@ -2246,9 +2629,50 @@ export function createSvGridController(rawProps, domIdBase) {
2246
2629
  const logical = virtualizer.getState().scrollOffset;
2247
2630
  return logical - rowScrollScaling.logicalToDom(logical);
2248
2631
  });
2632
+ /**
2633
+ * The first `frozenRows` rows are rendered ahead of the virtual window
2634
+ * and stick under the header. The virtualizer still counts them, so its
2635
+ * offsets stay in row-index space and nothing downstream has to translate;
2636
+ * the window skips them, and the top spacer gives back the height they
2637
+ * already occupy in the flow.
2638
+ */
2639
+ const frozenRowCount = $derived(Math.max(0, Math.min(Math.floor(props.frozenRows ?? 0), allRows.length)));
2640
+ const frozenRowList = $derived(frozenRowCount ? allRows.slice(0, frozenRowCount) : []);
2641
+ /** The merged cells, indexed; null (the usual case) costs nothing. */
2642
+ const mergeIndex = $derived(buildMergeIndex(props.mergedCells));
2643
+ const frozenBandHeight = $derived.by(() => {
2644
+ if (!frozenRowCount)
2645
+ return 0;
2646
+ rowResizeVersion;
2647
+ autoRowHeightVersion;
2648
+ let total = 0;
2649
+ for (let i = 0; i < frozenRowCount; i += 1)
2650
+ total += rowSizePxOf(i);
2651
+ return total;
2652
+ });
2653
+ /** The declared or dragged height of one row; measured under auto height. */
2654
+ function rowSizePxOf(i) {
2655
+ if (isRowCollapsed(i))
2656
+ return 0;
2657
+ if (autoRowHeightOn)
2658
+ return measuredRowHeights.get(i) ?? autoRowHeightFallback;
2659
+ const own = rowResizeHeightPx(i);
2660
+ if (own != null)
2661
+ return own;
2662
+ const rh = props.rowHeight;
2663
+ return typeof rh === "function" ? rh(i) : (rh ?? 30);
2664
+ }
2665
+ /** Where frozen row i sticks: under the header and the frozen rows above it. */
2666
+ function frozenRowTop(i) {
2667
+ let top = headerHeight;
2668
+ for (let k = 0; k < i; k += 1)
2669
+ top += rowSizePxOf(k);
2670
+ return top;
2671
+ }
2249
2672
  // Spacer heights in DOM space. With scaling inert these equal the original
2250
- // virtualRowStart / virtualRowBottomSpacer.
2251
- const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment, 0));
2673
+ // virtualRowStart / virtualRowBottomSpacer. The frozen band is in the flow
2674
+ // before the spacer whatever the window shows, so it comes off the spacer.
2675
+ const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment - frozenBandHeight, 0));
2252
2676
  const rowBottomSpacer = $derived(Math.max(rowDomTotalSize - (virtualRowEnd - rowOffsetAdjustment), 0));
2253
2677
  const virtualColumns = $derived.by(function virtualColumns_d() {
2254
2678
  columnVirtualizerVersion;
@@ -2585,35 +3009,117 @@ export function createSvGridController(rawProps, domIdBase) {
2585
3009
  let autoRowHeightVersion = $state(0);
2586
3010
  const autoRowHeightFallback = $derived(typeof props.rowHeight === "number" ? props.rowHeight : 30);
2587
3011
  // ----- Interactive row resize (`rowResize` prop) -----
2588
- // Heights the user has dragged, by row index. A plain Map plus a version
2589
- // counter, exactly like `measuredRowHeights` above and for the same reason:
2590
- // the reset effect below has to read this collection to know whether there is
2591
- // anything to clear, and if the collection were reactive that read would make
2592
- // every resize re-trigger the reset and wipe the height it just stored.
3012
+ // Heights the user has dragged, or set through `api.setRowHeight`, keyed
3013
+ // by ROW ID so they belong to the row rather than to a slot: the height
3014
+ // follows its row through a sort or a filter and survives the data being
3015
+ // replaced. They used to be keyed by index and wiped on every data change,
3016
+ // which in a spreadsheet meant every keystroke, so a row dragged taller
3017
+ // snapped back the moment the next cell was typed. A plain Map plus a
3018
+ // version counter, like `measuredRowHeights` above: the readers touch the
3019
+ // version, and the Map itself stays out of the reactive graph.
2593
3020
  const rowResizeHeights = new Map();
2594
3021
  let rowResizeVersion = $state(0);
2595
- function setRowResizeHeight(index, height) {
2596
- if (!rowResizeOn || !Number.isFinite(index) || height <= 0)
3022
+ /**
3023
+ * Rows folded to nothing, the row-side twin of `collapsedColumns`: the
3024
+ * row keeps its index and its cells and takes no height. Keyed by row id
3025
+ * like the heights and announced through the same version counter, so
3026
+ * every reader of a row's size sees the change.
3027
+ */
3028
+ const collapsedRows = new Set();
3029
+ function isRowCollapsed(index) {
3030
+ rowResizeVersion;
3031
+ const id = allRows[index]?.id;
3032
+ return id != null && collapsedRows.has(id);
3033
+ }
3034
+ function setRowCollapsed(index, collapsed) {
3035
+ const id = allRows[index]?.id;
3036
+ if (id == null)
3037
+ return;
3038
+ if (collapsed === collapsedRows.has(id))
2597
3039
  return;
2598
- rowResizeHeights.set(index, Math.round(height));
3040
+ if (collapsed)
3041
+ collapsedRows.add(id);
3042
+ else
3043
+ collapsedRows.delete(id);
2599
3044
  rowResizeVersion += 1;
2600
3045
  }
2601
- /** A user-dragged height for this row, or undefined when it has not been
2602
- * resized. Read by the view AND by the virtualizer's `estimateSize`. */
2603
- function rowResizeHeightPx(index) {
2604
- return rowResizeOn ? rowResizeHeights.get(index) : undefined;
3046
+ /**
3047
+ * A size the user changed by hand is one undo step, as it is in a
3048
+ * spreadsheet: Ctrl+Z after a column drag puts the width back. Recorded
3049
+ * only for the user's gestures; the api's own setters record nothing, so
3050
+ * a consumer restoring saved sizes does not fill the history.
3051
+ */
3052
+ function recordSizeUndo(undo, redo) {
3053
+ pushHistory(ctx, [
3054
+ { rowId: "", columnId: "", field: "", before: undefined, after: undefined, custom: { undo, redo } },
3055
+ ]);
2605
3056
  }
2606
- // A row index means a different row once the data changes, so keeping the
2607
- // heights would size new rows by the old ones - the same reasoning as the
2608
- // measured-height reset below.
2609
- $effect(() => {
2610
- void allRows.length;
2611
- void internalData;
2612
- if (rowResizeHeights.size === 0)
3057
+ // The undo and redo of a size step report through the same callbacks as
3058
+ // the gesture did: a consumer keeping the sizes (a sheet saving its
3059
+ // document) would otherwise hold the width the user dragged to while the
3060
+ // grid showed the one Ctrl+Z put back.
3061
+ function resizeColumnByUser(columnId, width, before) {
3062
+ const apply = (w) => {
3063
+ columnWidths = { ...columnWidths, [columnId]: w };
3064
+ props.onColumnResize?.({ columnId, width: w });
3065
+ };
3066
+ apply(width);
3067
+ if (before !== width)
3068
+ recordSizeUndo(() => apply(before), () => apply(width));
3069
+ }
3070
+ function autosizeColumnByUser(columnId) {
3071
+ const before = getColumnWidth(columnId);
3072
+ autosizeColumn(columnId);
3073
+ const width = getColumnWidth(columnId);
3074
+ const apply = (w) => {
3075
+ columnWidths = { ...columnWidths, [columnId]: w };
3076
+ props.onColumnResize?.({ columnId, width: w });
3077
+ };
3078
+ if (before !== width)
3079
+ recordSizeUndo(() => apply(before), () => apply(width));
3080
+ else
3081
+ props.onColumnResize?.({ columnId, width });
3082
+ }
3083
+ function resizeRowByUser(index, height) {
3084
+ const before = rowResizeHeightPx(index) ?? null;
3085
+ const apply = (h) => {
3086
+ setRowResizeHeight(index, h);
3087
+ props.onRowResize?.({ rowIndex: index, height: h });
3088
+ };
3089
+ apply(height);
3090
+ if (before !== height)
3091
+ recordSizeUndo(() => apply(before), () => apply(height));
3092
+ }
3093
+ function setRowResizeHeight(index, height) {
3094
+ const id = allRows[index]?.id;
3095
+ if (id == null)
2613
3096
  return;
2614
- rowResizeHeights.clear();
3097
+ if (height == null) {
3098
+ if (!rowResizeHeights.delete(id))
3099
+ return;
3100
+ }
3101
+ else {
3102
+ if (!Number.isFinite(height) || height <= 0)
3103
+ return;
3104
+ rowResizeHeights.set(id, Math.round(height));
3105
+ }
2615
3106
  rowResizeVersion += 1;
2616
- });
3107
+ }
3108
+ /** A row's own height, or undefined when it has none. Read by the view AND
3109
+ * by the virtualizer's `estimateSize`. Auto height wins over it: the
3110
+ * content decides the height there, and a manual one would be overwritten
3111
+ * by the next measurement. */
3112
+ function rowResizeHeightPx(index) {
3113
+ const id = allRows[index]?.id;
3114
+ if (id == null)
3115
+ return undefined;
3116
+ // A collapsed row is 0 whatever else it has, under auto height too.
3117
+ if (collapsedRows.has(id))
3118
+ return 0;
3119
+ if (autoRowHeightOn)
3120
+ return undefined;
3121
+ return rowResizeHeights.get(id);
3122
+ }
2617
3123
  /** Report a row's measured height. Called by the view's measuring action.
2618
3124
  * Sub-pixel churn is ignored so a fractional layout can't loop. */
2619
3125
  function reportRowHeight(index, height) {
@@ -2638,12 +3144,20 @@ export function createSvGridController(rawProps, domIdBase) {
2638
3144
  function measureRowHeight(node, index) {
2639
3145
  let current = index;
2640
3146
  let stop = null;
2641
- const measure = () => reportRowHeight(current, node.getBoundingClientRect().height);
3147
+ // Read the rect only under autoRowHeight. `getBoundingClientRect` forces a
3148
+ // synchronous layout, and the virtualizer's slot keys hand every rendered
3149
+ // <tr> a new index on each one-row scroll, so measuring unconditionally
3150
+ // meant ~30 forced layouts per arrow key on a fixed-height grid whose
3151
+ // measurement `reportRowHeight` then threw away.
3152
+ const measure = () => {
3153
+ if (!autoRowHeightOn)
3154
+ return;
3155
+ reportRowHeight(current, node.getBoundingClientRect().height);
3156
+ };
2642
3157
  const attach = () => {
2643
3158
  stop?.();
2644
3159
  stop = autoRowHeightOn ? observeSizeRaf(node, measure) : null;
2645
- if (autoRowHeightOn)
2646
- measure();
3160
+ measure();
2647
3161
  };
2648
3162
  attach();
2649
3163
  return {
@@ -2694,8 +3208,8 @@ export function createSvGridController(rawProps, domIdBase) {
2694
3208
  : typeof rh === "function"
2695
3209
  ? rh
2696
3210
  : (rh ?? 30);
2697
- const estimateSize = rowResizeOn
2698
- ? (index) => rowResizeHeights.get(index) ??
3211
+ const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0
3212
+ ? (index) => rowResizeHeightPx(index) ??
2699
3213
  (typeof base === "function" ? base(index) : base)
2700
3214
  : base;
2701
3215
  virtualizer.setOptions({
@@ -2755,6 +3269,7 @@ export function createSvGridController(rawProps, domIdBase) {
2755
3269
  // closure each run; the virtualizer sees a new function reference and
2756
3270
  // re-derives its layout from the current per-column widths.
2757
3271
  columnWidths;
3272
+ collapsedColumns;
2758
3273
  columnVirtualizer.setOptions({
2759
3274
  count: allColumns.length,
2760
3275
  estimateSize: (index) => {
@@ -2795,9 +3310,17 @@ export function createSvGridController(rawProps, domIdBase) {
2795
3310
  // Lets the resolver answer from cache without re-invoking the source, so an
2796
3311
  // async source fires one request per row instead of one per render.
2797
3312
  const asyncEditorColumns = new Set();
3313
+ // Depends on `allRows` and `rowSelectionState` only. It used to read the
3314
+ // catch-all `gridStateVersion` too, which made every arrow key walk all the
3315
+ // rows (10k iterations on the large-dataset demo, 100k on its top tier) to
3316
+ // recount a selection that had not changed.
2798
3317
  const headerSelectionState = $derived.by(function headerSelectionState_d() {
2799
- gridStateVersion;
2800
- const selectable = allRows.filter((row) => !isGroupRow(row));
3318
+ // An external model knows about rows the grid has never seen, so it is
3319
+ // the only thing that can answer honestly once the data is a window
3320
+ // onto a server.
3321
+ if (props.rowSelectionModel)
3322
+ return props.rowSelectionModel.headerState();
3323
+ const selectable = allRows.filter((row) => !isGroupRow(row) && !placeholderStateOf(row));
2801
3324
  if (!selectable.length)
2802
3325
  return "none";
2803
3326
  let selected = 0;
@@ -2815,6 +3338,10 @@ export function createSvGridController(rawProps, domIdBase) {
2815
3338
  * from a user-focused (0,0). The fill handle keys off this flag so it
2816
3339
  * stays hidden until the user actually selects something. */
2817
3340
  let userHasActivatedCell = $state(false);
3341
+ /** The column a run of Tabs began in, so the Enter that ends the run goes
3342
+ * down from there (`getEntryStep`). Plain, not state: nothing renders
3343
+ * it. `setActiveCell` clears it; the Tab and Enter paths set it back. */
3344
+ let tabRunOrigin = null;
2818
3345
  /**
2819
3346
  * Per-column fitted widths when `fitColumns` is on. Computed in one pass
2820
3347
  * so the LAST auto-sized column can absorb the rounding residue and make
@@ -2837,7 +3364,7 @@ export function createSvGridController(rawProps, domIdBase) {
2837
3364
  return null;
2838
3365
  // Exclude columns hidden by a collapsed group too (#57) - otherwise their
2839
3366
  // widths stay in the fit distribution and leave a blank gap at the edge.
2840
- const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id]);
3367
+ const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id] && !collapsedColumns[c.id]);
2841
3368
  if (!cols.length)
2842
3369
  return null;
2843
3370
  const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
@@ -2946,7 +3473,7 @@ export function createSvGridController(rawProps, domIdBase) {
2946
3473
  const meta = isBucketableColumn(column);
2947
3474
  if (!meta)
2948
3475
  continue;
2949
- const buckets = buildBuckets(column, meta.isDate, props.data, getColumnAccessorValue);
3476
+ const buckets = buildBuckets(column, meta.isDate, props.data ?? EMPTY_DATA, getColumnAccessorValue);
2950
3477
  if (buckets)
2951
3478
  map.set(column.id, buckets);
2952
3479
  }
@@ -3170,6 +3697,17 @@ export function createSvGridController(rawProps, domIdBase) {
3170
3697
  // state mutation inside the callback (e.g. `api.setGroupBy(...)`) created
3171
3698
  // an infinite update loop. Now it's a true mount-once notification.
3172
3699
  let apiNotified = false;
3700
+ // Memoized: `buildApi()` builds a fresh object each call, and a feature pack
3701
+ // that keys per-grid state on the api (a WeakMap in @svgrid/enterprise) needs
3702
+ // the object the consumer received and the object a command handler sees to
3703
+ // be the same one. Every method reads through `ctx`, so one instance stays
3704
+ // correct for the life of the grid.
3705
+ let apiInstance = null;
3706
+ function api() {
3707
+ if (!apiInstance)
3708
+ apiInstance = buildApi();
3709
+ return apiInstance;
3710
+ }
3173
3711
  $effect(() => {
3174
3712
  if (apiNotified)
3175
3713
  return;
@@ -3177,7 +3715,7 @@ export function createSvGridController(rawProps, domIdBase) {
3177
3715
  if (!cb)
3178
3716
  return;
3179
3717
  apiNotified = true;
3180
- cb(buildApi());
3718
+ cb(api());
3181
3719
  });
3182
3720
  const ctx = {
3183
3721
  get props() { return props; },
@@ -3239,6 +3777,8 @@ export function createSvGridController(rawProps, domIdBase) {
3239
3777
  set history(v) { history = v; },
3240
3778
  get historyPtr() { return historyPtr; },
3241
3779
  set historyPtr(v) { historyPtr = v; },
3780
+ get historyGroupId() { return historyGroupId; },
3781
+ set historyGroupId(v) { historyGroupId = v; },
3242
3782
  get historyVersion() { return historyVersion; },
3243
3783
  set historyVersion(v) { historyVersion = v; },
3244
3784
  get tooltip() { return tooltip; },
@@ -3261,6 +3801,9 @@ export function createSvGridController(rawProps, domIdBase) {
3261
3801
  get editorSelectAll() { return editorSelectAll; },
3262
3802
  set editorSelectAll(v) { editorSelectAll = v; },
3263
3803
  get columnWidths() { return columnWidths; },
3804
+ get collapsedColumns() { return collapsedColumns; },
3805
+ get mergeIndex() { return mergeIndex; },
3806
+ get setColumnCollapsed() { return setColumnCollapsed; },
3264
3807
  set columnWidths(v) { columnWidths = v; },
3265
3808
  get MIN_COLUMN_WIDTH() { return MIN_COLUMN_WIDTH; },
3266
3809
  get columnPinning() { return columnPinning; },
@@ -3332,6 +3875,7 @@ export function createSvGridController(rawProps, domIdBase) {
3332
3875
  get moveCellsEffective() { return moveCellsEffective; },
3333
3876
  get flushScheduledScrollSync() { return flushScheduledScrollSync; },
3334
3877
  get scheduleScrollSync() { return scheduleScrollSync; },
3878
+ get syncScrollNow() { return syncScrollNow; },
3335
3879
  get internalData() { return internalData; },
3336
3880
  set internalData(v) { internalData = v; },
3337
3881
  get internalColumns() { return internalColumns; },
@@ -3417,6 +3961,8 @@ export function createSvGridController(rawProps, domIdBase) {
3417
3961
  get selectionBarMaxVisible() { return selectionBarMaxVisible; },
3418
3962
  get selectionBarHideClear() { return selectionBarHideClear; },
3419
3963
  get selectionBarTarget() { return selectionBarTarget; },
3964
+ get selectionBarCount() { return selectionBarCount; },
3965
+ get selectionBarVisible() { return selectionBarVisible; },
3420
3966
  get lastSelectionSerialized() { return lastSelectionSerialized; },
3421
3967
  set lastSelectionSerialized(v) { lastSelectionSerialized = v; },
3422
3968
  get lastCellRangeSerialized() { return lastCellRangeSerialized; },
@@ -3435,6 +3981,8 @@ export function createSvGridController(rawProps, domIdBase) {
3435
3981
  get moveColumnInPanel() { return moveColumnInPanel; },
3436
3982
  // ---- Localized chrome strings ----
3437
3983
  get messages() { return gridMessages; },
3984
+ /** The raw `localization.text` map, for the lazy chart panel's own strings. */
3985
+ get localizationText() { return props.localization?.text; },
3438
3986
  // ---- In-grid pivot mode ----
3439
3987
  get pivotConfig() { return pivotConfig; },
3440
3988
  get pivotActive() { return pivotActive; },
@@ -3467,6 +4015,35 @@ export function createSvGridController(rawProps, domIdBase) {
3467
4015
  set chartLogScale(v) { activeChart.logScale = v; },
3468
4016
  get chartTimeAxis() { return effectiveChartTimeAxis; },
3469
4017
  set chartTimeAxis(v) { activeChart.timeAxis = v; },
4018
+ get chartBucket() { return chartBucket; },
4019
+ set chartBucket(v) { activeChart.bucket = v; },
4020
+ get chartBins() { return chartBins; },
4021
+ set chartBins(v) { activeChart.bins = v; },
4022
+ get chartFunnelShape() { return chartFunnelShape; },
4023
+ set chartFunnelShape(v) { activeChart.funnelShape = v; },
4024
+ get chartCandleStyle() { return chartCandleStyle; },
4025
+ set chartCandleStyle(v) { activeChart.candleStyle = v; },
4026
+ get chartFormat() { return activeChart.format; },
4027
+ set chartFormat(v) { activeChart.format = v; },
4028
+ get chartFrozen() { return activeChart.frozen; },
4029
+ get chartIsOhlc() { return chartIsOhlc; },
4030
+ get chartOhlc() { return effectiveChartOhlc; },
4031
+ set chartOhlc(v) { activeChart.ohlc = v; },
4032
+ get chartOhlcDateId() { return effectiveChartOhlcDateId; },
4033
+ get chartIndicators() { return chartIndicators; },
4034
+ set chartIndicators(v) { activeChart.indicators = v; },
4035
+ get chartIndicatorPanes() { return chartIndicatorPanes; },
4036
+ get chartZoom() { return activeChart.zoom; },
4037
+ // The panel keeps one chart component across tabs, so "tabs share a
4038
+ // window" is simply every tab carrying the same window: a zoom on one is
4039
+ // written to all, and switching tabs hands the component the same value.
4040
+ set chartZoom(v) {
4041
+ if (chartCfg?.syncTabs)
4042
+ for (const c of charts)
4043
+ c.zoom = v;
4044
+ else
4045
+ activeChart.zoom = v;
4046
+ },
3470
4047
  get chartValueFormat() { return effectiveChartValueFormat ?? "number"; },
3471
4048
  set chartValueFormat(v) { activeChart.valueFormat = v; },
3472
4049
  get chartDimensionIsDate() { return chartDimensionIsDate; },
@@ -3487,8 +4064,23 @@ export function createSvGridController(rawProps, domIdBase) {
3487
4064
  get activeChartIndex() { return Math.min(activeChartIndex, charts.length - 1); },
3488
4065
  set activeChartIndex(v) { activeChartIndex = Math.max(0, Math.min(charts.length - 1, v)); },
3489
4066
  addChart() {
3490
- charts = [...charts, makeChart(`Chart ${charts.length + 1}`)];
4067
+ const next = makeChart(`Chart ${charts.length + 1}`);
4068
+ if (chartCfg?.syncTabs)
4069
+ next.zoom = activeChart.zoom;
4070
+ charts = [...charts, next];
3491
4071
  activeChartIndex = charts.length - 1;
4072
+ chartCfg?.onChartCreated?.({ index: activeChartIndex, title: next.title, type: next.type, spec: null });
4073
+ },
4074
+ /** Unlink the active chart from the grid: it keeps the spec it has now. */
4075
+ freezeChart() {
4076
+ const spec = chartSpec;
4077
+ if (!spec)
4078
+ return;
4079
+ activeChart.frozen = { spec: JSON.parse(JSON.stringify(spec)), at: new Date().toISOString() };
4080
+ },
4081
+ /** Link the active chart back to the grid's rows. */
4082
+ unfreezeChart() {
4083
+ activeChart.frozen = null;
3492
4084
  },
3493
4085
  removeChart(index) {
3494
4086
  if (charts.length <= 1)
@@ -3504,48 +4096,40 @@ export function createSvGridController(rawProps, domIdBase) {
3504
4096
  c.title = title;
3505
4097
  },
3506
4098
  getChartsState() {
3507
- return charts.map((c) => ({
3508
- title: c.title,
3509
- type: c.type,
3510
- dimension: c.dimensionId,
3511
- series: c.seriesId ?? null,
3512
- measure: c.measureId,
3513
- measure2: c.measure2Id,
3514
- orientation: c.orientation,
3515
- stacked100: c.stacked100,
3516
- donut: c.donut,
3517
- palette: c.palette,
3518
- reduce: c.reduce,
3519
- stacked: c.stacked,
3520
- dataLabels: c.dataLabels,
3521
- logScale: c.logScale,
3522
- timeAxis: c.timeAxis,
3523
- valueFormat: c.valueFormat,
3524
- }));
4099
+ return charts.map(tabToState);
3525
4100
  },
3526
4101
  applyChartsState(list, active) {
3527
- const restored = list.map((c, i) => ({
3528
- id: `chart-${chartSeq++}`,
3529
- title: typeof c.title === "string" ? c.title : `Chart ${i + 1}`,
3530
- type: c.type ?? "bar",
3531
- reduce: c.reduce ?? "sum",
3532
- dimensionId: c.dimension ?? null,
3533
- measureId: c.measure ?? null,
3534
- seriesId: c.series,
3535
- measure2Id: c.measure2 ?? null,
3536
- orientation: c.orientation ?? null,
3537
- stacked100: c.stacked100 ?? null,
3538
- donut: c.donut ?? null,
3539
- palette: c.palette ?? null,
3540
- stacked: c.stacked ?? null,
3541
- dataLabels: c.dataLabels ?? null,
3542
- logScale: c.logScale ?? null,
3543
- timeAxis: c.timeAxis ?? null,
3544
- valueFormat: c.valueFormat ?? null,
3545
- }));
4102
+ const restored = list.map(tabFromState);
3546
4103
  charts = restored.length ? restored : [makeChart("Chart 1")];
3547
4104
  activeChartIndex = Math.max(0, Math.min(charts.length - 1, active ?? 0));
3548
4105
  },
4106
+ // ---- Saved charts ----
4107
+ get savedCharts() { return savedCharts; },
4108
+ /** Keep the active tab's configuration under a name (replacing a same-named one). */
4109
+ saveChart(name) {
4110
+ const entry = { name, savedAt: new Date().toISOString(), tab: tabToState(activeChart) };
4111
+ savedCharts = [...savedCharts.filter((s) => s.name !== name), entry];
4112
+ },
4113
+ /** Apply a saved chart to the active tab; the tab keeps its id and title. */
4114
+ applySavedChart(name) {
4115
+ const entry = savedCharts.find((s) => s.name === name);
4116
+ if (!entry)
4117
+ return false;
4118
+ const { id: _id, title: _title, ...rest } = tabFromState(entry.tab, activeChartIndex);
4119
+ Object.assign(activeChart, rest);
4120
+ return true;
4121
+ },
4122
+ removeSavedChart(name) {
4123
+ savedCharts = savedCharts.filter((s) => s.name !== name);
4124
+ },
4125
+ getSavedCharts() {
4126
+ return savedCharts.map((s) => ({ ...s, tab: { ...s.tab } }));
4127
+ },
4128
+ applySavedCharts(list) {
4129
+ savedCharts = list
4130
+ .filter((s) => typeof s?.name === "string" && s.tab && typeof s.tab === "object")
4131
+ .map((s) => ({ name: s.name, savedAt: String(s.savedAt ?? ""), tab: s.tab }));
4132
+ },
3549
4133
  get chartCrossFilterable() { return chartCrossFilterable; },
3550
4134
  get chartOrientation() { return effectiveChartOrientation; },
3551
4135
  set chartOrientation(v) { activeChart.orientation = v; },
@@ -3560,7 +4144,11 @@ export function createSvGridController(rawProps, domIdBase) {
3560
4144
  get effectiveChartMeasure2Id() { return effectiveChartMeasure2Id; },
3561
4145
  get chartAiHandler() { return chartAiHandler; },
3562
4146
  set chartAiHandler(v) { chartAiHandler = v; },
4147
+ get chartExplainHandler() { return chartExplainHandler; },
4148
+ set chartExplainHandler(v) { chartExplainHandler = v; },
3563
4149
  applyChartConfig(config) {
4150
+ if (config.saved)
4151
+ this.applySavedChart(config.saved);
3564
4152
  const cols = [...chartableColumns.dims, ...chartableColumns.measures];
3565
4153
  const resolve = (v) => {
3566
4154
  if (v == null)
@@ -3593,6 +4181,33 @@ export function createSvGridController(rawProps, domIdBase) {
3593
4181
  activeChart.timeAxis = config.timeAxis;
3594
4182
  if (config.valueFormat)
3595
4183
  activeChart.valueFormat = config.valueFormat;
4184
+ if ("bucket" in config)
4185
+ activeChart.bucket = config.bucket ?? null;
4186
+ if ("bins" in config)
4187
+ activeChart.bins = config.bins ?? null;
4188
+ if ("funnelShape" in config)
4189
+ activeChart.funnelShape = config.funnelShape ?? null;
4190
+ if ("candleStyle" in config)
4191
+ activeChart.candleStyle = config.candleStyle ?? null;
4192
+ if ("measure2" in config)
4193
+ activeChart.measure2Id = resolve(config.measure2);
4194
+ if ("ohlc" in config) {
4195
+ const o = config.ohlc;
4196
+ activeChart.ohlc = o
4197
+ ? { open: resolve(o.open), high: resolve(o.high), low: resolve(o.low), close: resolve(o.close), volume: resolve(o.volume) }
4198
+ : null;
4199
+ }
4200
+ if (config.indicators)
4201
+ activeChart.indicators = [...config.indicators];
4202
+ if ("format" in config)
4203
+ activeChart.format = config.format ?? null;
4204
+ if (config.frozen === false)
4205
+ activeChart.frozen = null;
4206
+ else if (config.frozen === true && !activeChart.frozen) {
4207
+ const spec = chartSpec;
4208
+ if (spec)
4209
+ activeChart.frozen = { spec: JSON.parse(JSON.stringify(spec)), at: new Date().toISOString() };
4210
+ }
3596
4211
  },
3597
4212
  get toggleGroupInPanel() { return toggleGroupInPanel; },
3598
4213
  get lastSortingSerialized() { return lastSortingSerialized; },
@@ -3615,8 +4230,17 @@ export function createSvGridController(rawProps, domIdBase) {
3615
4230
  },
3616
4231
  /** True when the `rowResize` prop is on and nothing overrides it. */
3617
4232
  get rowResizeOn() { return rowResizeOn; },
4233
+ get frozenRowCount() { return frozenRowCount; },
4234
+ get frozenRowList() { return frozenRowList; },
4235
+ get frozenBandHeight() { return frozenBandHeight; },
4236
+ get frozenRowTop() { return frozenRowTop; },
3618
4237
  /** Record the height the user dragged a row to. */
3619
4238
  get setRowResizeHeight() { return setRowResizeHeight; },
4239
+ get isRowCollapsed() { return isRowCollapsed; },
4240
+ get setRowCollapsed() { return setRowCollapsed; },
4241
+ get resizeRowByUser() { return resizeRowByUser; },
4242
+ get resizeColumnByUser() { return resizeColumnByUser; },
4243
+ get autosizeColumnByUser() { return autosizeColumnByUser; },
3620
4244
  /** A row's dragged height, or undefined when it has not been resized. */
3621
4245
  get rowResizeHeightPx() {
3622
4246
  rowResizeVersion;
@@ -3676,9 +4300,18 @@ export function createSvGridController(rawProps, domIdBase) {
3676
4300
  get isRowSelected() { return isRowSelected; },
3677
4301
  get toggleRowSelectionById() { return toggleRowSelectionById; },
3678
4302
  get headerSelectionState() { return headerSelectionState; },
4303
+ get visibleRowRange() { return visibleRowRange; },
4304
+ placeholderStateOf,
4305
+ /** Retry a failed placeholder row. No-op without `onRetryRow`. */
4306
+ retryRow(row) {
4307
+ props.onRetryRow?.(row.original, row.index);
4308
+ },
3679
4309
  get toggleSelectAllRows() { return toggleSelectAllRows; },
4310
+ get setSelectAllRows() { return setSelectAllRows; },
3680
4311
  get userHasActivatedCell() { return userHasActivatedCell; },
3681
4312
  set userHasActivatedCell(v) { userHasActivatedCell = v; },
4313
+ get tabRunOrigin() { return tabRunOrigin; },
4314
+ set tabRunOrigin(v) { tabRunOrigin = v; },
3682
4315
  get setActiveCell() { return setActiveCell; },
3683
4316
  get scrollActiveCellIntoView() { return scrollActiveCellIntoView; },
3684
4317
  get getColumnBaseWidth() { return getColumnBaseWidth; },
@@ -3688,6 +4321,8 @@ export function createSvGridController(rawProps, domIdBase) {
3688
4321
  get extendSelection() { return extendSelection; },
3689
4322
  get isCellInSelectedRange() { return isCellInSelectedRange; },
3690
4323
  get getCellRangeEdges() { return getCellRangeEdges; },
4324
+ get activeRangeRect() { return activeRangeRect; },
4325
+ get cellSelectionState() { return cellSelectionState; },
3691
4326
  get getSelectionRects() { return getSelectionRects; },
3692
4327
  get fillHandleCell() { return fillHandleCell; },
3693
4328
  get isInFillPreview() { return isInFillPreview; },
@@ -3710,6 +4345,7 @@ export function createSvGridController(rawProps, domIdBase) {
3710
4345
  get startFillDrag() { return startFillDrag; },
3711
4346
  get onFillPointerMove() { return onFillPointerMove; },
3712
4347
  get onFillPointerUp() { return onFillPointerUp; },
4348
+ get fillDownToNeighbour() { return fillDownToNeighbour; },
3713
4349
  get isOnMoveGrabStrip() { return isOnMoveGrabStrip; },
3714
4350
  get startMoveDrag() { return startMoveDrag; },
3715
4351
  get onMovePointerMove() { return onMovePointerMove; },
@@ -3729,6 +4365,7 @@ export function createSvGridController(rawProps, domIdBase) {
3729
4365
  get cutSelectionToClipboard() { return cutSelectionToClipboard; },
3730
4366
  get pasteFromClipboard() { return pasteFromClipboard; },
3731
4367
  get onGridPaste() { return onGridPaste; },
4368
+ get armPasteFallback() { return armPasteFallback; },
3732
4369
  get clearSelectedCells() { return clearSelectedCells; },
3733
4370
  get onCellDoubleClick() { return onCellDoubleClick; },
3734
4371
  get startEditingWithChar() { return startEditingWithChar; },
@@ -3805,12 +4442,12 @@ export function createSvGridController(rawProps, domIdBase) {
3805
4442
  get clearColumnFilter() { return clearColumnFilter; },
3806
4443
  get onWindowKeydown() { return onWindowKeydown; },
3807
4444
  get columnDefMatchesId() { return columnDefMatchesId; },
3808
- get buildApi() { return buildApi; },
4445
+ get buildApi() { return api; },
3809
4446
  get apiNotified() { return apiNotified; },
3810
4447
  set apiNotified(v) { apiNotified = v; },
3811
4448
  };
3812
4449
  const { resolveEffectiveFeatures } = createFeatures(ctx);
3813
- const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, onBodyScroll } = createScrollSync(ctx);
4450
+ const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, syncScrollNow, onBodyScroll } = createScrollSync(ctx);
3814
4451
  const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard(ctx);
3815
4452
  const { computeSummaries, hasRenderedColumn } = createSummaries(ctx);
3816
4453
  // Hand the aggregator to `eagerSummaries` (declared above `ctx`, which this
@@ -3818,13 +4455,13 @@ export function createSvGridController(rawProps, domIdBase) {
3818
4455
  summarize = computeSummaries;
3819
4456
  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(ctx);
3820
4457
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender(ctx);
3821
- const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing(ctx);
3822
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
4458
+ const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste, armPasteFallback } = createEditing(ctx);
4459
+ 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(ctx);
3823
4460
  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(ctx);
3824
4461
  const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, onRowPointerDown, destroyRowDrag } = createRowDrag(ctx);
3825
4462
  const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids(ctx);
3826
4463
  const { buildApi } = createGridApi(ctx);
3827
- const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
4464
+ const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, fillDownToNeighbour, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
3828
4465
  // Dev-time configuration checks. Silent misconfiguration was the grid's
3829
4466
  // biggest usability gap - a misspelled `field` rendered a column of blank
3830
4467
  // cells and printed nothing, and an inert `pageSize` was simply ignored.
@@ -3858,6 +4495,7 @@ export function createSvGridController(rawProps, domIdBase) {
3858
4495
  pageSize: props.pageSize,
3859
4496
  groupBy: props.groupBy,
3860
4497
  treeData: props.treeData,
4498
+ serverGroup: props.serverGroup,
3861
4499
  initialColumnPinning: props.initialColumnPinning,
3862
4500
  columnVirtualization: props.columnVirtualization,
3863
4501
  externalPagination: props.externalPagination,