@svgrid/grid 3.0.1 → 3.0.3
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.
- package/dist/GridFooter.svelte +14 -5
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/GridMenus.svelte +18 -10
- package/dist/GridMenus.svelte.d.ts +2 -1
- package/dist/SvGrid.controller.svelte.d.ts +17 -0
- package/dist/SvGrid.controller.svelte.js +128 -14
- package/dist/SvGrid.css +79 -10
- package/dist/SvGrid.svelte +191 -186
- package/dist/SvGrid.types.d.ts +42 -1
- package/dist/SvGridChart.svelte +571 -41
- package/dist/SvGridChart.svelte.d.ts +63 -1
- package/dist/SvGridChartPanel.svelte +169 -21
- package/dist/SvGridChartView.svelte +9 -0
- package/dist/SvGridChartView.svelte.d.ts +7 -0
- package/dist/SvGroupCell.svelte +28 -3
- package/dist/SvGroupCell.svelte.d.ts +8 -0
- package/dist/SvRowGroupPanel.svelte +20 -4
- package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
- package/dist/ai.d.ts +11 -2
- package/dist/ai.js +51 -7
- package/dist/cdn/GridMenus-CHOMnW-c.js +602 -0
- package/dist/cdn/{GridMenus-DFB6iwF8.js → GridMenus-DhdPF700.js} +152 -150
- package/dist/cdn/SvGridChart-1PMSp2LZ.js +1481 -0
- package/dist/cdn/SvGridChart-D02avhyr.js +1480 -0
- package/dist/cdn/SvGridChartPanel-DJnViOv4.js +834 -0
- package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +810 -0
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-C--HhdUo.js} +5 -3
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-KyyFG-wY.js} +5 -3
- package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
- package/dist/cdn/chart-DLg3_zTQ.js +1652 -0
- package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
- package/dist/cdn/{src-C8QjSaq-.js → src-Bugf5XlI.js} +3608 -3414
- package/dist/cdn/{src-D0hO2eYN.js → src-D6XvA1Ij.js} +8617 -8423
- package/dist/cdn/svgrid.js +16 -15
- package/dist/cdn/svgrid.svelte-external.js +16 -15
- package/dist/cell-formatting.d.ts +7 -0
- package/dist/cell-formatting.js +7 -1
- package/dist/chart-export.d.ts +23 -2
- package/dist/chart-export.js +80 -6
- package/dist/chart.d.ts +410 -18
- package/dist/chart.js +824 -24
- package/dist/grid-icons.d.ts +49 -0
- package/dist/grid-icons.js +86 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +11 -11
- package/src/GridFooter.svelte +14 -5
- package/src/GridMenus.svelte +18 -10
- package/src/SvGrid.controller.svelte.ts +146 -13
- package/src/SvGrid.css +79 -10
- package/src/SvGrid.svelte +191 -186
- package/src/SvGrid.types.ts +42 -1
- package/src/SvGridChart.svelte +571 -41
- package/src/SvGridChart.test.ts +243 -1
- package/src/SvGridChartPanel.svelte +169 -21
- package/src/SvGridChartView.svelte +9 -0
- package/src/SvGroupCell.svelte +28 -3
- package/src/SvRowGroupPanel.svelte +20 -4
- package/src/ai.test.ts +60 -1
- package/src/ai.ts +59 -6
- package/src/cell-formatting.ts +7 -1
- package/src/chart-export.test.ts +112 -0
- package/src/chart-export.ts +76 -8
- package/src/chart.test.ts +830 -1
- package/src/chart.ts +1053 -32
- package/src/grid-icons.test.ts +46 -0
- package/src/grid-icons.ts +184 -0
- package/src/icon-overrides.grid.test.ts +188 -0
- package/src/icon-seam.test.ts +100 -0
- package/src/index.ts +19 -0
- package/src/svgrid.charting.test.ts +120 -0
- package/dist/cdn/GridMenus-CYzOvmNQ.js +0 -600
- package/dist/cdn/SvGridChart-BEJmNNx9.js +0 -1203
- package/dist/cdn/SvGridChart-BwVos976.js +0 -1202
- package/dist/cdn/SvGridChartPanel-BOknOkrP.js +0 -561
- package/dist/cdn/SvGridChartPanel-D2PjII4O.js +0 -585
- 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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
432
|
-
<
|
|
433
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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>;
|
package/dist/SvGroupCell.svelte
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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"
|
|
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"
|
|
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)}`}
|
|
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
|
-
/**
|
|
270
|
-
|
|
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;
|