@svgrid/grid 2.1.6 → 2.1.8
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/SvCarousel.svelte +35 -6
- package/dist/SvCarousel.svelte.d.ts +3 -0
- package/dist/SvColorInput.svelte +0 -1
- package/dist/SvContextMenu.svelte +26 -6
- package/dist/SvDrawer.svelte +5 -3
- package/dist/SvGrid.controller.svelte.d.ts +85 -2
- package/dist/SvGrid.controller.svelte.js +494 -1
- package/dist/SvGrid.css +142 -0
- package/dist/SvGrid.svelte +49 -27
- package/dist/SvGrid.types.d.ts +78 -0
- package/dist/SvGridBoard.svelte +71 -13
- package/dist/SvGridChart.svelte +41 -7
- package/dist/SvGridChart.svelte.d.ts +4 -0
- package/dist/SvGridChartPanel.svelte +485 -0
- package/dist/SvGridChartPanel.svelte.d.ts +28 -0
- package/dist/SvGridDropdown.svelte +18 -2
- package/dist/SvGroupCell.svelte +5 -5
- package/dist/SvMenu.svelte +25 -2
- package/dist/SvMenu.svelte.d.ts +3 -0
- package/dist/SvMenuList.svelte +31 -9
- package/dist/SvMenuList.svelte.d.ts +9 -1
- package/dist/SvNumberInput.svelte +1 -1
- package/dist/SvOtpInput.svelte +3 -2
- package/dist/SvPopover.svelte +13 -1
- package/dist/SvRowGroupPanel.svelte +23 -0
- package/dist/SvScrollArea.svelte +13 -2
- package/dist/SvScrollArea.svelte.d.ts +2 -0
- package/dist/SvTooltip.svelte +27 -2
- package/dist/SvTooltip.svelte.d.ts +3 -2
- package/dist/SvTour.svelte +30 -5
- package/dist/SvTour.svelte.d.ts +3 -0
- package/dist/SvTree.svelte +7 -3
- package/dist/SvTreeSelect.svelte +16 -9
- package/dist/build-api.js +51 -0
- package/dist/cdn/svgrid.js +17394 -16052
- package/dist/cdn/svgrid.svelte-external.js +16784 -15644
- package/dist/chart-export.d.ts +21 -0
- package/dist/chart-export.js +52 -11
- package/dist/chart.d.ts +30 -0
- package/dist/chart.js +53 -13
- package/dist/createAutocomplete.svelte.d.ts +6 -0
- package/dist/createAutocomplete.svelte.js +10 -0
- package/dist/createCombobox.svelte.d.ts +1 -0
- package/dist/createCombobox.svelte.js +1 -0
- package/dist/createCountryInput.svelte.d.ts +5 -0
- package/dist/createCountryInput.svelte.js +11 -0
- package/dist/createDropdownList.svelte.d.ts +4 -0
- package/dist/createDropdownList.svelte.js +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +5 -3
- package/dist/createPopoverSelect.svelte.js +5 -3
- package/dist/createTree.svelte.d.ts +1 -0
- package/dist/createTree.svelte.js +15 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +58 -0
- package/package.json +1 -1
- package/src/SvCarousel.svelte +35 -6
- package/src/SvColorInput.svelte +0 -1
- package/src/SvContextMenu.svelte +26 -6
- package/src/SvDrawer.svelte +5 -3
- package/src/SvGrid.controller.svelte.ts +481 -0
- package/src/SvGrid.css +142 -0
- package/src/SvGrid.svelte +49 -27
- package/src/SvGrid.types.ts +83 -0
- package/src/SvGridBoard.svelte +71 -13
- package/src/SvGridChart.svelte +41 -7
- package/src/SvGridChartPanel.svelte +485 -0
- package/src/SvGridDropdown.svelte +18 -2
- package/src/SvGroupCell.svelte +5 -5
- package/src/SvMenu.svelte +25 -2
- package/src/SvMenuList.svelte +31 -9
- package/src/SvNumberInput.svelte +1 -1
- package/src/SvOtpInput.svelte +3 -2
- package/src/SvPopover.svelte +13 -1
- package/src/SvRowGroupPanel.svelte +23 -0
- package/src/SvScrollArea.svelte +13 -2
- package/src/SvTooltip.svelte +27 -2
- package/src/SvTour.svelte +30 -5
- package/src/SvTree.svelte +7 -3
- package/src/SvTreeSelect.svelte +16 -9
- package/src/build-api.ts +49 -0
- package/src/chart-export.ts +57 -0
- package/src/chart.ts +70 -13
- package/src/createAutocomplete.svelte.ts +11 -0
- package/src/createCombobox.svelte.ts +1 -0
- package/src/createCountryInput.svelte.ts +12 -0
- package/src/createDropdownList.svelte.ts +8 -0
- package/src/createPopoverSelect.svelte.ts +5 -3
- package/src/createTree.svelte.ts +15 -2
- package/src/index.ts +6 -0
- package/src/overlays.test.ts +5 -0
- package/src/svgrid-wrapper.types.ts +62 -0
- package/src/svgrid.charting.test.ts +527 -0
- package/src/test-setup.ts +22 -0
package/dist/chart-export.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export a rendered `SvGridChart` to a standalone SVG file or a PNG image.
|
|
3
|
+
*
|
|
4
|
+
* The on-screen chart styles its axes / labels / gridlines with CSS classes
|
|
5
|
+
* and `--sg-*` theme variables, which don't survive a raw serialization. So
|
|
6
|
+
* we clone the SVG, read the live computed colors, and inline a small `<style>`
|
|
7
|
+
* block with concrete values - the export looks like what's on screen.
|
|
8
|
+
*
|
|
9
|
+
* Pass the chart's `<svg class="sv-grid-chart-svg">` element (or any ancestor
|
|
10
|
+
* that contains it).
|
|
11
|
+
*/
|
|
12
|
+
import type { ChartSpec } from './chart';
|
|
1
13
|
export type ChartExportOptions = {
|
|
2
14
|
/** Background color of the exported image. Default: the grid background. */
|
|
3
15
|
background?: string;
|
|
@@ -12,3 +24,12 @@ export declare function downloadChartSvg(source: SVGSVGElement | HTMLElement, fi
|
|
|
12
24
|
export declare function chartToPngBlob(source: SVGSVGElement | HTMLElement, options?: ChartExportOptions): Promise<Blob>;
|
|
13
25
|
/** Download the chart as a `.png` file. */
|
|
14
26
|
export declare function downloadChartPng(source: SVGSVGElement | HTMLElement, filename?: string, options?: ChartExportOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a chart's data to CSV: one row per category, one column per series
|
|
29
|
+
* (`Category, <series 1>, ...`). Empty for exotic specs (sankey/treemap/gauge/
|
|
30
|
+
* calendar) that don't carry a rectangular categories x series grid.
|
|
31
|
+
*/
|
|
32
|
+
export declare function chartSpecToCsv(spec: ChartSpec): string;
|
|
33
|
+
/** Download the chart's data as a `.csv` file. Returns false if there's no
|
|
34
|
+
* rectangular category/series data to export. */
|
|
35
|
+
export declare function downloadChartCsv(spec: ChartSpec, filename?: string): boolean;
|
package/dist/chart-export.js
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Export a rendered `SvGridChart` to a standalone SVG file or a PNG image.
|
|
3
|
-
*
|
|
4
|
-
* The on-screen chart styles its axes / labels / gridlines with CSS classes
|
|
5
|
-
* and `--sg-*` theme variables, which don't survive a raw serialization. So
|
|
6
|
-
* we clone the SVG, read the live computed colors, and inline a small `<style>`
|
|
7
|
-
* block with concrete values - the export looks like what's on screen.
|
|
8
|
-
*
|
|
9
|
-
* Pass the chart's `<svg class="sv-grid-chart-svg">` element (or any ancestor
|
|
10
|
-
* that contains it).
|
|
11
|
-
*/
|
|
12
1
|
function resolveSvg(el) {
|
|
13
2
|
if (el instanceof SVGSVGElement)
|
|
14
3
|
return el;
|
|
@@ -34,6 +23,29 @@ export function chartToSvgString(source, options = {}) {
|
|
|
34
23
|
clone.setAttribute('width', String(w));
|
|
35
24
|
clone.setAttribute('height', String(h));
|
|
36
25
|
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
26
|
+
// Inline computed paint/text styles so the export is what's on screen -
|
|
27
|
+
// dynamic SVG presentation attributes (a line's `stroke={color}`) and scoped
|
|
28
|
+
// component CSS don't survive a raw clone + XMLSerializer. Walk the live tree
|
|
29
|
+
// and its clone (1:1 here, before we prune hit layers) and copy the resolved
|
|
30
|
+
// style, so line paths etc. export with their real stroke instead of nothing.
|
|
31
|
+
const PAINT_PROPS = [
|
|
32
|
+
'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity',
|
|
33
|
+
'stroke-dasharray', 'stroke-linecap', 'stroke-linejoin', 'opacity',
|
|
34
|
+
'font-size', 'font-family', 'font-weight', 'text-anchor',
|
|
35
|
+
];
|
|
36
|
+
const liveNodes = svg.querySelectorAll('*');
|
|
37
|
+
const cloneNodes = clone.querySelectorAll('*');
|
|
38
|
+
for (let i = 0; i < liveNodes.length; i += 1) {
|
|
39
|
+
const el = cloneNodes[i];
|
|
40
|
+
if (!el || !el.style)
|
|
41
|
+
continue;
|
|
42
|
+
const cs = getComputedStyle(liveNodes[i]);
|
|
43
|
+
for (const prop of PAINT_PROPS) {
|
|
44
|
+
const v = cs.getPropertyValue(prop);
|
|
45
|
+
if (v && v !== 'normal')
|
|
46
|
+
el.style.setProperty(prop, v);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
// Drop interaction-only hit layers.
|
|
38
50
|
clone.querySelectorAll('.sv-grid-chart-hit').forEach((n) => n.remove());
|
|
39
51
|
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
@@ -110,3 +122,32 @@ export async function downloadChartPng(source, filename = 'chart.png', options =
|
|
|
110
122
|
triggerDownload(url, filename);
|
|
111
123
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
112
124
|
}
|
|
125
|
+
const csvCell = (v) => {
|
|
126
|
+
const s = v == null ? '' : String(v);
|
|
127
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Serialize a chart's data to CSV: one row per category, one column per series
|
|
131
|
+
* (`Category, <series 1>, ...`). Empty for exotic specs (sankey/treemap/gauge/
|
|
132
|
+
* calendar) that don't carry a rectangular categories x series grid.
|
|
133
|
+
*/
|
|
134
|
+
export function chartSpecToCsv(spec) {
|
|
135
|
+
const cats = spec.categories ?? [];
|
|
136
|
+
const series = spec.series ?? [];
|
|
137
|
+
if (!cats.length || !series.length)
|
|
138
|
+
return '';
|
|
139
|
+
const header = ['Category', ...series.map((s) => s.label)].map(csvCell).join(',');
|
|
140
|
+
const rows = cats.map((cat, i) => [cat, ...series.map((s) => s.values[i] ?? '')].map(csvCell).join(','));
|
|
141
|
+
return [header, ...rows].join('\n');
|
|
142
|
+
}
|
|
143
|
+
/** Download the chart's data as a `.csv` file. Returns false if there's no
|
|
144
|
+
* rectangular category/series data to export. */
|
|
145
|
+
export function downloadChartCsv(spec, filename = 'chart.csv') {
|
|
146
|
+
const csv = chartSpecToCsv(spec);
|
|
147
|
+
if (!csv)
|
|
148
|
+
return false;
|
|
149
|
+
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
|
150
|
+
triggerDownload(url, filename);
|
|
151
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
package/dist/chart.d.ts
CHANGED
|
@@ -101,6 +101,19 @@ export type ChartSpec = {
|
|
|
101
101
|
height?: number;
|
|
102
102
|
/** Palette used when a series has no explicit `color`. */
|
|
103
103
|
palette?: string[];
|
|
104
|
+
/** Per-category color overrides (by category label) - for pie / donut slice
|
|
105
|
+
* recolouring, where colour follows the category, not a series. */
|
|
106
|
+
categoryColors?: Record<string, string>;
|
|
107
|
+
/** Number format for the value axis, tooltips, data labels and reference
|
|
108
|
+
* lines. Unset = the default compact `1.2k` / `1.2M` style. */
|
|
109
|
+
valueFormat?: ChartValueFormat;
|
|
110
|
+
/** Grouped (nested) category axis: a parent tier spanning consecutive leaf
|
|
111
|
+
* categories (spans must sum to `categories.length`). Vertical category
|
|
112
|
+
* charts only (ignored for time / horizontal / pie). */
|
|
113
|
+
categoryGroups?: Array<{
|
|
114
|
+
label: string;
|
|
115
|
+
span: number;
|
|
116
|
+
}>;
|
|
104
117
|
/** Stack bar / area series (per axis) instead of grouping them. */
|
|
105
118
|
stacked?: boolean;
|
|
106
119
|
/** Stack to 100% (each category normalized to its total). Implies stacked. */
|
|
@@ -441,6 +454,14 @@ export type ChartGeometry = {
|
|
|
441
454
|
y2Ticks: ChartAxisTick[];
|
|
442
455
|
hasRightAxis: boolean;
|
|
443
456
|
xTicks: ChartCategoryTick[];
|
|
457
|
+
/** Grouped category axis parent tier: label + span extent (pixels). Empty
|
|
458
|
+
* unless `spec.categoryGroups` is set on a vertical category chart. */
|
|
459
|
+
categoryGroupTicks: Array<{
|
|
460
|
+
label: string;
|
|
461
|
+
xCenter: number;
|
|
462
|
+
x0: number;
|
|
463
|
+
x1: number;
|
|
464
|
+
}>;
|
|
444
465
|
/** True when x labels are long/many and should be rotated. */
|
|
445
466
|
xLabelRotated: boolean;
|
|
446
467
|
legend: ChartLegendItem[];
|
|
@@ -558,6 +579,15 @@ export declare function exponentialMovingAverage(values: number[], period: numbe
|
|
|
558
579
|
export declare function computeOverlay(values: number[], spec: SeriesOverlay): number[];
|
|
559
580
|
/** Round a [min,max] domain out to nice tick boundaries. */
|
|
560
581
|
export declare function niceScale(min: number, max: number, tickCount?: number): NiceScale;
|
|
582
|
+
/** Value-axis / tooltip / label number format. */
|
|
583
|
+
export type ChartValueFormat = 'number' | 'currency' | 'percent' | 'compact';
|
|
584
|
+
/**
|
|
585
|
+
* Format a numeric value for display, honouring an optional `valueFormat`.
|
|
586
|
+
* Builds on the compact `1.2k` / `1.2M` base: currency prefixes `$` (sign
|
|
587
|
+
* outside), percent multiplies by 100 and suffixes `%`. Unset / `'number'` /
|
|
588
|
+
* `'compact'` = the plain compact form.
|
|
589
|
+
*/
|
|
590
|
+
export declare function formatChartValue(n: number, format?: ChartValueFormat): string;
|
|
561
591
|
export declare function buildChart(spec: ChartSpec, theme?: 'light' | 'dark'): ChartGeometry;
|
|
562
592
|
/**
|
|
563
593
|
* Aggregate flat rows into a chart spec. Group by a category field, reduce a
|
package/dist/chart.js
CHANGED
|
@@ -314,6 +314,23 @@ function fmtTick(n) {
|
|
|
314
314
|
return `${(n / 1000).toFixed(abs % 1000 ? 1 : 0)}k`;
|
|
315
315
|
return String(Math.round(n * 100) / 100);
|
|
316
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Format a numeric value for display, honouring an optional `valueFormat`.
|
|
319
|
+
* Builds on the compact `1.2k` / `1.2M` base: currency prefixes `$` (sign
|
|
320
|
+
* outside), percent multiplies by 100 and suffixes `%`. Unset / `'number'` /
|
|
321
|
+
* `'compact'` = the plain compact form.
|
|
322
|
+
*/
|
|
323
|
+
export function formatChartValue(n, format) {
|
|
324
|
+
if (!Number.isFinite(n))
|
|
325
|
+
return '';
|
|
326
|
+
if (format === 'currency')
|
|
327
|
+
return `${n < 0 ? '-' : ''}$${fmtTick(Math.abs(n))}`;
|
|
328
|
+
if (format === 'percent') {
|
|
329
|
+
const p = n * 100;
|
|
330
|
+
return `${Math.round(p * 10) / 10}%`;
|
|
331
|
+
}
|
|
332
|
+
return fmtTick(n);
|
|
333
|
+
}
|
|
317
334
|
const DAY = 86400000;
|
|
318
335
|
/** Nice date-tick timestamps across [min, max]. */
|
|
319
336
|
function dateTicks(tMin, tMax) {
|
|
@@ -413,6 +430,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
413
430
|
y2Ticks: [],
|
|
414
431
|
hasRightAxis: false,
|
|
415
432
|
xTicks: [],
|
|
433
|
+
categoryGroupTicks: [],
|
|
416
434
|
xLabelRotated: false,
|
|
417
435
|
legend,
|
|
418
436
|
donut: null,
|
|
@@ -527,7 +545,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
527
545
|
x: round(padL + slotW * i + slotW / 2),
|
|
528
546
|
}));
|
|
529
547
|
const yTicks = dom.ticks.map((value) => ({
|
|
530
|
-
value, y: yOfW(value), label:
|
|
548
|
+
value, y: yOfW(value), label: formatChartValue(value, spec.valueFormat),
|
|
531
549
|
}));
|
|
532
550
|
return {
|
|
533
551
|
...empty,
|
|
@@ -723,7 +741,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
723
741
|
const legend = Array.from({ length: 5 }, (_, i) => {
|
|
724
742
|
const t = i / 4;
|
|
725
743
|
const value = vMin + (vMax - vMin) * t;
|
|
726
|
-
return { value, color: colorAt(value), label:
|
|
744
|
+
return { value, color: colorAt(value), label: formatChartValue(value, spec.valueFormat) };
|
|
727
745
|
});
|
|
728
746
|
return {
|
|
729
747
|
...empty,
|
|
@@ -1110,7 +1128,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1110
1128
|
const heatmapLegend = Array.from({ length: 5 }, (_, i) => {
|
|
1111
1129
|
const t = i / 4;
|
|
1112
1130
|
const value = vMin + (vMax - vMin) * t;
|
|
1113
|
-
return { value, color: colorAt(value), label:
|
|
1131
|
+
return { value, color: colorAt(value), label: formatChartValue(value, spec.valueFormat) };
|
|
1114
1132
|
});
|
|
1115
1133
|
return {
|
|
1116
1134
|
...empty,
|
|
@@ -1163,10 +1181,11 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1163
1181
|
else {
|
|
1164
1182
|
path = `M${round(cx)},${round(cy)} L${round(ox0)},${round(oy0)} A${r},${r} 0 ${large} 1 ${round(ox1)},${round(oy1)} Z`;
|
|
1165
1183
|
}
|
|
1184
|
+
const catLabel = spec.categories[i] ?? String(i);
|
|
1166
1185
|
return {
|
|
1167
1186
|
path,
|
|
1168
|
-
color: palette[i % palette.length],
|
|
1169
|
-
label:
|
|
1187
|
+
color: spec.categoryColors?.[catLabel] ?? palette[i % palette.length],
|
|
1188
|
+
label: catLabel,
|
|
1170
1189
|
value: v,
|
|
1171
1190
|
percent: frac * 100,
|
|
1172
1191
|
cx: round(cx + labelR * Math.cos(mid)),
|
|
@@ -1176,7 +1195,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1176
1195
|
return {
|
|
1177
1196
|
...empty,
|
|
1178
1197
|
slices,
|
|
1179
|
-
legend: spec.categories.map((label, i) => ({ label, color: palette[i % palette.length] })),
|
|
1198
|
+
legend: spec.categories.map((label, i) => ({ label, color: spec.categoryColors?.[label] ?? palette[i % palette.length] })),
|
|
1180
1199
|
donut: ir > 0 ? { cx: round(cx), cy: round(cy), r: round(ir), total: s.values.reduce((a, b) => a + Math.max(0, b), 0) } : null,
|
|
1181
1200
|
};
|
|
1182
1201
|
}
|
|
@@ -1239,7 +1258,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1239
1258
|
}
|
|
1240
1259
|
const referenceLines = (spec.referenceLines ?? []).map((ref) => ({
|
|
1241
1260
|
y: yOf(ref.value),
|
|
1242
|
-
label: ref.label ??
|
|
1261
|
+
label: ref.label ?? formatChartValue(ref.value, spec.valueFormat),
|
|
1243
1262
|
color: ref.color ?? '#ef4444',
|
|
1244
1263
|
dashed: ref.dashed !== false,
|
|
1245
1264
|
}));
|
|
@@ -1248,7 +1267,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1248
1267
|
plot,
|
|
1249
1268
|
scatterPoints,
|
|
1250
1269
|
referenceLines,
|
|
1251
|
-
yTicks: yDom.ticks.map((value) => ({ value, y: yOf(value), label:
|
|
1270
|
+
yTicks: yDom.ticks.map((value) => ({ value, y: yOf(value), label: formatChartValue(value, spec.valueFormat) })),
|
|
1252
1271
|
xTicks: xDom.ticks.map((value) => ({ label: fmtTick(value), x: xOf(value) })),
|
|
1253
1272
|
};
|
|
1254
1273
|
}
|
|
@@ -1331,7 +1350,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1331
1350
|
});
|
|
1332
1351
|
}
|
|
1333
1352
|
const valueTicks = dom.ticks.map((value) => ({
|
|
1334
|
-
label: spec.stacked100 ? `${fmtTick(value)}%` :
|
|
1353
|
+
label: spec.stacked100 ? `${fmtTick(value)}%` : formatChartValue(value, spec.valueFormat),
|
|
1335
1354
|
x: xOf(value),
|
|
1336
1355
|
}));
|
|
1337
1356
|
const catTicks = spec.categories.map((label, i) => ({
|
|
@@ -1341,7 +1360,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1341
1360
|
}));
|
|
1342
1361
|
const referenceLinesV = (spec.referenceLines ?? []).map((ref) => ({
|
|
1343
1362
|
x: xOf(ref.value),
|
|
1344
|
-
label: ref.label ??
|
|
1363
|
+
label: ref.label ?? formatChartValue(ref.value, spec.valueFormat),
|
|
1345
1364
|
color: ref.color ?? '#ef4444',
|
|
1346
1365
|
dashed: ref.dashed !== false,
|
|
1347
1366
|
}));
|
|
@@ -1362,10 +1381,19 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1362
1381
|
const hasRightAxis = rightSeries.length > 0;
|
|
1363
1382
|
const maxLabel = spec.categories.reduce((m, c) => Math.max(m, c.length), 0);
|
|
1364
1383
|
const xLabelRotated = spec.categories.length > 8 || maxLabel > 9;
|
|
1384
|
+
// Grouped (nested) category axis: valid only when the spans cover every leaf.
|
|
1385
|
+
const validGroups = spec.categoryGroups &&
|
|
1386
|
+
spec.categoryGroups.length > 0 &&
|
|
1387
|
+
spec.xType !== 'time' &&
|
|
1388
|
+
spec.orientation !== 'horizontal' &&
|
|
1389
|
+
spec.categoryGroups.reduce((a, g) => a + g.span, 0) === spec.categories.length
|
|
1390
|
+
? spec.categoryGroups
|
|
1391
|
+
: null;
|
|
1392
|
+
const groupTierH = validGroups ? 18 : 0;
|
|
1365
1393
|
const padL = 48 + (spec.yAxisTitle ? 16 : 0);
|
|
1366
1394
|
const padR = (hasRightAxis ? 48 : 12) + (spec.y2AxisTitle ? 16 : 0);
|
|
1367
1395
|
const padT = 10;
|
|
1368
|
-
const padB = (xLabelRotated ? 54 : 28) + (spec.xAxisTitle ? 16 : 0);
|
|
1396
|
+
const padB = (xLabelRotated ? 54 : 28) + (spec.xAxisTitle ? 16 : 0) + groupTierH;
|
|
1369
1397
|
const plotW = Math.max(1, width - padL - padR);
|
|
1370
1398
|
const plotH = Math.max(1, height - padT - padB);
|
|
1371
1399
|
const plot = { x: padL, y: padT, w: plotW, h: plotH };
|
|
@@ -1410,6 +1438,17 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1410
1438
|
x: round(padL + ((t - tMin) / tSpan) * plotW),
|
|
1411
1439
|
}))
|
|
1412
1440
|
: spec.categories.map((label, i) => ({ label, x: xCenter(i) }));
|
|
1441
|
+
// Parent-tier ticks for a grouped category axis: each spans its leaves.
|
|
1442
|
+
const categoryGroupTicks = [];
|
|
1443
|
+
if (validGroups && !timeOk) {
|
|
1444
|
+
let start = 0;
|
|
1445
|
+
for (const g of validGroups) {
|
|
1446
|
+
const x0 = round(padL + slot * start);
|
|
1447
|
+
const x1 = round(padL + slot * (start + g.span));
|
|
1448
|
+
categoryGroupTicks.push({ label: g.label, x0, x1, xCenter: round((x0 + x1) / 2) });
|
|
1449
|
+
start += g.span;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1413
1452
|
const barSeries = series.filter((s) => s.kind === 'bar');
|
|
1414
1453
|
const bars = [];
|
|
1415
1454
|
if (barSeries.length) {
|
|
@@ -1607,14 +1646,14 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1607
1646
|
}
|
|
1608
1647
|
lines.push({ path, areaPath, color: s.color, label: s.label, points: pts, bandPath });
|
|
1609
1648
|
}
|
|
1610
|
-
const tickFor = (dom, log) => dom.ticks.map((value) => ({ value, y: yOf(dom, value, log), label:
|
|
1649
|
+
const tickFor = (dom, log) => dom.ticks.map((value) => ({ value, y: yOf(dom, value, log), label: formatChartValue(value, spec.valueFormat) }));
|
|
1611
1650
|
const referenceLines = (spec.referenceLines ?? []).map((ref) => {
|
|
1612
1651
|
const onRight = ref.axis === 'right';
|
|
1613
1652
|
const dom = onRight ? (rightDom ?? leftDom) : leftDom;
|
|
1614
1653
|
const log = onRight ? rightLog : leftLog;
|
|
1615
1654
|
return {
|
|
1616
1655
|
y: yOf(dom, ref.value, log),
|
|
1617
|
-
label: ref.label ??
|
|
1656
|
+
label: ref.label ?? formatChartValue(ref.value, spec.valueFormat),
|
|
1618
1657
|
color: ref.color ?? '#ef4444',
|
|
1619
1658
|
dashed: ref.dashed !== false,
|
|
1620
1659
|
};
|
|
@@ -1699,6 +1738,7 @@ export function buildChart(spec, theme = 'light') {
|
|
|
1699
1738
|
y2Ticks: rightDom ? tickFor(rightDom, rightLog) : [],
|
|
1700
1739
|
hasRightAxis,
|
|
1701
1740
|
xTicks,
|
|
1741
|
+
categoryGroupTicks,
|
|
1702
1742
|
xLabelRotated: timeOk ? false : xLabelRotated,
|
|
1703
1743
|
referenceLines,
|
|
1704
1744
|
overlays,
|
|
@@ -46,6 +46,8 @@ export declare function createAutocomplete(config: AutocompleteConfig): {
|
|
|
46
46
|
readonly filtered: ListOption[];
|
|
47
47
|
/** Controlled text value. */
|
|
48
48
|
readonly value: string;
|
|
49
|
+
/** id of the listbox element (matches inputProps `aria-controls`). */
|
|
50
|
+
listId: string;
|
|
49
51
|
isActive: (i: number) => boolean;
|
|
50
52
|
setActive: (i: number) => void;
|
|
51
53
|
maybeOpen: () => void;
|
|
@@ -67,14 +69,18 @@ export declare function createAutocomplete(config: AutocompleteConfig): {
|
|
|
67
69
|
id: string | undefined;
|
|
68
70
|
role: "combobox";
|
|
69
71
|
'aria-expanded': boolean;
|
|
72
|
+
'aria-controls': string;
|
|
70
73
|
'aria-autocomplete': "list";
|
|
74
|
+
'aria-activedescendant': string | undefined;
|
|
71
75
|
};
|
|
72
76
|
/** Spread onto the listbox container. */
|
|
73
77
|
listboxProps: () => {
|
|
78
|
+
id: string;
|
|
74
79
|
role: "listbox";
|
|
75
80
|
};
|
|
76
81
|
/** Spread onto the suggestion at `index` (index into `filtered`). */
|
|
77
82
|
optionProps: (index: number) => {
|
|
83
|
+
id: string;
|
|
78
84
|
role: "option";
|
|
79
85
|
tabindex: number;
|
|
80
86
|
'aria-selected': boolean;
|
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { filterOptions } from './list-option';
|
|
23
23
|
import { editorAria } from './editor-contract';
|
|
24
|
+
let uid = 0;
|
|
24
25
|
export function createAutocomplete(config) {
|
|
26
|
+
const id = `sv-ac-${uid++}`;
|
|
27
|
+
const listId = `${id}-list`;
|
|
28
|
+
const optionId = (index) => `${listId}-opt-${index}`;
|
|
25
29
|
const minChars = () => config.minChars?.() ?? 1;
|
|
26
30
|
const ariaState = () => ({
|
|
27
31
|
id: config.id?.(),
|
|
@@ -73,6 +77,8 @@ export function createAutocomplete(config) {
|
|
|
73
77
|
get filtered() { return filtered; },
|
|
74
78
|
/** Controlled text value. */
|
|
75
79
|
get value() { return config.value(); },
|
|
80
|
+
/** id of the listbox element (matches inputProps `aria-controls`). */
|
|
81
|
+
listId,
|
|
76
82
|
isActive: (i) => i === active,
|
|
77
83
|
setActive: (i) => { active = i; },
|
|
78
84
|
maybeOpen,
|
|
@@ -85,7 +91,9 @@ export function createAutocomplete(config) {
|
|
|
85
91
|
id: config.id?.(),
|
|
86
92
|
role: 'combobox',
|
|
87
93
|
'aria-expanded': open,
|
|
94
|
+
'aria-controls': listId,
|
|
88
95
|
'aria-autocomplete': 'list',
|
|
96
|
+
'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
|
|
89
97
|
...editorAria(ariaState()),
|
|
90
98
|
value: config.value(),
|
|
91
99
|
disabled: config.disabled?.() ?? false,
|
|
@@ -95,10 +103,12 @@ export function createAutocomplete(config) {
|
|
|
95
103
|
}),
|
|
96
104
|
/** Spread onto the listbox container. */
|
|
97
105
|
listboxProps: () => ({
|
|
106
|
+
id: listId,
|
|
98
107
|
role: 'listbox',
|
|
99
108
|
}),
|
|
100
109
|
/** Spread onto the suggestion at `index` (index into `filtered`). */
|
|
101
110
|
optionProps: (index) => ({
|
|
111
|
+
id: optionId(index),
|
|
102
112
|
role: 'option',
|
|
103
113
|
tabindex: -1,
|
|
104
114
|
'aria-selected': index === active,
|
|
@@ -84,6 +84,7 @@ export declare function createCombobox(config: ComboboxConfig): {
|
|
|
84
84
|
'aria-expanded': boolean;
|
|
85
85
|
'aria-controls': string;
|
|
86
86
|
'aria-autocomplete': "list";
|
|
87
|
+
'aria-activedescendant': string | undefined;
|
|
87
88
|
};
|
|
88
89
|
/** Spread onto the chevron/toggle <button>. */
|
|
89
90
|
triggerProps: () => {
|
|
@@ -143,6 +143,7 @@ export function createCombobox(config) {
|
|
|
143
143
|
'aria-expanded': open,
|
|
144
144
|
'aria-controls': listId,
|
|
145
145
|
'aria-autocomplete': 'list',
|
|
146
|
+
'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
|
|
146
147
|
...editorAria(ariaState()),
|
|
147
148
|
value: shownText,
|
|
148
149
|
disabled: disabled(),
|
|
@@ -71,21 +71,26 @@ export declare function createCountryInput(config: CountryInputConfig): {
|
|
|
71
71
|
type: "button";
|
|
72
72
|
'aria-haspopup': "listbox";
|
|
73
73
|
'aria-expanded': boolean;
|
|
74
|
+
'aria-controls': string;
|
|
74
75
|
};
|
|
75
76
|
/** Spread onto the search <input> inside the panel. */
|
|
76
77
|
searchProps: () => {
|
|
77
78
|
type: "text";
|
|
78
79
|
value: string;
|
|
79
80
|
'aria-label': string;
|
|
81
|
+
'aria-controls': string;
|
|
82
|
+
'aria-activedescendant': string | undefined;
|
|
80
83
|
oninput: (e: Event) => void;
|
|
81
84
|
onkeydown: (e: KeyboardEvent) => void;
|
|
82
85
|
};
|
|
83
86
|
/** Spread onto the listbox container. */
|
|
84
87
|
listboxProps: () => {
|
|
88
|
+
id: string;
|
|
85
89
|
role: "listbox";
|
|
86
90
|
};
|
|
87
91
|
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
88
92
|
optionProps: (index: number) => {
|
|
93
|
+
id: string;
|
|
89
94
|
role: "option";
|
|
90
95
|
tabindex: number;
|
|
91
96
|
'aria-selected': boolean;
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { COUNTRIES, COUNTRY_BY_CODE } from './countries';
|
|
24
24
|
import { editorAria } from './editor-contract';
|
|
25
|
+
let uid = 0;
|
|
25
26
|
export function createCountryInput(config) {
|
|
27
|
+
const id = `sv-country-${uid++}`;
|
|
28
|
+
const listId = `${id}-list`;
|
|
29
|
+
const optionId = (i) => `${listId}-opt-${i}`;
|
|
26
30
|
const disabled = () => config.disabled?.() ?? false;
|
|
27
31
|
const ariaState = () => ({
|
|
28
32
|
id: config.id?.(),
|
|
@@ -53,6 +57,8 @@ export function createCountryInput(config) {
|
|
|
53
57
|
function pick(code) { config.onChange?.(code); open = false; config.focusTrigger?.(); }
|
|
54
58
|
function setQuery(v) { query = v; active = 0; }
|
|
55
59
|
function onSearchInput(e) { setQuery(e.currentTarget.value); }
|
|
60
|
+
/** The current active-descendant id (for the focused search input), or undefined. */
|
|
61
|
+
const activeDescendant = () => (open && filtered.length > 0 ? optionId(active) : undefined);
|
|
56
62
|
function onKeydown(e) {
|
|
57
63
|
if (!open)
|
|
58
64
|
return;
|
|
@@ -103,6 +109,7 @@ export function createCountryInput(config) {
|
|
|
103
109
|
type: 'button',
|
|
104
110
|
'aria-haspopup': 'listbox',
|
|
105
111
|
'aria-expanded': open,
|
|
112
|
+
'aria-controls': listId,
|
|
106
113
|
...editorAria(ariaState()),
|
|
107
114
|
disabled: disabled(),
|
|
108
115
|
onclick: toggle,
|
|
@@ -112,17 +119,21 @@ export function createCountryInput(config) {
|
|
|
112
119
|
type: 'text',
|
|
113
120
|
value: query,
|
|
114
121
|
'aria-label': config.searchLabel?.() ?? 'Search countries',
|
|
122
|
+
'aria-controls': listId,
|
|
123
|
+
'aria-activedescendant': activeDescendant(),
|
|
115
124
|
oninput: onSearchInput,
|
|
116
125
|
onkeydown: onKeydown,
|
|
117
126
|
}),
|
|
118
127
|
/** Spread onto the listbox container. */
|
|
119
128
|
listboxProps: () => ({
|
|
129
|
+
id: listId,
|
|
120
130
|
role: 'listbox',
|
|
121
131
|
}),
|
|
122
132
|
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
123
133
|
optionProps: (index) => {
|
|
124
134
|
const c = filtered[index];
|
|
125
135
|
return {
|
|
136
|
+
id: optionId(index),
|
|
126
137
|
role: 'option',
|
|
127
138
|
tabindex: -1,
|
|
128
139
|
'aria-selected': c ? c.code === config.value() : false,
|
|
@@ -67,15 +67,19 @@ export declare function createDropdownList(config: DropdownListConfig): {
|
|
|
67
67
|
type: "button";
|
|
68
68
|
'aria-haspopup': "listbox";
|
|
69
69
|
'aria-expanded': boolean;
|
|
70
|
+
'aria-controls': string;
|
|
71
|
+
'aria-activedescendant': string | undefined;
|
|
70
72
|
};
|
|
71
73
|
/** Spread onto the listbox container. */
|
|
72
74
|
listboxProps: () => {
|
|
75
|
+
id: string;
|
|
73
76
|
role: "listbox";
|
|
74
77
|
tabindex: number;
|
|
75
78
|
};
|
|
76
79
|
/** Spread onto the option at `index` (index into `options`). */
|
|
77
80
|
optionProps: (index: number) => {
|
|
78
81
|
role: "option";
|
|
82
|
+
id: string;
|
|
79
83
|
tabindex: number;
|
|
80
84
|
'aria-selected': boolean;
|
|
81
85
|
'aria-disabled': boolean | undefined;
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { createTypeaheadBuffer, isTypeaheadKey, nextTypeaheadIndex } from './list-option';
|
|
23
23
|
import { editorAria } from './editor-contract';
|
|
24
|
+
let uid = 0;
|
|
24
25
|
export function createDropdownList(config) {
|
|
26
|
+
const listId = `sv-ddl-${uid++}`;
|
|
27
|
+
const optionId = (index) => `${listId}-opt-${index}`;
|
|
25
28
|
const opts = () => config.options();
|
|
26
29
|
const disabled = () => config.disabled?.() ?? false;
|
|
27
30
|
const ariaState = () => ({
|
|
@@ -138,6 +141,8 @@ export function createDropdownList(config) {
|
|
|
138
141
|
type: 'button',
|
|
139
142
|
'aria-haspopup': 'listbox',
|
|
140
143
|
'aria-expanded': open,
|
|
144
|
+
'aria-controls': listId,
|
|
145
|
+
'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
|
|
141
146
|
...editorAria(ariaState()),
|
|
142
147
|
disabled: disabled(),
|
|
143
148
|
onclick: toggle,
|
|
@@ -145,6 +150,7 @@ export function createDropdownList(config) {
|
|
|
145
150
|
}),
|
|
146
151
|
/** Spread onto the listbox container. */
|
|
147
152
|
listboxProps: () => ({
|
|
153
|
+
id: listId,
|
|
148
154
|
role: 'listbox',
|
|
149
155
|
tabindex: -1,
|
|
150
156
|
}),
|
|
@@ -153,6 +159,7 @@ export function createDropdownList(config) {
|
|
|
153
159
|
const o = opts()[index];
|
|
154
160
|
return {
|
|
155
161
|
role: 'option',
|
|
162
|
+
id: optionId(index),
|
|
156
163
|
tabindex: -1,
|
|
157
164
|
'aria-selected': o ? o.value === config.value() : false,
|
|
158
165
|
'aria-disabled': o?.disabled,
|
|
@@ -62,9 +62,11 @@ export declare function createPopoverSelect(config: PopoverSelectConfig): {
|
|
|
62
62
|
onPanelKeydown: (e: KeyboardEvent) => void;
|
|
63
63
|
/** True when item `i` is the roving-active one. */
|
|
64
64
|
isActive: (i: number) => boolean;
|
|
65
|
-
/** Spread onto the trigger button.
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/** Spread onto the trigger button. Pass the ARIA popup type the panel
|
|
66
|
+
* exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
|
|
67
|
+
* the classic select panels. */
|
|
68
|
+
triggerProps: (haspopup?: "listbox" | "tree" | "grid") => {
|
|
69
|
+
'aria-haspopup': "listbox" | "grid" | "tree";
|
|
68
70
|
'aria-expanded': boolean;
|
|
69
71
|
'aria-controls': string | undefined;
|
|
70
72
|
onkeydown: (e: KeyboardEvent) => void;
|
|
@@ -171,9 +171,11 @@ export function createPopoverSelect(config) {
|
|
|
171
171
|
onPanelKeydown,
|
|
172
172
|
/** True when item `i` is the roving-active one. */
|
|
173
173
|
isActive: (i) => i === active,
|
|
174
|
-
/** Spread onto the trigger button.
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
/** Spread onto the trigger button. Pass the ARIA popup type the panel
|
|
175
|
+
* exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
|
|
176
|
+
* the classic select panels. */
|
|
177
|
+
triggerProps: (haspopup = 'listbox') => ({
|
|
178
|
+
'aria-haspopup': haspopup,
|
|
177
179
|
'aria-expanded': open,
|
|
178
180
|
'aria-controls': open ? panelId : undefined,
|
|
179
181
|
onkeydown: onTriggerKeydown,
|
|
@@ -106,6 +106,7 @@ export declare function createTree(config: TreeConfig): {
|
|
|
106
106
|
'aria-selected': boolean;
|
|
107
107
|
'aria-expanded': boolean | undefined;
|
|
108
108
|
'aria-checked': "true" | "mixed" | "false" | undefined;
|
|
109
|
+
'aria-disabled': true | undefined;
|
|
109
110
|
'data-row': number;
|
|
110
111
|
tabindex: number;
|
|
111
112
|
onclick: () => void;
|
|
@@ -184,6 +184,18 @@ export function createTree(config) {
|
|
|
184
184
|
currentlyOn ? next.delete(id) : next.add(id);
|
|
185
185
|
config.onCheck?.([...next]);
|
|
186
186
|
}
|
|
187
|
+
// Step from `from` towards `dir` (+1/-1), skipping disabled rows, so roving
|
|
188
|
+
// focus never lands on a node with zero semantic affordance for AT users.
|
|
189
|
+
// Falls back to `from` if every row in that direction is disabled.
|
|
190
|
+
function stepEnabled(from, dir) {
|
|
191
|
+
let i = from;
|
|
192
|
+
while (i + dir >= 0 && i + dir <= rows.length - 1) {
|
|
193
|
+
i += dir;
|
|
194
|
+
if (!rows[i]?.node.disabled)
|
|
195
|
+
return i;
|
|
196
|
+
}
|
|
197
|
+
return from;
|
|
198
|
+
}
|
|
187
199
|
function onKeydown(e) {
|
|
188
200
|
const item = rows[active];
|
|
189
201
|
if (!item)
|
|
@@ -209,11 +221,11 @@ export function createTree(config) {
|
|
|
209
221
|
switch (e.key) {
|
|
210
222
|
case 'ArrowDown':
|
|
211
223
|
e.preventDefault();
|
|
212
|
-
setActive(
|
|
224
|
+
setActive(stepEnabled(active, 1), true);
|
|
213
225
|
break;
|
|
214
226
|
case 'ArrowUp':
|
|
215
227
|
e.preventDefault();
|
|
216
|
-
setActive(
|
|
228
|
+
setActive(stepEnabled(active, -1), true);
|
|
217
229
|
break;
|
|
218
230
|
case 'ArrowRight':
|
|
219
231
|
e.preventDefault();
|
|
@@ -277,6 +289,7 @@ export function createTree(config) {
|
|
|
277
289
|
'aria-selected': row.node.id === selected(),
|
|
278
290
|
'aria-expanded': row.hasChildren ? row.open : undefined,
|
|
279
291
|
'aria-checked': ariaChecked,
|
|
292
|
+
'aria-disabled': row.node.disabled || undefined,
|
|
280
293
|
'data-row': row.index,
|
|
281
294
|
tabindex: row.index === active ? 0 : -1,
|
|
282
295
|
onclick: () => { setActive(row.index); select(row.node); },
|
package/dist/index.d.ts
CHANGED
|
@@ -137,8 +137,10 @@ export { isDisabledDay, isOutOfRange, isRestricted, isImportant, type RestrictOp
|
|
|
137
137
|
export { default as FlexRender } from './FlexRender.svelte';
|
|
138
138
|
export { renderComponent, renderSnippet } from './render-component';
|
|
139
139
|
export { default as SvGridChart } from './SvGridChart.svelte';
|
|
140
|
-
export {
|
|
141
|
-
export {
|
|
140
|
+
export { default as SvGridChartPanel } from './SvGridChartPanel.svelte';
|
|
141
|
+
export type { ChartingConfig, ChartAggregateRequest, ChartAggregateBucket } from './SvGrid.types';
|
|
142
|
+
export { buildChart, formatChartValue, rowsToChartSpec, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, type ChartType, type ChartSpec, type ChartSeries, type ChartGeometry, type ChartBar, type ChartLine, type ChartLinePoint, type ChartPieSlice, type ChartSelection, type ChartReferenceLine, type ChartRefLineGeo, type ChartRefLineGeoV, type ChartScatterDot, type ScatterPoint, type NiceScale, type SeriesOverlay, type SeriesPattern, type ChartAnnotation, type ChartHeatmapCell, type ChartFunnelSegment, type ChartRadarSeries, type ChartRadarAxis, type ChartTreemapCell, type ChartCalendarCell, type ChartGaugeLayout, type ChartSankeyNode, type ChartSankeyLink, type ChartValueFormat, type TreeNode, } from './chart';
|
|
143
|
+
export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, chartSpecToCsv, downloadChartCsv, type ChartExportOptions, } from './chart-export';
|
|
142
144
|
export { buildSparkline, toSparklineValues, type SparklineConfig, type SparklineType, type SparklineGeometry, } from './sparkline';
|
|
143
145
|
export { spreadsheetLayout, spansToMerges, type SpanColumn, type SpreadsheetActionOptions, type MergeSpec, type CellBorderSpec, type BorderSpec, } from './spreadsheet';
|
|
144
146
|
export { rowResize, type RowResizeOptions } from './row-resize';
|