@svgrid/grid 2.1.6 → 2.1.7

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 (94) hide show
  1. package/dist/SvCarousel.svelte +35 -6
  2. package/dist/SvCarousel.svelte.d.ts +3 -0
  3. package/dist/SvColorInput.svelte +0 -1
  4. package/dist/SvContextMenu.svelte +26 -6
  5. package/dist/SvDrawer.svelte +5 -3
  6. package/dist/SvGrid.controller.svelte.d.ts +85 -2
  7. package/dist/SvGrid.controller.svelte.js +494 -1
  8. package/dist/SvGrid.css +142 -0
  9. package/dist/SvGrid.svelte +49 -27
  10. package/dist/SvGrid.types.d.ts +78 -0
  11. package/dist/SvGridBoard.svelte +71 -13
  12. package/dist/SvGridChart.svelte +41 -7
  13. package/dist/SvGridChart.svelte.d.ts +4 -0
  14. package/dist/SvGridChartPanel.svelte +485 -0
  15. package/dist/SvGridChartPanel.svelte.d.ts +28 -0
  16. package/dist/SvGridDropdown.svelte +18 -2
  17. package/dist/SvGroupCell.svelte +5 -5
  18. package/dist/SvMenu.svelte +25 -2
  19. package/dist/SvMenu.svelte.d.ts +3 -0
  20. package/dist/SvMenuList.svelte +31 -9
  21. package/dist/SvMenuList.svelte.d.ts +9 -1
  22. package/dist/SvNumberInput.svelte +1 -1
  23. package/dist/SvOtpInput.svelte +3 -2
  24. package/dist/SvPopover.svelte +13 -1
  25. package/dist/SvRowGroupPanel.svelte +23 -0
  26. package/dist/SvScrollArea.svelte +13 -2
  27. package/dist/SvScrollArea.svelte.d.ts +2 -0
  28. package/dist/SvTooltip.svelte +27 -2
  29. package/dist/SvTooltip.svelte.d.ts +3 -2
  30. package/dist/SvTour.svelte +30 -5
  31. package/dist/SvTour.svelte.d.ts +3 -0
  32. package/dist/SvTree.svelte +7 -3
  33. package/dist/SvTreeSelect.svelte +16 -9
  34. package/dist/build-api.js +51 -0
  35. package/dist/cdn/svgrid.js +17394 -16052
  36. package/dist/cdn/svgrid.svelte-external.js +16784 -15644
  37. package/dist/chart-export.d.ts +21 -0
  38. package/dist/chart-export.js +52 -11
  39. package/dist/chart.d.ts +30 -0
  40. package/dist/chart.js +53 -13
  41. package/dist/createAutocomplete.svelte.d.ts +6 -0
  42. package/dist/createAutocomplete.svelte.js +10 -0
  43. package/dist/createCombobox.svelte.d.ts +1 -0
  44. package/dist/createCombobox.svelte.js +1 -0
  45. package/dist/createCountryInput.svelte.d.ts +5 -0
  46. package/dist/createCountryInput.svelte.js +11 -0
  47. package/dist/createDropdownList.svelte.d.ts +4 -0
  48. package/dist/createDropdownList.svelte.js +7 -0
  49. package/dist/createPopoverSelect.svelte.d.ts +5 -3
  50. package/dist/createPopoverSelect.svelte.js +5 -3
  51. package/dist/createTree.svelte.d.ts +1 -0
  52. package/dist/createTree.svelte.js +15 -2
  53. package/dist/index.d.ts +4 -2
  54. package/dist/index.js +3 -2
  55. package/dist/svgrid-wrapper.types.d.ts +58 -0
  56. package/package.json +1 -1
  57. package/src/SvCarousel.svelte +35 -6
  58. package/src/SvColorInput.svelte +0 -1
  59. package/src/SvContextMenu.svelte +26 -6
  60. package/src/SvDrawer.svelte +5 -3
  61. package/src/SvGrid.controller.svelte.ts +481 -0
  62. package/src/SvGrid.css +142 -0
  63. package/src/SvGrid.svelte +49 -27
  64. package/src/SvGrid.types.ts +83 -0
  65. package/src/SvGridBoard.svelte +71 -13
  66. package/src/SvGridChart.svelte +41 -7
  67. package/src/SvGridChartPanel.svelte +485 -0
  68. package/src/SvGridDropdown.svelte +18 -2
  69. package/src/SvGroupCell.svelte +5 -5
  70. package/src/SvMenu.svelte +25 -2
  71. package/src/SvMenuList.svelte +31 -9
  72. package/src/SvNumberInput.svelte +1 -1
  73. package/src/SvOtpInput.svelte +3 -2
  74. package/src/SvPopover.svelte +13 -1
  75. package/src/SvRowGroupPanel.svelte +23 -0
  76. package/src/SvScrollArea.svelte +13 -2
  77. package/src/SvTooltip.svelte +27 -2
  78. package/src/SvTour.svelte +30 -5
  79. package/src/SvTree.svelte +7 -3
  80. package/src/SvTreeSelect.svelte +16 -9
  81. package/src/build-api.ts +49 -0
  82. package/src/chart-export.ts +57 -0
  83. package/src/chart.ts +70 -13
  84. package/src/createAutocomplete.svelte.ts +11 -0
  85. package/src/createCombobox.svelte.ts +1 -0
  86. package/src/createCountryInput.svelte.ts +12 -0
  87. package/src/createDropdownList.svelte.ts +8 -0
  88. package/src/createPopoverSelect.svelte.ts +5 -3
  89. package/src/createTree.svelte.ts +15 -2
  90. package/src/index.ts +6 -0
  91. package/src/overlays.test.ts +5 -0
  92. package/src/svgrid-wrapper.types.ts +62 -0
  93. package/src/svgrid.charting.test.ts +527 -0
  94. package/src/test-setup.ts +22 -0
@@ -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;
@@ -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: fmtTick(value),
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: fmtTick(value) };
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: fmtTick(value) };
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: spec.categories[i] ?? String(i),
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 ?? fmtTick(ref.value),
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: fmtTick(value) })),
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)}%` : 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 ?? fmtTick(ref.value),
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: fmtTick(value) }));
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 ?? fmtTick(ref.value),
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
- triggerProps: () => {
67
- 'aria-haspopup': "listbox";
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
- triggerProps: () => ({
176
- 'aria-haspopup': 'listbox',
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(Math.min(active + 1, rows.length - 1), true);
224
+ setActive(stepEnabled(active, 1), true);
213
225
  break;
214
226
  case 'ArrowUp':
215
227
  e.preventDefault();
216
- setActive(Math.max(active - 1, 0), true);
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 { buildChart, 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 TreeNode, } from './chart';
141
- export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, type ChartExportOptions, } from './chart-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';