@svgrid/grid 3.0.1 → 3.0.2

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 (77) hide show
  1. package/dist/GridFooter.svelte +14 -5
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/GridMenus.svelte +18 -10
  4. package/dist/GridMenus.svelte.d.ts +2 -1
  5. package/dist/SvGrid.controller.svelte.d.ts +17 -0
  6. package/dist/SvGrid.controller.svelte.js +128 -14
  7. package/dist/SvGrid.css +79 -10
  8. package/dist/SvGrid.svelte +191 -186
  9. package/dist/SvGrid.types.d.ts +42 -1
  10. package/dist/SvGridChart.svelte +571 -41
  11. package/dist/SvGridChart.svelte.d.ts +63 -1
  12. package/dist/SvGridChartPanel.svelte +169 -21
  13. package/dist/SvGridChartView.svelte +9 -0
  14. package/dist/SvGridChartView.svelte.d.ts +7 -0
  15. package/dist/SvGroupCell.svelte +28 -3
  16. package/dist/SvGroupCell.svelte.d.ts +8 -0
  17. package/dist/SvRowGroupPanel.svelte +20 -4
  18. package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
  19. package/dist/ai.d.ts +11 -2
  20. package/dist/ai.js +51 -7
  21. package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
  22. package/dist/cdn/{GridMenus-DFB6iwF8.js → GridMenus-M2XP9t0_.js} +152 -150
  23. package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
  24. package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
  25. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
  26. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
  27. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-CxfakxR0.js} +5 -3
  28. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-Dm_YEx9L.js} +5 -3
  29. package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
  30. package/dist/cdn/chart-CKJRCPOk.js +1652 -0
  31. package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
  32. package/dist/cdn/{src-C8QjSaq-.js → src-BJ2opd0j.js} +3608 -3414
  33. package/dist/cdn/{src-D0hO2eYN.js → src-DNbz94Ct.js} +8617 -8423
  34. package/dist/cdn/svgrid.js +16 -15
  35. package/dist/cdn/svgrid.svelte-external.js +16 -15
  36. package/dist/cell-formatting.d.ts +7 -0
  37. package/dist/cell-formatting.js +7 -1
  38. package/dist/chart-export.d.ts +23 -2
  39. package/dist/chart-export.js +80 -6
  40. package/dist/chart.d.ts +410 -18
  41. package/dist/chart.js +824 -24
  42. package/dist/grid-icons.d.ts +49 -0
  43. package/dist/grid-icons.js +86 -0
  44. package/dist/index.d.ts +3 -2
  45. package/dist/index.js +3 -2
  46. package/package.json +11 -11
  47. package/src/GridFooter.svelte +14 -5
  48. package/src/GridMenus.svelte +18 -10
  49. package/src/SvGrid.controller.svelte.ts +146 -13
  50. package/src/SvGrid.css +79 -10
  51. package/src/SvGrid.svelte +191 -186
  52. package/src/SvGrid.types.ts +42 -1
  53. package/src/SvGridChart.svelte +571 -41
  54. package/src/SvGridChart.test.ts +243 -1
  55. package/src/SvGridChartPanel.svelte +169 -21
  56. package/src/SvGridChartView.svelte +9 -0
  57. package/src/SvGroupCell.svelte +28 -3
  58. package/src/SvRowGroupPanel.svelte +20 -4
  59. package/src/ai.test.ts +60 -1
  60. package/src/ai.ts +59 -6
  61. package/src/cell-formatting.ts +7 -1
  62. package/src/chart-export.test.ts +112 -0
  63. package/src/chart-export.ts +76 -8
  64. package/src/chart.test.ts +830 -1
  65. package/src/chart.ts +1053 -32
  66. package/src/grid-icons.test.ts +46 -0
  67. package/src/grid-icons.ts +184 -0
  68. package/src/icon-overrides.grid.test.ts +188 -0
  69. package/src/icon-seam.test.ts +100 -0
  70. package/src/index.ts +19 -0
  71. package/src/svgrid.charting.test.ts +120 -0
  72. package/dist/cdn/GridMenus-CYzOvmNQ.js +0 -600
  73. package/dist/cdn/SvGridChart-BEJmNNx9.js +0 -1203
  74. package/dist/cdn/SvGridChart-BwVos976.js +0 -1202
  75. package/dist/cdn/SvGridChartPanel-BOknOkrP.js +0 -561
  76. package/dist/cdn/SvGridChartPanel-D2PjII4O.js +0 -585
  77. package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
