@svgrid/grid 3.0.2 → 3.0.4

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 (281) hide show
  1. package/README.md +2 -1
  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 +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  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 +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  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/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
@@ -16,13 +16,33 @@
16
16
  import { cubicOut } from "svelte/easing";
17
17
  import SvGridChart from "./SvGridChart.svelte";
18
18
  import { downloadChartPng, downloadChartSvg, downloadChartCsv, chartCsvExportable, chartToPngBlob } from "./chart-export";
19
- import type { ChartType, ChartSelection, RowData, TableFeatures } from "./index";
19
+ import type { ChartSelection, RowData, TableFeatures } from "./index";
20
+ import SvChartPanes from "./SvChartPanes.svelte";
21
+ import SvGridChartPickers, { chartPanelTypes } from "./SvGridChartPickers.svelte";
22
+ import { chartPanelMessage, resolveChartPanelMessages } from "./chart-panel-messages";
23
+ // The builder is a lazy chunk: a modal with a thumbnail per type, loaded
24
+ // the first time someone opens it.
25
+ let Builder = $state<typeof import("./SvGridChartBuilder.svelte").default | null>(null);
26
+ let builderOpen = $state(false);
27
+ function openBuilder() {
28
+ if (Builder) { builderOpen = true; return; }
29
+ import("./SvGridChartBuilder.svelte").then((m) => { Builder = m.default; builderOpen = true; });
30
+ }
20
31
  import type { SvGridController } from "./SvGrid.controller.svelte";
21
32
 
22
33
  let { ctrl }: { ctrl: SvGridController<TFeatures, TData> } = $props();
23
34
 
35
+ /** The panel's strings, from `localization.text`; unset keys stay English. */
36
+ const m = $derived(resolveChartPanelMessages(ctrl.localizationText));
24
37
  const config = $derived(ctrl.chartingConfig);
25
38
  const cfg = $derived(ctrl.chartCfg);
39
+ /** Legend placement from the builder's format; the default is below. */
40
+ const legendProp = $derived(ctrl.chartFormat?.legend === undefined ? true : ctrl.chartFormat.legend);
41
+ const dataLabelsProp = $derived(
42
+ ctrl.chartFormat?.dataLabels === undefined ? ctrl.chartDataLabels : ctrl.chartFormat.dataLabels === null ? false : { placement: ctrl.chartFormat.dataLabels },
43
+ );
44
+ /** The crosshair's axis pills, from the builder's format; on by default. */
45
+ const crosshairLabelsProp = $derived(ctrl.chartFormat?.crosshairLabels !== false);
26
46
  const isCustom = $derived(ctrl.chartIsCustom);
27
47
  const position = $derived(config?.position ?? "bottom");
28
48
  const crossFilter = $derived(config?.crossFilter !== false);
@@ -115,124 +135,13 @@
115
135
  : `height:${size}px`,
116
136
  );
117
137
 
118
- // Measure the chart area so the chart lays out to fit it (no overflow /
119
- // clipped axis labels) - the docked panel is a fixed-size container. Reserve
120
- // room for the legend (rendered below the SVG for >1 series) and the export
121
- // toolbar, so neither gets clipped.
122
- let bodyW = $state(0);
123
- let bodyH = $state(0);
124
- const seriesCount = $derived(spec ? (spec.type === "pie" ? spec.categories.length : spec.series.length) : 0);
125
- const reserve = $derived((seriesCount > 1 ? 36 : 0) + (cfg?.export === true ? 30 : 0));
126
- const chartW = $derived(bodyW > 24 ? bodyW - 8 : undefined);
127
- const chartH = $derived(bodyH > 24 ? Math.max(120, bodyH - 8 - reserve) : undefined);
138
+ // The docked panel is a fixed-size container, and the chart sizes itself
139
+ // to it: measures the body and subtracts the chart's own toolbar,
140
+ // legend and brush. This used to be a reserve heuristic here (36px for a
141
+ // legend, 30px for the toolbar) that was wrong whenever the legend wrapped.
128
142
 
129
- /**
130
- * The chart types the picker offers, grouped by what they are FOR.
131
- *
132
- * This used to be four entries while the engine drew fifteen, so nine types
133
- * were reachable only by hand-writing a spec - and `charting.defaultType`
134
- * accepted them all, which meant setting one left the picker showing
135
- * something else entirely.
136
- *
137
- * `needs` gates on COLUMN SHAPE only, never on the data. Column shape is
138
- * stable while the user filters; row counts are not, and a picker whose
139
- * options vanish mid-session because a filter emptied a group is worse than
140
- * one that explains itself. Data-level problems get a sentence instead, see
141
- * `typeIssue` below.
142
- */
143
- type TypeDef = {
144
- value: ChartType;
145
- label: string;
146
- group: string;
147
- needs?: (c: { dims: unknown[]; measures: unknown[]; dates: unknown[] }) => boolean;
148
- };
149
- const TYPES: TypeDef[] = [
150
- { value: "bar", label: "Bar", group: "Compare" },
151
- { value: "line", label: "Line", group: "Compare" },
152
- { value: "area", label: "Area", group: "Compare" },
153
- { value: "pie", label: "Pie", group: "Part of a whole" },
154
- { value: "treemap", label: "Tree map", group: "Part of a whole" },
155
- { value: "funnel", label: "Funnel", group: "Flow" },
156
- { value: "waterfall", label: "Waterfall", group: "Flow" },
157
- { value: "sankey", label: "Sankey", group: "Flow", needs: (c) => c.dims.length >= 2 },
158
- { value: "radar", label: "Radar", group: "Distribution" },
159
- { value: "heatmap", label: "Heat map", group: "Distribution", needs: (c) => c.dims.length >= 2 },
160
- { value: "scatter", label: "Scatter", group: "Distribution", needs: (c) => c.measures.length >= 2 },
161
- { value: "boxplot", label: "Box plot", group: "Distribution" },
162
- { value: "gauge", label: "Gauge", group: "Single value" },
163
- { value: "calendar", label: "Calendar", group: "Over time", needs: (c) => c.dates.length >= 1 },
164
- ];
165
- const availableTypes = $derived(TYPES.filter((t) => !t.needs || t.needs(columns)));
166
-
167
- // Which controls this type can actually use. Allow-lists rather than the old
168
- // "not pie" guards: with thirteen types, "everything except pie" hands a
169
- // gauge a Stacked checkbox.
170
- const CARTESIAN: ChartType[] = ["bar", "line", "area"];
171
-
172
- /** Named series palettes. "Theme" defers to --sg-chart-1..8 and the defaults. */
173
- const PALETTES: Array<{ key: string; label: string; colors: string[] | null }> = [
174
- { key: "theme", label: "Theme", colors: null },
175
- { key: "ocean", label: "Ocean", colors: ["#0ea5e9", "#14b8a6", "#6366f1", "#8b5cf6", "#06b6d4"] },
176
- { key: "sunset", label: "Sunset", colors: ["#f59e0b", "#ef4444", "#ec4899", "#8b5cf6", "#f97316"] },
177
- { key: "forest", label: "Forest", colors: ["#16a34a", "#65a30d", "#0d9488", "#4d7c0f", "#0891b2"] },
178
- { key: "slate", label: "Slate", colors: ["#475569", "#0ea5e9", "#64748b", "#38bdf8", "#94a3b8"] },
179
- ];
180
- const activePalette = $derived(
181
- PALETTES.find((x) => x.colors && ctrl.chartPalette && x.colors.join() === ctrl.chartPalette.join())?.key ??
182
- "theme",
183
- );
184
- const wantsHorizontal = $derived(ctrl.chartType === "bar");
185
- const wantsDonut = $derived(ctrl.chartType === "pie");
186
- const wantsGroupBy = $derived(ctrl.chartType !== "gauge");
187
- const wantsSplitBy = $derived(
188
- columns.dims.length > 1 &&
189
- !["pie", "gauge", "waterfall", "funnel", "calendar"].includes(ctrl.chartType),
190
- );
191
- const wantsStacked = $derived(CARTESIAN.includes(ctrl.chartType));
192
- const wantsLogScale = $derived(
193
- CARTESIAN.includes(ctrl.chartType) || ctrl.chartType === "scatter" || ctrl.chartType === "boxplot",
194
- );
195
- const wantsDataLabels = $derived(
196
- !["gauge", "sankey", "calendar", "treemap", "scatter", "boxplot"].includes(ctrl.chartType),
197
- );
198
- const wantsSecondMeasure = $derived(ctrl.chartType === "scatter");
199
- /**
200
- * A box plot is the one type that reduces its groups itself: the whole point
201
- * is the spread of the sample, so `sum | avg | count` has nothing to say
202
- * about it. Showing an Aggregate select that changes nothing is worse than
203
- * not showing one.
204
- */
205
- const wantsReduce = $derived(!wantsSecondMeasure && ctrl.chartType !== "boxplot");
206
- const typeGroups = $derived([...new Set(availableTypes.map((t) => t.group))]);
207
-
208
- /**
209
- * Why the current type cannot draw anything useful yet, as a sentence.
210
- *
211
- * The alternative - removing the option - means a chart can disappear from
212
- * under the user when they filter. Saying what is missing keeps the picker
213
- * honest without making it unstable.
214
- */
215
- const typeIssue = $derived.by<string | null>(() => {
216
- const t = ctrl.chartType;
217
- const cats = ctrl.chartSpec?.categories.length ?? 0;
218
- if (t === "heatmap" && !ctrl.chartSeriesId && columns.dims.length > 1)
219
- return "Heat map needs a Split by column for its rows.";
220
- if (t === "sankey" && !ctrl.chartSeriesId)
221
- return "Sankey needs a Split by column: Group by is the source, Split by the target.";
222
- if (t === "radar" && cats > 0 && cats < 3) return "Radar needs at least 3 groups.";
223
- if (t === "funnel" && cats === 1) return "Funnel needs at least 2 stages.";
224
- return null;
225
- });
226
- const REDUCERS = [
227
- { value: "sum", label: "Sum" },
228
- { value: "avg", label: "Average" },
229
- { value: "count", label: "Count" },
230
- ] as const;
231
- const FORMATS = [
232
- { value: "number", label: "Number" },
233
- { value: "currency", label: "Currency" },
234
- { value: "percent", label: "Percent" },
235
- ] as const;
143
+ /** The types the builder's gallery shows for the current columns. */
144
+ const availableTypes = $derived(chartPanelTypes(columns, m));
236
145
 