@@ -6,7 +6,17 @@
6
6
  * focus tooltips, a clickable legend that toggles series, optional data
7
7
  * labels, and an `onSelect` drill hook. No external charting dependency.
8
8
  */
9
- import { type ChartSpec, type ChartSelection } from './chart.js';
9
+ import type { Snippet } from 'svelte';
10
+ import { type ChartSpec, type ChartSelection, type ChartGeometry, type ChartScales } from './chart.js';
11
+ /** What a custom mark is handed. `scales` is null on non-cartesian types. */
12
+ type ChartRenderContext = {
13
+ geo: ChartGeometry;
14
+ /** Pixel x at the centre of category `i`. Null when there is no x axis. */
15
+ xOf: ChartScales['xOf'] | null;
16
+ /** Pixel y for a value. Null when there is no value axis. */
17
+ yOf: ChartScales['yOf'] | null;
18
+ scales: ChartScales | null;
19
+ };
10
20
  type Props = {
11
21
  spec: ChartSpec;
12
22
  /** Show the (clickable) legend. Default true. */
@@ -40,6 +50,58 @@ type Props = {
40
50
  * fit exactly - used by the docked panel to size the chart to its body. */
41
51
  width?: number;
42
52
  height?: number;
53
+ /**
54
+ * Draw your own marks in the chart's own coordinates - the custom-series
55
+ * seam. `underlay` paints beneath the built-in marks (bands, shaded
56
+ * regions, a background you want the bars to sit on); `overlay` paints
57
+ * above them but below the crosshair and the hit layer, so tooltips and
58
+ * clicks keep working over whatever you draw.
59
+ *
60
+ * Both receive the laid-out geometry and its scales, so a custom mark lands
61
+ * on exactly the axis the built-in ones did:
62
+ *
63
+ * ```svelte
64
+ * <SvChart {spec}>
65
+ * {#snippet overlay({ geo, xOf, yOf })}
66
+ * <circle cx={xOf(3)} cy={yOf(120)} r="5" fill="tomato" />
67
+ * {/snippet}
68
+ * </SvChart>
69
+ * ```
70
+ *
71
+ * `xOf` / `yOf` are null for the types with no cartesian axes (pie, gauge,
72
+ * treemap, sankey, calendar, radar, funnel); `geo` is always there.
73
+ */
74
+ underlay?: Snippet<[ChartRenderContext]>;
75
+ overlay?: Snippet<[ChartRenderContext]>;
76
+ /**
77
+ * Let a reader pin annotations on the plot. Adds an "Annotate" toggle to
78
+ * the toolbar; while it is on, clicking the plot fires `onAnnotate` with
79
+ * the point that was clicked, and clicking an existing marker fires
80
+ * `onAnnotationRemove` with its index.
81
+ *
82
+ * The chart does NOT store them. It owns the gesture - which you cannot
83
+ * build from outside, because drag is already zoom and click is already
84
+ * drill - and hands back the data point; where the annotations live, and
85
+ * whether they survive a reload, is the host's business. Put what comes
86
+ * back into `spec.annotations` and they render like any other.
87
+ */
88
+ annotatable?: boolean;
89
+ /**
90
+ * A point the reader picked while annotating. It is a DATA point, not a
91
+ * pixel: annotate mode takes over the category gesture, so a note anchors
92
+ * to the category it was pinned on and stays there when the chart is
93
+ * re-laid-out, re-sorted or zoomed. That also means it works from the
94
+ * keyboard, since the category hit zones already navigate with arrows.
95
+ */
96
+ onAnnotate?: (at: {
97
+ /** The category picked, and its index. */
98
+ category: string;
99
+ index: number;
100
+ /** The value at that category on the first series. */
101
+ value: number;
102
+ }) => void;
103
+ /** An existing `spec.annotations` entry was clicked, by index. */
104
+ onAnnotationRemove?: (index: number) => void;
43
105
  };
44
106
  declare const SvGridChart: import("svelte").Component<Props, {}, "">;
45
107
  type SvGridChart = ReturnType<typeof SvGridChart>;
@@ -15,7 +15,7 @@
15
15
  import { fly } from "svelte/transition";
16
16
  import { cubicOut } from "svelte/easing";
17
17
  import SvGridChart from "./SvGridChart.svelte";
18
- import { downloadChartPng, downloadChartSvg, downloadChartCsv, chartToPngBlob } from "./chart-export";
18
+ import { downloadChartPng, downloadChartSvg, downloadChartCsv, chartCsvExportable, chartToPngBlob } from "./chart-export";
19
19
  import type { ChartType, ChartSelection, RowData, TableFeatures } from "./index";
20
20
  import type { SvGridController } from "./SvGrid.controller.svelte";
21
21
 
@@ -126,14 +126,103 @@
126
126
  const chartW = $derived(bodyW > 24 ? bodyW - 8 : undefined);
127
127
  const chartH = $derived(bodyH > 24 ? Math.max(120, bodyH - 8 - reserve) : undefined);
128
128
 
129
- // Aggregation-friendly types offered in the picker; exotic types come via
130
- // `buildSpec`, where the picker is hidden.
131
- const TYPES: Array<{ value: ChartType; label: string }> = [
132
- { value: "bar", label: "Bar" },
133
- { value: "line", label: "Line" },
134
- { value: "area", label: "Area" },
135
- { value: "pie", label: "Pie" },
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 },
136
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
+ });
137
226
  const REDUCERS = [
138
227
  { value: "sum", label: "Sum" },
139
228
  { value: "avg", label: "Average" },
@@ -154,7 +243,9 @@
154
243
  let exportOpen = $state(false);
155
244
  let copied = $state(false);
156
245
  const svgEl = () => bodyEl?.querySelector("svg") ?? null;
157
- const csvExportable = $derived(!!(spec && spec.categories?.length && spec.series?.length));
246
+ // One source of truth with the serializer: a scatter chart has no categories
247
+ // but does have points, so the old shape test greyed out an export that works.
248
+ const csvExportable = $derived(!!spec && chartCsvExportable(spec));
158
249
  function exportPng() {
159
250
  const el = svgEl();
160
251
  if (el) void downloadChartPng(el, "chart.png");
@@ -386,18 +477,28 @@
386
477
  <label class="sv-grid-chart-ctl">
387
478
  <span class="sv-grid-chart-ctl-lbl">Type</span>
388
479
  <select value={ctrl.chartType} onchange={(e) => (ctrl.chartType = e.currentTarget.value as ChartType)}>
389
- {#each TYPES as t (t.value)}<option value={t.value}>{t.label}</option>{/each}
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}
390
489
  </select>
391
490
  </label>
491
+ {#if wantsGroupBy}
392
492
  <label class="sv-grid-chart-ctl">
393
- <span class="sv-grid-chart-ctl-lbl">Group by</span>
493
+ <span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "sankey" ? "From" : ctrl.chartType === "heatmap" ? "Columns" : "Group by"}</span>
394
494
  <select value={ctrl.chartDimensionId ?? ""} onchange={(e) => (ctrl.chartDimensionId = e.currentTarget.value || null)}>
395
495
  {#each columns.dims as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
396
496
  </select>
397
497
  </label>
398
- {#if columns.dims.length > 1}
498
+ {/if}
499
+ {#if wantsSplitBy}
399
500
  <label class="sv-grid-chart-ctl">
400
- <span class="sv-grid-chart-ctl-lbl">Split by</span>
501
+ <span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "sankey" ? "To" : ctrl.chartType === "heatmap" ? "Rows" : "Split by"}</span>
401
502
  <select value={ctrl.chartSeriesId ?? ""} onchange={(e) => (ctrl.chartSeriesId = e.currentTarget.value || null)}>
402
503
  <option value="">(none)</option>
403
504
  {#each columns.dims as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
@@ -405,51 +506,98 @@
405
506
  </label>
406
507
  {/if}
407
508
  <label class="sv-grid-chart-ctl">
408
- <span class="sv-grid-chart-ctl-lbl">Value</span>
509
+ <span class="sv-grid-chart-ctl-lbl">{wantsSecondMeasure ? "X" : ctrl.chartType === "gauge" ? "Metric" : "Value"}</span>
409
510
  <select value={ctrl.chartMeasureId ?? ""} onchange={(e) => (ctrl.chartMeasureId = e.currentTarget.value || null)}>
410
511
  {#each columns.measures as m (m.id)}<option value={m.id}>{m.label}</option>{/each}
411
512
  </select>
412
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}
413
523
  <label class="sv-grid-chart-ctl">
414
524
  <span class="sv-grid-chart-ctl-lbl">Aggregate</span>
415
525
  <select value={ctrl.chartReduce} onchange={(e) => (ctrl.chartReduce = e.currentTarget.value as "sum" | "avg" | "count")}>
416
526
  {#each REDUCERS as r (r.value)}<option value={r.value}>{r.label}</option>{/each}
417
527
  </select>
418
528
  </label>
529
+ {/if}
419
530
  <label class="sv-grid-chart-ctl">
420
531
  <span class="sv-grid-chart-ctl-lbl">Format</span>
421
532
  <select value={ctrl.chartValueFormat} onchange={(e) => (ctrl.chartValueFormat = e.currentTarget.value as "number" | "currency" | "percent")}>
422
533
  {#each FORMATS as f (f.value)}<option value={f.value}>{f.label}</option>{/each}
423
534
  </select>
424
535
  </label>
425
- {#if ctrl.chartType !== "pie"}
536
+ {#if wantsStacked}
426
537
  <label class="sv-grid-chart-toggle">
427
538
  <input type="checkbox" checked={ctrl.chartStacked} onchange={(e) => (ctrl.chartStacked = e.currentTarget.checked)} />
428
539
  Stacked
429
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>
430
555
  {/if}
431
- <label class="sv-grid-chart-toggle">
432
- <input type="checkbox" checked={ctrl.chartDataLabels} onchange={(e) => (ctrl.chartDataLabels = e.currentTarget.checked)} />
433
- Labels
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>
434
570
  </label>
435
- {#if ctrl.chartType !== "pie"}
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}
436
578
  <label class="sv-grid-chart-toggle" title="Logarithmic value axis - flattens wide-range data">
437
579
  <input type="checkbox" checked={ctrl.chartLogScale} onchange={(e) => (ctrl.chartLogScale = e.currentTarget.checked)} />
438
580
  Log scale
439
581
  </label>
440
582
  {/if}
441
- {#if ctrl.chartDimensionIsDate && ctrl.chartType !== "pie"}
583
+ {#if ctrl.chartDimensionIsDate && CARTESIAN.includes(ctrl.chartType)}
442
584
  <label class="sv-grid-chart-toggle" title="Space points by actual date - real time ticks and proportional gaps">
443
585
  <input type="checkbox" checked={ctrl.chartTimeAxis} onchange={(e) => (ctrl.chartTimeAxis = e.currentTarget.checked)} />
444
586
  Date axis
445
587
  </label>
446
588
  {/if}
447
589
  {/if}
590
+ {#if typeIssue}
591
+ <p class="sv-grid-chart-hint" role="note">{typeIssue}</p>
592
+ {/if}
448
593
  </div>
449
594
  </header>
450
595
 
451
596
  <div class="sv-grid-chart-panel-body" bind:this={bodyEl} bind:clientWidth={bodyW} bind:clientHeight={bodyH}>
452
- {#if spec && (spec.series.length || isCustom)}
597
+ <!-- A gauge is one number, so it legitimately carries no series and no
598
+ categories. Gating purely on `series.length` sent it to the "No data"
599
+ message even with a perfectly good spec. -->
600
+ {#if spec && (spec.series.length || spec.gaugeValue != null || isCustom)}
453
601
  <SvGridChart
454
602
  {spec}
455
603
  width={chartW}
@@ -17,12 +17,18 @@
17
17
  let {
18
18
  data,
19
19
  chart,
20
+ localization,
20
21
  }: {
21
22
  data: ReadonlyArray<Record<string, unknown>>;
22
23
  /** Present for parity with board/scheduler renderers; unused here. */
23
24
  columns?: unknown;
24
25
  getRowId?: unknown;
25
26
  chart: ChartViewConfig;
27
+ /** The grid's localization, so a localized grid gets a localized chart
28
+ * without saying so twice. Resolved here rather than in SvGrid because
29
+ * this renderer is lazy: a grid that never charts should not carry the
30
+ * fallback in its base bundle. */
31
+ localization?: { locale?: string | ReadonlyArray<string> };
26
32
  } = $props();
27
33
 
28
34
  // Container size -> chart viewBox, so the chart fills the grid area and
@@ -44,6 +50,9 @@
44
50
  palette: chart.palette,
45
51
  });
46
52
  if (chart.valueFormat) s.valueFormat = chart.valueFormat;
53
+ const locale = chart.locale ?? localization?.locale;
54
+ if (locale) s.locale = locale;
55
+ if (chart.currency) s.currency = chart.currency;
47
56
  return s;
48
57
  });
49
58
  </script>
@@ -5,6 +5,13 @@ type $$ComponentProps = {
5
5
  columns?: unknown;
6
6
  getRowId?: unknown;
7
7
  chart: ChartViewConfig;
8
+ /** The grid's localization, so a localized grid gets a localized chart
9
+ * without saying so twice. Resolved here rather than in SvGrid because
10
+ * this renderer is lazy: a grid that never charts should not carry the
11
+ * fallback in its base bundle. */
12
+ localization?: {
13
+ locale?: string | ReadonlyArray<string>;
14
+ };
8
15
  };
9
16
  declare const SvGridChartView: import("svelte").Component<$$ComponentProps, {}, "">;
10
17
  type SvGridChartView = ReturnType<typeof SvGridChartView>;
@@ -10,6 +10,7 @@
10
10
  -->
11
11
  <script lang="ts" generics="TData">
12
12
  import type { ServerDisplayRow } from './server-data-source'
13
+ import type { GridIcons } from './grid-icons'
13
14
 
14
15
  type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
15
16
  type Props = {
@@ -24,9 +25,17 @@
24
25
  leafField?: string
25
26
  /** Pixels of indentation per level. Default 18. */
26
27
  indent?: number
28
+ /**
29
+ * Icon overrides, the same map `<SvGrid icons>` takes. Only
30
+ * `chevron-right` is drawn here. You mount this component yourself, so it
31
+ * cannot inherit the grid's icons - pass the same object to both to keep
32
+ * the expander consistent with the rest of the chrome.
33
+ */
34
+ icons?: GridIcons
27
35
  }
28
36
 
29
- let { row, onToggle, leafField, indent = 18 }: Props = $props()
37
+ let { row, onToggle, leafField, indent = 18, icons }: Props = $props()
38
+ const chevron = $derived(icons?.['chevron-right'])
30
39
  const meta = $derived(row.__group)
31
40
  </script>
32
41
 
@@ -42,8 +51,12 @@
42
51
  <!-- Same chevron the grid's own expanders and SvTree draw, so every
43
52
  expander in the library looks identical. -->
44
53
  <span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">
45
- <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
46
- stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
54
+ {#if chevron}
55
+ {@render chevron()}
56
+ {:else}
57
+ <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
58
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
59
+ {/if}
47
60
  </span>
48
61
  <span class="sv-group-key">{meta.key}</span>
49
62
  {#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
@@ -84,9 +97,21 @@
84
97
  display: inline-flex;
85
98
  align-items: center;
86
99
  justify-content: center;
100
+ width: 12px;
101
+ height: 12px;
87
102
  transition: transform 0.15s;
88
103
  color: var(--sg-muted, #64748b);
89
104
  }
105
+ /* An `icons` override renders inside this wrapper, and this style block is
106
+ scoped, so its markup needs :global() to be reached. Sizing it here means
107
+ a custom chevron lands in the same box as the built-in and rotates with
108
+ the wrapper rather than needing to know the rule exists. */
109
+ .sv-group-chev :global(svg),
110
+ .sv-group-chev :global(img) {
111
+ width: 100%;
112
+ height: 100%;
113
+ display: block;
114
+ }
90
115
  .sv-group-chev.open { transform: rotate(90deg); }
91
116
  .sv-group-key { font-weight: 600; }
92
117
  .sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
@@ -1,4 +1,5 @@
1
1
  import type { ServerDisplayRow } from './server-data-source.js';
2
+ import type { GridIcons } from './grid-icons.js';
2
3
  declare function $$render<TData>(): {
3
4
  props: {
4
5
  /** The grid row from `serverGroupRows` (carries the `__group` marker). */
@@ -16,6 +17,13 @@ declare function $$render<TData>(): {
16
17
  leafField?: string;
17
18
  /** Pixels of indentation per level. Default 18. */
18
19
  indent?: number;
20
+ /**
21
+ * Icon overrides, the same map `<SvGrid icons>` takes. Only
22
+ * `chevron-right` is drawn here. You mount this component yourself, so it
23
+ * cannot inherit the grid's icons - pass the same object to both to keep
24
+ * the expander consistent with the rest of the chrome.
25
+ */
26
+ icons?: GridIcons;
19
27
  };
20
28
  exports: {};
21
29
  bindings: "";
@@ -12,6 +12,8 @@
12
12
  />
13
13
  -->
14
14
  <script lang="ts">
15
+ import { GRID_ICON_GLYPHS, type GridIconName, type GridIcons } from './grid-icons'
16
+
15
17
  type Col = { id: string; label: string }
16
18
  type Props = {
17
19
  /** Columns that can be grouped on. */
@@ -22,8 +24,15 @@
22
24
  onChange: (groupBy: string[]) => void
23
25
  /** Text shown when nothing is grouped. */
24
26
  placeholder?: string
27
+ /**
28
+ * Icon overrides, the same map `<SvGrid icons>` takes. This panel draws
29
+ * `drag-handle`, `remove` and `breadcrumb-separator`. You mount it
30
+ * yourself, so pass the same object you gave the grid to keep them
31
+ * consistent.
32
+ */
33
+ icons?: GridIcons
25
34
  }
26
- let { columns, groupBy, onChange, placeholder = 'Drag a column here to group by it' }: Props = $props()
35
+ let { columns, groupBy, onChange, placeholder = 'Drag a column here to group by it', icons }: Props = $props()
27
36
 
28
37
  const labelOf = (id: string) => columns.find((c) => c.id === id)?.label ?? id
29
38
  const available = $derived(columns.filter((c) => !groupBy.includes(c.id)))
@@ -68,6 +77,13 @@
68
77
  }
69
78
  </script>
70
79
 
80
+ <!-- Same override-or-default resolution SvGrid uses, kept local because this
81
+ component is mounted by the consumer and never sees the grid's snippet. -->
82
+ {#snippet ic(name: GridIconName)}
83
+ {@const override = icons?.[name]}
84
+ {#if override}{@render override()}{:else}{GRID_ICON_GLYPHS[name]}{/if}
85
+ {/snippet}
86
+
71
87
  <div
72
88
  class="sv-rowgroup-panel"
73
89
  role="group"
@@ -80,7 +96,7 @@
80
96
  <span class="sv-rgp-empty">{placeholder}</span>
81
97
  {:else}
82
98
  {#each groupBy as id, i (id)}
83
- {#if i > 0}<span class="sv-rgp-sep" aria-hidden="true">›</span>{/if}
99
+ {#if i > 0}<span class="sv-rgp-sep" aria-hidden="true">{@render ic('breadcrumb-separator')}</span>{/if}
84
100
  <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
85
101
  <span
86
102
  class="sv-rgp-chip"
@@ -95,9 +111,9 @@
95
111
  ondrop={() => reorder(i)}
96
112
  onkeydown={(e) => onChipKeydown(e, i)}
97
113
  >
98
- <span class="sv-rgp-grip" aria-hidden="true">⠿</span>
114
+ <span class="sv-rgp-grip" aria-hidden="true">{@render ic('drag-handle')}</span>
99
115
  {labelOf(id)}
100
- <button type="button" class="sv-rgp-x" onclick={() => remove(id)} aria-label={`Stop grouping by ${labelOf(id)}`}>×</button>
116
+ <button type="button" class="sv-rgp-x" onclick={() => remove(id)} aria-label={`Stop grouping by ${labelOf(id)}`}>{@render ic('remove')}</button>
101
117
  </span>
102
118
  {/each}
103
119
  {/if}
@@ -1,3 +1,4 @@
1
+ import { type GridIcons } from './grid-icons.js';
1
2
  type Col = {
2
3
  id: string;
3
4
  label: string;
@@ -11,6 +12,13 @@ type Props = {
11
12
  onChange: (groupBy: string[]) => void;
12
13
  /** Text shown when nothing is grouped. */
13
14
  placeholder?: string;
15
+ /**
16
+ * Icon overrides, the same map `<SvGrid icons>` takes. This panel draws
17
+ * `drag-handle`, `remove` and `breadcrumb-separator`. You mount it
18
+ * yourself, so pass the same object you gave the grid to keep them
19
+ * consistent.
20
+ */
21
+ icons?: GridIcons;
14
22
  };
15
23
  declare const SvRowGroupPanel: import("svelte").Component<Props, {}, "">;
16
24
  type SvRowGroupPanel = ReturnType<typeof SvRowGroupPanel>;
package/dist/ai.d.ts CHANGED
@@ -266,8 +266,17 @@ export type AIAnomalyOptions = {
266
266
  * export just those".
267
267
  */
268
268
  export declare function aiFindAnomalies<TFeatures extends TableFeatures, TData extends RowData>(api: SvGridApi<TFeatures, TData>, opts?: AIAnomalyOptions): Promise<AIAnomalyResult>;
269
- /** Chart shapes the model may choose from when planning a visualisation. */
270
- export type AIChartType = 'bar' | 'line' | 'area' | 'pie';
269
+ /**
270
+ * Chart shapes the model may choose from when planning a visualisation.
271
+ *
272
+ * These are exactly the types the built-in panel can build from a dimension, a
273
+ * measure and an optional split - which is all a chart plan carries. Two
274
+ * families are deliberately absent, because a plan cannot express them:
275
+ * scatter needs a SECOND measure for its y axis, and candlestick / OHLC need
276
+ * four under first/max/min/last. Asking for either would produce a plan the
277
+ * panel renders as an empty frame, which is worse than not offering it.
278
+ */
279
+ export type AIChartType = 'bar' | 'line' | 'area' | 'pie' | 'treemap' | 'funnel' | 'waterfall' | 'radar' | 'heatmap' | 'boxplot' | 'gauge' | 'calendar' | 'sankey';
271
280
  /** A chart the model proposed: its type, and the fields to plot. */
272
281
  export type AIChartPlan = {
273
282
  type: AIChartType;