237
146
  function onChartSelect(sel: ChartSelection) {
238
147
  if (crossFilter && sel.category != null) ctrl.applyChartCrossFilter(String(sel.category));
@@ -260,6 +169,20 @@
260
169
  if (spec) downloadChartCsv(spec, "chart.csv");
261
170
  exportOpen = false;
262
171
  }
172
+ async function exportPdf() {
173
+ const el = svgEl();
174
+ exportOpen = false;
175
+ if (!el) return;
176
+ const m = await import("./chart-export-pdf");
177
+ await m.downloadChartPdf(el, "chart.pdf", { title: spec?.title ?? ctrl.charts[ctrl.activeChartIndex]?.title });
178
+ }
179
+ async function printChart() {
180
+ const el = svgEl();
181
+ exportOpen = false;
182
+ if (!el) return;
183
+ const m = await import("./chart-export-pdf");
184
+ m.printChart(el, { title: spec?.title ?? ctrl.charts[ctrl.activeChartIndex]?.title });
185
+ }
263
186
  async function copyImage() {
264
187
  const el = svgEl();
265
188
  exportOpen = false;
@@ -273,17 +196,77 @@
273
196
  /* clipboard blocked - no-op */
274
197
  }
275
198
  }
199
+ // A popover closes on a click outside and on Escape, and Escape hands
200
+ // focus back to the button that opened it. Without the key the export
201
+ // menu and the saved-charts dialog stayed open until something was
202
+ // clicked, which the keyboard could not do.
203
+ let exportBtn = $state<HTMLButtonElement | null>(null);
204
+ let savedBtn = $state<HTMLButtonElement | null>(null);
205
+ function dismissable(scope: string, close: () => void, trigger: () => HTMLButtonElement | null) {
206
+ const onDown = (e: PointerEvent) => {
207
+ if (!(e.target as HTMLElement)?.closest(scope)) close();
208
+ };
209
+ const onKey = (e: KeyboardEvent) => {
210
+ if (e.key !== "Escape") return;
211
+ e.stopPropagation();
212
+ close();
213
+ trigger()?.focus();
214
+ };
215
+ window.addEventListener("pointerdown", onDown);
216
+ window.addEventListener("keydown", onKey, true);
217
+ return () => {
218
+ window.removeEventListener("pointerdown", onDown);
219
+ window.removeEventListener("keydown", onKey, true);
220
+ };
221
+ }
276
222
  $effect(() => {
277
223
  if (!exportOpen) return;
278
- const close = (e: PointerEvent) => {
279
- if (!(e.target as HTMLElement)?.closest(".sv-grid-chart-export")) exportOpen = false;
280
- };
281
- window.addEventListener("pointerdown", close);
282
- return () => window.removeEventListener("pointerdown", close);
224
+ return dismissable(".sv-grid-chart-export", () => (exportOpen = false), () => exportBtn);
225
+ });
226
+
227
+ // ---- Saved charts: name the active tab, apply or remove a saved one -----
228
+ let savedOpen = $state(false);
229
+ let saveName = $state("");
230
+ function saveChart() {
231
+ const name = saveName.trim();
232
+ if (!name) return;
233
+ ctrl.saveChart(name);
234
+ saveName = "";
235
+ }
236
+ $effect(() => {
237
+ if (!savedOpen) return;
238
+ return dismissable(".sv-grid-chart-saved", () => (savedOpen = false), () => savedBtn);
283
239
  });
284
240
 
241
+ // ---- Describe: the chart's summary sentence to the clipboard ------------
242
+ async function describeChart() {
243
+ exportOpen = false;
244
+ if (!spec) return;
245
+ const { chartSummary } = await import("./chart-summary");
246
+ const text = chartSummary(spec);
247
+ try { await navigator.clipboard?.writeText(text); } catch { /* no clipboard */ }
248
+ aiRationale = text;
249
+ }
250
+
285
251
  // ---- AI "chart this" (enterprise-registered handler) -------------------
286
252
  const aiEnabled = $derived(!!ctrl.chartAiHandler);
253
+ const explainEnabled = $derived(!!ctrl.chartExplainHandler);
254
+ async function explainChart() {
255
+ const handler = ctrl.chartExplainHandler;
256
+ if (!handler || aiBusy) return;
257
+ aiBusy = true;
258
+ aiError = "";
259
+ aiRationale = "";
260
+ try {
261
+ const out = await handler();
262
+ aiRationale = out ? [out.summary, ...out.insights].filter(Boolean).join(" ") : "";
263
+ if (!out) aiError = m.chartAiNoExplanation;
264
+ } catch (e) {
265
+ aiError = e instanceof Error ? e.message : m.chartAiFailed;
266
+ } finally {
267
+ aiBusy = false;
268
+ }
269
+ }
287
270
  let aiOpen = $state(false);
288
271
  let aiPrompt = $state("");
289
272
  let aiBusy = $state(false);
@@ -302,10 +285,10 @@
302
285
  ctrl.applyChartConfig(cfg as Parameters<typeof ctrl.applyChartConfig>[0]);
303
286
  aiRationale = typeof cfg.rationale === "string" ? cfg.rationale : "";
304
287
  } else {
305
- aiError = "Couldn't build a chart from that request.";
288
+ aiError = m.chartAiNoChart;
306
289
  }
307
290
  } catch (e) {
308
- aiError = e instanceof Error ? e.message : "Something went wrong.";
291
+ aiError = e instanceof Error ? e.message : m.chartAiFailed;
309
292
  } finally {
310
293
  aiBusy = false;
311
294
  }
@@ -341,7 +324,7 @@
341
324
  class:is-floating={floating}
342
325
  class:is-maximized={floating && maximized}
343
326
  style={floatStyle}
344
- aria-label="Chart"
327
+ aria-label={m.chartPanelTitle}
345
328
  transition:fly={{
346
329
  x: !floating && position === "right" ? size : 0,
347
330
  y: !floating && position === "bottom" ? size : 0,
@@ -357,7 +340,7 @@
357
340
  onpointerdown={startResize}
358
341
  role="separator"
359
342
  aria-orientation={position === "right" ? "vertical" : "horizontal"}
360
- aria-label="Resize chart panel"
343
+ aria-label={m.chartResize}
361
344
  ></div>
362
345
  {/if}
363
346
 
@@ -371,13 +354,13 @@
371
354
  <svg class="sv-grid-chart-title-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
372
355
  <line x1="12" y1="20" x2="12" y2="10" /><line x1="18" y1="20" x2="18" y2="4" /><line x1="6" y1="20" x2="6" y2="16" />
373
356
  </svg>
374
- <span class="sv-grid-chart-title">Chart</span>
357
+ <span class="sv-grid-chart-title">{m.chartPanelTitle}</span>
375
358
  <div class="sv-grid-chart-actions">
376
359
  {#if crossFilter && !isCustom}
377
- <button type="button" class="sv-grid-chart-btn" onclick={() => ctrl.clearChartCrossFilter()}>Clear filter</button>
360
+ <button type="button" class="sv-grid-chart-btn" onclick={() => ctrl.clearChartCrossFilter()}>{m.chartClearFilter}</button>
378
361
  {/if}
379
362
  <div class="sv-grid-chart-export">
380
- <button type="button" class="sv-grid-chart-icon-btn" aria-label="Export chart" title="Export" aria-expanded={exportOpen} onclick={() => (exportOpen = !exportOpen)}>
363
+ <button type="button" class="sv-grid-chart-icon-btn" aria-label={m.chartExportLabel} title={m.chartExport} aria-expanded={exportOpen} bind:this={exportBtn} onclick={() => (exportOpen = !exportOpen)}>
381
364
  {#if copied}
382
365
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12" /></svg>
383
366
  {:else}
@@ -386,45 +369,92 @@
386
369
  </button>
387
370
  {#if exportOpen}
388
371
  <div class="sv-grid-chart-export-menu" role="menu">
389
- <button type="button" role="menuitem" onclick={exportPng}>PNG image</button>
390
- <button type="button" role="menuitem" onclick={exportSvg}>SVG vector</button>
391
- <button type="button" role="menuitem" disabled={!csvExportable} onclick={exportCsv}>CSV data</button>
392
- <button type="button" role="menuitem" onclick={copyImage}>Copy to clipboard</button>
372
+ <button type="button" role="menuitem" onclick={exportPng}>{m.chartExportPng}</button>
373
+ <button type="button" role="menuitem" onclick={exportSvg}>{m.chartExportSvg}</button>
374
+ <button type="button" role="menuitem" onclick={exportPdf}>{m.chartExportPdf}</button>
375
+ <button type="button" role="menuitem" disabled={!csvExportable} onclick={exportCsv}>{m.chartExportCsv}</button>
376
+ <button type="button" role="menuitem" onclick={copyImage}>{m.chartExportCopy}</button>
377
+ <button type="button" role="menuitem" onclick={printChart}>{m.chartExportPrint}</button>
378
+ <button type="button" role="menuitem" onclick={describeChart}>{m.chartExportDescribe}</button>
393
379
  </div>
394
380
  {/if}
395
381
  </div>
396
382
  {#if aiEnabled && !isCustom}
397
- <button type="button" class="sv-grid-chart-btn sv-grid-chart-ai-btn" class:is-active={aiOpen} aria-label="Chart with AI" title="Chart with AI" aria-expanded={aiOpen} onclick={() => (aiOpen = !aiOpen)}>
383
+ <button type="button" class="sv-grid-chart-btn sv-grid-chart-ai-btn" class:is-active={aiOpen} aria-label={m.chartAiLabel} title={m.chartAiLabel} aria-expanded={aiOpen} onclick={() => (aiOpen = !aiOpen)}>
398
384
  <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l1.9 4.6L18.5 8.5 13.9 10.4 12 15l-1.9-4.6L5.5 8.5l4.6-1.9L12 2zm6 11l1 2.4 2.4 1-2.4 1L18 21l-1-2.6-2.4-1 2.4-1L18 13zM6 14l.8 2 2 .8-2 .8L6 20l-.8-2.4-2-.8 2-.8L6 14z" /></svg>
399
- AI
385
+ {m.chartAi}
400
386
  </button>
401
387
  {/if}
402
388
  {#if !isCustom}
403
- <button type="button" class="sv-grid-chart-icon-btn" aria-label="Add chart" title="Add chart" onclick={() => ctrl.addChart()}>
389
+ <button type="button" class="sv-grid-chart-btn sv-grid-chart-build-btn" aria-label={m.chartBuildLabel} title={m.chartBuildTitle} aria-haspopup="dialog" onclick={openBuilder}>
390
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></svg>
391
+ {m.chartBuild}
392
+ </button>
393
+ <div class="sv-grid-chart-export sv-grid-chart-saved">
394
+ <button type="button" class="sv-grid-chart-icon-btn sv-grid-chart-saved-btn" aria-label={m.chartSaveLabel} title={m.chartSavedTitle} aria-expanded={savedOpen} aria-haspopup="dialog" bind:this={savedBtn} onclick={() => (savedOpen = !savedOpen)}>
395
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" /><polyline points="17 21 17 13 7 13 7 21" /><polyline points="7 3 7 8 15 8" /></svg>
396
+ </button>
397
+ {#if savedOpen}
398
+ <div class="sv-grid-chart-export-menu sv-grid-chart-saved-menu" role="dialog" aria-label={m.chartSavedTitle}>
399
+ <form class="sv-grid-chart-saved-row" onsubmit={(e) => { e.preventDefault(); saveChart(); }}>
400
+ <input class="sv-grid-chart-saved-name" type="text" aria-label={m.chartSaveName} placeholder={m.chartSaveNamePlaceholder} bind:value={saveName} />
401
+ <button type="submit" class="sv-grid-chart-btn" disabled={!saveName.trim()}>{m.chartSave}</button>
402
+ </form>
403
+ {#if ctrl.savedCharts.length}
404
+ <ul class="sv-grid-chart-saved-list">
405
+ {#each ctrl.savedCharts as sc (sc.name)}
406
+ <li class="sv-grid-chart-saved-item">
407
+ <button type="button" class="sv-grid-chart-saved-apply" title={m.chartSaveApply} onclick={() => { ctrl.applySavedChart(sc.name); savedOpen = false; }}>{sc.name}</button>
408
+ <button type="button" class="sv-grid-chart-saved-x" aria-label={`${m.chartSaveRemove}: ${sc.name}`} title={m.chartSaveRemove} onclick={() => ctrl.removeSavedChart(sc.name)}>&times;</button>
409
+ </li>
410
+ {/each}
411
+ </ul>
412
+ {:else}
413
+ <p class="sv-grid-chart-saved-empty">{m.chartSavedEmpty}</p>
414
+ {/if}
415
+ </div>
416
+ {/if}
417
+ </div>
418
+ <button
419
+ type="button"
420
+ class="sv-grid-chart-icon-btn sv-grid-chart-link-btn"
421
+ class:is-unlinked={!!ctrl.chartFrozen}
422
+ aria-label={ctrl.chartFrozen ? m.chartLinkBack : m.chartUnlink}
423
+ aria-pressed={!!ctrl.chartFrozen}
424
+ title={ctrl.chartFrozen ? m.chartUnlinkedTitle : m.chartLinkedTitle}
425
+ onclick={() => (ctrl.chartFrozen ? ctrl.unfreezeChart() : ctrl.freezeChart())}
426
+ >
427
+ {#if ctrl.chartFrozen}
428
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18.84 12.25l1.72-1.71a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M5.17 11.75l-1.71 1.71a5 5 0 0 0 7.07 7.07l1.71-1.71" /><line x1="8" y1="2" x2="8" y2="5" /><line x1="2" y1="8" x2="5" y2="8" /><line x1="16" y1="19" x2="16" y2="22" /><line x1="19" y1="16" x2="22" y2="16" /></svg>
429
+ {:else}
430
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></svg>
431
+ {/if}
432
+ </button>
433
+ <button type="button" class="sv-grid-chart-icon-btn" aria-label={m.chartAdd} title={m.chartAdd} onclick={() => ctrl.addChart()}>
404
434
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
405
435
  </button>
406
436
  {/if}
407
437
  {#if floating}
408
- <button type="button" class="sv-grid-chart-icon-btn" aria-label={maximized ? "Restore" : "Maximize"} title={maximized ? "Restore" : "Maximize"} onclick={toggleMax}>
438
+ <button type="button" class="sv-grid-chart-icon-btn" aria-label={maximized ? m.chartRestore : m.chartMaximize} title={maximized ? m.chartRestore : m.chartMaximize} onclick={toggleMax}>
409
439
  {#if maximized}
410
440
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3v3a2 2 0 0 1-2 2H3" /><path d="M21 8h-3a2 2 0 0 1-2-2V3" /><path d="M3 16h3a2 2 0 0 1 2 2v3" /><path d="M16 21v-3a2 2 0 0 1 2-2h3" /></svg>
411
441
  {:else}
412
442
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3" /><path d="M21 8V5a2 2 0 0 0-2-2h-3" /><path d="M3 16v3a2 2 0 0 0 2 2h3" /><path d="M16 21h3a2 2 0 0 0 2-2v-3" /></svg>
413
443
  {/if}
414
444
  </button>
415
- <button type="button" class="sv-grid-chart-icon-btn" aria-label="Dock chart" title="Dock" onclick={dockBack}>
445
+ <button type="button" class="sv-grid-chart-icon-btn" aria-label={m.chartDockLabel} title={m.chartDock} onclick={dockBack}>
416
446
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" /><line x1="15" y1="3" x2="15" y2="21" /></svg>
417
447
  </button>
418
448
  {:else}
419
- <button type="button" class="sv-grid-chart-icon-btn" aria-label="Pop out chart" title="Pop out" onclick={popOut}>
449
+ <button type="button" class="sv-grid-chart-icon-btn" aria-label={m.chartPopOutLabel} title={m.chartPopOut} onclick={popOut}>
420
450
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
421
451
  </button>
422
452
  {/if}
423
- <button type="button" class="sv-grid-chart-close" aria-label="Close chart" onclick={() => (ctrl.chartPanelOpen = false)}>&times;</button>
453
+ <button type="button" class="sv-grid-chart-close" aria-label={m.chartClose} onclick={() => (ctrl.chartPanelOpen = false)}>&times;</button>
424
454
  </div>
425
455
  </div>
426
456
  {#if !isCustom && ctrl.charts.length > 1}
427
- <div class="sv-grid-chart-tabs" role="tablist" aria-label="Charts">
457
+ <div class="sv-grid-chart-tabs" role="tablist" aria-label={m.chartTabs}>
428
458
  {#each ctrl.charts as c, i (c.id)}
429
459
  <div class="sv-grid-chart-tab" class:is-active={i === ctrl.activeChartIndex}>
430
460
  <button
@@ -437,8 +467,8 @@
437
467
  <button
438
468
  type="button"
439
469
  class="sv-grid-chart-tab-x"
440
- aria-label={`Remove ${c.title}`}
441
- title="Remove"
470
+ aria-label={chartPanelMessage(m.chartRemoveTab, { title: c.title })}
471
+ title={m.chartRemove}
442
472
  onclick={() => ctrl.removeChart(i)}
443
473
  >&times;</button>
444
474
  </div>
@@ -457,13 +487,16 @@
457
487
  <input
458
488
  class="sv-grid-chart-ai-input"
459
489
  type="text"
460
- placeholder="Describe a chart, e.g. revenue by region stacked by product"
490
+ placeholder={m.chartAiPlaceholder}
461
491
  bind:value={aiPrompt}
462
492
  disabled={aiBusy}
463
493
  />
464
494
  <button type="submit" class="sv-grid-chart-btn" disabled={aiBusy || !aiPrompt.trim()}>
465
- {aiBusy ? "Thinking..." : "Chart it"}
495
+ {aiBusy ? m.chartAiThinking : m.chartAiRun}
466
496
  </button>
497
+ {#if explainEnabled}
498
+ <button type="button" class="sv-grid-chart-btn sv-grid-chart-explain-btn" disabled={aiBusy || !spec} onclick={explainChart} title={m.chartAiExplainTitle}>{m.chartAiExplain}</button>
499
+ {/if}
467
500
  </form>
468
501
  {#if aiError}
469
502
  <p class="sv-grid-chart-ai-msg is-error">{aiError}</p>
@@ -472,162 +505,75 @@
472
505
  {/if}
473
506
  </div>
474
507
  {/if}
475
- <div class="sv-grid-chart-controls">
476
- {#if !isCustom}
477
- <label class="sv-grid-chart-ctl">
478
- <span class="sv-grid-chart-ctl-lbl">Type</span>
479
- <select value={ctrl.chartType} onchange={(e) => (ctrl.chartType = e.currentTarget.value as ChartType)}>
480
- <!-- Grouped: a flat thirteen-item list is a wall. optgroup is
481
- native, so this costs no new component. -->
482
- {#each typeGroups as g (g)}
483
- <optgroup label={g}>
484
- {#each availableTypes.filter((t) => t.group === g) as t (t.value)}
485
- <option value={t.value}>{t.label}</option>
486
- {/each}
487
- </optgroup>
488
- {/each}
489
- </select>
490
- </label>
491
- {#if wantsGroupBy}
492
- <label class="sv-grid-chart-ctl">
493
- <span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "sankey" ? "From" : ctrl.chartType === "heatmap" ? "Columns" : "Group by"}</span>
494
- <select value={ctrl.chartDimensionId ?? ""} onchange={(e) => (ctrl.chartDimensionId = e.currentTarget.value || null)}>
495
- {#each columns.dims as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
496
- </select>
497
- </label>
498
- {/if}
499
- {#if wantsSplitBy}
500
- <label class="sv-grid-chart-ctl">
501
- <span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "sankey" ? "To" : ctrl.chartType === "heatmap" ? "Rows" : "Split by"}</span>
502
- <select value={ctrl.chartSeriesId ?? ""} onchange={(e) => (ctrl.chartSeriesId = e.currentTarget.value || null)}>
503
- <option value="">(none)</option>
504
- {#each columns.dims as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
505
- </select>
506
- </label>
507
- {/if}
508
- <label class="sv-grid-chart-ctl">
509
- <span class="sv-grid-chart-ctl-lbl">{wantsSecondMeasure ? "X" : ctrl.chartType === "gauge" ? "Metric" : "Value"}</span>
510
- <select value={ctrl.chartMeasureId ?? ""} onchange={(e) => (ctrl.chartMeasureId = e.currentTarget.value || null)}>
511
- {#each columns.measures as m (m.id)}<option value={m.id}>{m.label}</option>{/each}
512
- </select>
513
- </label>
514
- {#if wantsSecondMeasure}
515
- <label class="sv-grid-chart-ctl">
516
- <span class="sv-grid-chart-ctl-lbl">Y</span>
517
- <select value={ctrl.effectiveChartMeasure2Id ?? ""} onchange={(e) => (ctrl.chartMeasure2Id = e.currentTarget.value || null)}>
518
- {#each columns.measures as m (m.id)}<option value={m.id}>{m.label}</option>{/each}
519
- </select>
520
- </label>
521
- {/if}
522
- {#if wantsReduce}
523
- <label class="sv-grid-chart-ctl">
524
- <span class="sv-grid-chart-ctl-lbl">Aggregate</span>
525
- <select value={ctrl.chartReduce} onchange={(e) => (ctrl.chartReduce = e.currentTarget.value as "sum" | "avg" | "count")}>
526
- {#each REDUCERS as r (r.value)}<option value={r.value}>{r.label}</option>{/each}
527
- </select>
528
- </label>
529
- {/if}
530
- <label class="sv-grid-chart-ctl">
531
- <span class="sv-grid-chart-ctl-lbl">Format</span>
532
- <select value={ctrl.chartValueFormat} onchange={(e) => (ctrl.chartValueFormat = e.currentTarget.value as "number" | "currency" | "percent")}>
533
- {#each FORMATS as f (f.value)}<option value={f.value}>{f.label}</option>{/each}
534
- </select>
535
- </label>
536
- {#if wantsStacked}
537
- <label class="sv-grid-chart-toggle">
538
- <input type="checkbox" checked={ctrl.chartStacked} onchange={(e) => (ctrl.chartStacked = e.currentTarget.checked)} />
539
- Stacked
540
- </label>
541
- <label class="sv-grid-chart-toggle" title="Normalise each category to 100 percent">
542
- <input type="checkbox" checked={ctrl.chartStacked100} onchange={(e) => (ctrl.chartStacked100 = e.currentTarget.checked)} />
543
- 100%
544
- </label>
545
- {/if}
546
- {#if wantsHorizontal}
547
- <label class="sv-grid-chart-toggle" title="Bars grow rightwards - suits long category labels">
548
- <input
549
- type="checkbox"
550
- checked={ctrl.chartOrientation === "horizontal"}
551
- onchange={(e) => (ctrl.chartOrientation = e.currentTarget.checked ? "horizontal" : "vertical")}
552
- />
553
- Horizontal
554
- </label>
555
- {/if}
556
- {#if wantsDonut}
557
- <label class="sv-grid-chart-toggle">
558
- <input type="checkbox" checked={ctrl.chartDonut} onchange={(e) => (ctrl.chartDonut = e.currentTarget.checked)} />
559
- Donut
560
- </label>
561
- {/if}
562
- <label class="sv-grid-chart-ctl">
563
- <span class="sv-grid-chart-ctl-lbl">Colours</span>
564
- <select
565
- value={activePalette}
566
- onchange={(e) => (ctrl.chartPalette = PALETTES.find((x) => x.key === e.currentTarget.value)?.colors ?? null)}
567
- >
568
- {#each PALETTES as pal (pal.key)}<option value={pal.key}>{pal.label}</option>{/each}
569
- </select>
570
- </label>
571
- {#if wantsDataLabels}
572
- <label class="sv-grid-chart-toggle">
573
- <input type="checkbox" checked={ctrl.chartDataLabels} onchange={(e) => (ctrl.chartDataLabels = e.currentTarget.checked)} />
574
- Labels
575
- </label>
576
- {/if}
577
- {#if wantsLogScale}
578
- <label class="sv-grid-chart-toggle" title="Logarithmic value axis - flattens wide-range data">
579
- <input type="checkbox" checked={ctrl.chartLogScale} onchange={(e) => (ctrl.chartLogScale = e.currentTarget.checked)} />
580
- Log scale
581
- </label>
582
- {/if}
583
- {#if ctrl.chartDimensionIsDate && CARTESIAN.includes(ctrl.chartType)}
584
- <label class="sv-grid-chart-toggle" title="Space points by actual date - real time ticks and proportional gaps">
585
- <input type="checkbox" checked={ctrl.chartTimeAxis} onchange={(e) => (ctrl.chartTimeAxis = e.currentTarget.checked)} />
586
- Date axis
587
- </label>
588
- {/if}
589
- {/if}
590
- {#if typeIssue}
591
- <p class="sv-grid-chart-hint" role="note">{typeIssue}</p>
592
- {/if}
593
- </div>
508
+ {#if !isCustom}
509
+ <SvGridChartPickers {ctrl} messages={m} />
510
+ {/if}
594
511
  </header>
595
512
 
596
- <div class="sv-grid-chart-panel-body" bind:this={bodyEl} bind:clientWidth={bodyW} bind:clientHeight={bodyH}>
513
+ <div class="sv-grid-chart-panel-body" bind:this={bodyEl}>
597
514
  <!-- A gauge is one number, so it legitimately carries no series and no
598
515
  categories. Gating purely on `series.length` sent it to the "No data"
599
516
  message even with a perfectly good spec. -->
600
- {#if spec && (spec.series.length || spec.gaugeValue != null || isCustom)}
517
+ {#if spec && ctrl.chartIndicatorPanes.length && spec.series[0]?.ohlc}
518
+ <div class="sv-grid-chart-panes-host">
519
+ <SvChartPanes
520
+ {spec}
521
+ indicators={ctrl.chartIndicatorPanes}
522
+ onSelect={onChartSelect}
523
+ interactive
524
+ legend={false}
525
+ zoomable={cfg?.zoom ?? false}
526
+ bind:zoom={ctrl.chartZoom}
527
+ rangePresets={cfg?.rangePresets}
528
+ syncGroup={cfg?.syncGroup}
529
+ contextMenu={cfg?.contextMenu}
530
+ animate={cfg?.animate}
531
+ localeText={cfg?.localeText}
532
+ crosshairLabels={crosshairLabelsProp}
533
+ toolbar={cfg?.export === true ? true : undefined}
534
+ />
535
+ </div>
536
+ {:else if spec && (spec.series.length || spec.gaugeValue != null || isCustom)}
601
537
  <SvGridChart
602
538
  {spec}
603
- width={chartW}
604
- height={chartH}
539
+ autosize
605
540
  onSelect={onChartSelect}
606
541
  interactive
607
- legend
608
- dataLabels={ctrl.chartDataLabels}
609
- zoomable={cfg?.zoom === true}
542
+ legend={legendProp}
543
+ dataLabels={dataLabelsProp}
544
+ zoomable={cfg?.zoom ?? false}
545
+ bind:zoom={ctrl.chartZoom}
610
546
  brush={cfg?.brush === true}
547
+ rangePresets={cfg?.rangePresets}
548
+ syncGroup={cfg?.syncGroup}
549
+ contextMenu={cfg?.contextMenu}
550
+ animate={cfg?.animate}
551
+ localeText={cfg?.localeText}
552
+ crosshairLabels={crosshairLabelsProp}
611
553
  toolbar={cfg?.export === true ? true : undefined}
612
554
  />
613
555
  {:else}
614
556
  <p class="sv-grid-chart-empty">
615
557
  {#if !columns.dims.length || !columns.measures.length}
616
- Charting needs at least one text column (to group by) and one numeric column (to measure).
558
+ {m.chartEmptyColumns}
617
559
  {:else}
618
- No data to chart.
560
+ {m.chartEmptyData}
619
561
  {/if}
620
562
  </p>
621
563
  {/if}
622
564
  </div>
623
565
 
566
+ {#if Builder}
567
+ <Builder {ctrl} bind:open={builderOpen} types={availableTypes} messages={m} />
568
+ {/if}
569
+
624
570
  {#if floating && !maximized}
625
571
  <!-- svelte-ignore a11y_no_static_element_interactions -->
626
572
  <div
627
573
  class="sv-grid-chart-float-resizer"
628
574
  onpointerdown={startFloatResize}
629
575
  role="separator"
630
- aria-label="Resize chart window"
576
+ aria-label={m.chartResizeWindow}
631
577
  ></div>
632
578
  {/if}
633
579
  </section>