@svgrid/grid 2.1.5 → 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
@@ -54,6 +54,7 @@
54
54
  } from "./SvGrid.helpers";
55
55
  import { createSvGridController } from "./SvGrid.controller.svelte";
56
56
  import GridMenus from "./GridMenus.svelte";
57
+ import SvGridChartPanel from "./SvGridChartPanel.svelte";
57
58
  import GridFooter from "./GridFooter.svelte";
58
59
  import SvGridBoard from "./SvGridBoard.svelte";
59
60
  let props: Props<TFeatures, TData> = $props();
@@ -1303,35 +1304,52 @@
1303
1304
  </label>
1304
1305
  {/if}
1305
1306
 
1306
- {#if toolPanelEnabled}
1307
+ {#if toolPanelEnabled || ctrl.chartingEnabled}
1307
1308
  <div class="sv-grid-toolbar">
1308
- <button
1309
- type="button"
1310
- class="sv-grid-toolbar-btn"
1311
- class:is-active={ctrl.toolPanelOpen}
1312
- aria-label={ctrl.toolPanelOpen
1313
- ? "Close tool panel"
1314
- : "Open tool panel (columns & filters)"}
1315
- aria-expanded={ctrl.toolPanelOpen}
1316
- onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
1317
- >
1318
- <svg
1319
- viewBox="0 0 24 24"
1320
- width="15"
1321
- height="15"
1322
- fill="none"
1323
- stroke="currentColor"
1324
- stroke-width="2"
1325
- stroke-linecap="round"
1326
- stroke-linejoin="round"
1327
- aria-hidden="true"
1309
+ {#if toolPanelEnabled}
1310
+ <button
1311
+ type="button"
1312
+ class="sv-grid-toolbar-btn"
1313
+ class:is-active={ctrl.toolPanelOpen}
1314
+ aria-label={ctrl.toolPanelOpen
1315
+ ? "Close tool panel"
1316
+ : "Open tool panel (columns & filters)"}
1317
+ aria-expanded={ctrl.toolPanelOpen}
1318
+ onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
1319
+ >
1320
+ <svg
1321
+ viewBox="0 0 24 24"
1322
+ width="15"
1323
+ height="15"
1324
+ fill="none"
1325
+ stroke="currentColor"
1326
+ stroke-width="2"
1327
+ stroke-linecap="round"
1328
+ stroke-linejoin="round"
1329
+ aria-hidden="true"
1330
+ >
1331
+ <rect x="3" y="4" width="6" height="16" rx="1" />
1332
+ <rect x="11" y="4" width="4" height="16" rx="1" />
1333
+ <rect x="17" y="4" width="4" height="16" rx="1" />
1334
+ </svg>
1335
+ Columns &amp; Filters
1336
+ </button>
1337
+ {/if}
1338
+ {#if ctrl.chartingEnabled}
1339
+ <button
1340
+ type="button"
1341
+ class="sv-grid-toolbar-btn sv-grid-chart-toggle"
1342
+ class:is-active={ctrl.chartPanelOpen}
1343
+ aria-label={ctrl.chartPanelOpen ? "Close chart" : "Open chart"}
1344
+ aria-expanded={ctrl.chartPanelOpen}
1345
+ onclick={() => (ctrl.chartPanelOpen = !ctrl.chartPanelOpen)}
1328
1346
  >
1329
- <rect x="3" y="4" width="6" height="16" rx="1" />
1330
- <rect x="11" y="4" width="4" height="16" rx="1" />
1331
- <rect x="17" y="4" width="4" height="16" rx="1" />
1332
- </svg>
1333
- Columns &amp; Filters
1334
- </button>
1347
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1348
+ <line x1="12" y1="20" x2="12" y2="10" /><line x1="18" y1="20" x2="18" y2="4" /><line x1="6" y1="20" x2="6" y2="16" />
1349
+ </svg>
1350
+ Chart
1351
+ </button>
1352
+ {/if}
1335
1353
  </div>
1336
1354
  {/if}
1337
1355
 
@@ -2757,6 +2775,10 @@
2757
2775
  </aside>
2758
2776
  {/if}
2759
2777
  {/if}
2778
+
2779
+ {#if ctrl.chartingEnabled && ctrl.chartPanelOpen}
2780
+ <SvGridChartPanel {ctrl} />
2781
+ {/if}
2760
2782
  </div>
2761
2783
  <!-- /.sv-grid-root -->
2762
2784
 
@@ -2,6 +2,79 @@ import type { Snippet } from "svelte";
2
2
  import type { CellEditorType, ColumnDef, RowData, SvGridApi, TableFeatures } from "./index";
3
3
  import type { ConditionalFormat } from "./conditional-formatting";
4
4
  import type { MenuItem } from "./SvMenuList.svelte";
5
+ import type { ChartType, ChartSpec, ChartAnnotation, ChartReferenceLine } from "./chart";
6
+ /** One aggregated bucket returned by a server-side `getAggregate`. */
7
+ export type ChartAggregateBucket = {
8
+ category: string;
9
+ series?: string;
10
+ value: number;
11
+ };
12
+ /** The request a server-side `getAggregate` receives (whole-dataset charting). */
13
+ export type ChartAggregateRequest = {
14
+ dimension: string | undefined;
15
+ measure: string | null;
16
+ series?: string;
17
+ reduce: "sum" | "avg" | "count";
18
+ filterModel: Record<string, unknown>;
19
+ };
20
+ /**
21
+ * Config for the built-in `charting` prop. All fields optional and additive;
22
+ * `charting={true}` is the zero-config form. Column references are column
23
+ * field names / ids.
24
+ */
25
+ export type ChartingConfig<TData extends RowData = RowData> = {
26
+ /** Right-side drawer (default) or bottom dock. */
27
+ position?: "bottom" | "right";
28
+ /** Open the panel on first render. */
29
+ defaultOpen?: boolean;
30
+ /** Initial chart type. */
31
+ defaultType?: ChartType;
32
+ /** Docked-bottom height (px). */
33
+ height?: number;
34
+ /** Drawer width (px). */
35
+ width?: number;
36
+ /** Clicking a chart category filters the grid on the charted dimension. */
37
+ crossFilter?: boolean;
38
+ /** Group-by column. */
39
+ dimension?: string;
40
+ /** Split-by column: one series per distinct value. */
41
+ series?: string;
42
+ /** Measure column, or several for a multi-series chart. */
43
+ measures?: string | string[];
44
+ reduce?: "sum" | "avg" | "count";
45
+ stacked?: boolean;
46
+ stacked100?: boolean;
47
+ /** Per-series-label type override (combo). */
48
+ seriesTypes?: Record<string, "bar" | "line" | "area">;
49
+ /** Per-series-label axis override (dual axis). */
50
+ seriesAxes?: Record<string, "left" | "right">;
51
+ referenceLines?: ChartReferenceLine[];
52
+ averageLine?: boolean;
53
+ trend?: "sma" | "ema" | "linear";
54
+ annotations?: ChartAnnotation[];
55
+ yScale?: "linear" | "log";
56
+ timeAxis?: boolean;
57
+ valueFormat?: "number" | "currency" | "percent" | "compact";
58
+ smooth?: boolean;
59
+ orientation?: "vertical" | "horizontal";
60
+ donut?: number | boolean;
61
+ palette?: string[];
62
+ patternFallback?: boolean;
63
+ topN?: number;
64
+ otherLabel?: string;
65
+ sort?: "value-desc" | "value-asc" | "category" | "none";
66
+ dataLabels?: boolean;
67
+ zoom?: boolean;
68
+ brush?: boolean;
69
+ /** Show the chart's PNG/SVG export toolbar. */
70
+ export?: boolean;
71
+ /** Escape hatch: build a fully custom ChartSpec from the (scoped) rows. */
72
+ buildSpec?: (rows: TData[]) => ChartSpec | null;
73
+ /** Server-side aggregation: chart the whole dataset, not just loaded rows. */
74
+ getAggregate?: (request: ChartAggregateRequest) => Promise<ReadonlyArray<ChartAggregateBucket>>;
75
+ /** Bump to force a server-side refetch. */
76
+ refreshKey?: unknown;
77
+ };
5
78
  /**
6
79
  * The built-in card detail drawer. Set `board.drawer` (`true` for all fields,
7
80
  * or this object to customize) and opening a card shows a drawer with an
@@ -556,6 +629,11 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
556
629
  * grid; the panel docks on the right edge.
557
630
  */
558
631
  toolPanel?: boolean;
632
+ /**
633
+ * Integrated charting: `true` for the zero-config docked chart panel, or a
634
+ * {@link ChartingConfig} object to author defaults + advanced options.
635
+ */
636
+ charting?: boolean | ChartingConfig<TData>;
559
637
  /**
560
638
  * Render the header column menu (⋮) as a tabbed popover - **General**,
561
639
  * **Filter**, and **Columns** tabs (the AG-Grid layout). Defaults to `false`,
@@ -656,13 +656,8 @@
656
656
  if (dragLaneId == null) return;
657
657
  e.preventDefault();
658
658
  const ids = lanes.map((l) => l.id);
659
- const from = ids.indexOf(dragLaneId);
660
659
  const to = ids.indexOf(laneId);
661
- if (from > -1 && to > -1 && from !== to) {
662
- ids.splice(to, 0, ids.splice(from, 1)[0]!);
663
- laneOrder = ids;
664
- board.onLaneReorder?.(ids);
665
- }
660
+ if (to > -1) reorderLaneTo(dragLaneId, to);
666
661
  dragLaneId = null;
667
662
  overLaneId = null;
668
663
  }
@@ -671,6 +666,35 @@
671
666
  overLaneId = null;
672
667
  }
673
668
 
669
+ // Shared by mouse drag-drop (above) and keyboard reorder (below): moves
670
+ // `fromId` to `toIndex` in the lane order, persists it, and notifies.
671
+ // Returns the new ordered ids, or null if the move was a no-op / invalid.
672
+ function reorderLaneTo(fromId: string, toIndex: number): string[] | null {
673
+ const ids = lanes.map((l) => l.id);
674
+ const from = ids.indexOf(fromId);
675
+ if (from < 0 || toIndex < 0 || toIndex >= ids.length || from === toIndex) return null;
676
+ ids.splice(toIndex, 0, ids.splice(from, 1)[0]!);
677
+ laneOrder = ids;
678
+ board.onLaneReorder?.(ids);
679
+ return ids;
680
+ }
681
+ // Keyboard lane reorder: Ctrl/Cmd+ArrowLeft/Right shifts the focused lane
682
+ // header by one position, mirroring the drag-reorder above.
683
+ function onLaneHeaderKeydown(e: KeyboardEvent, laneId: string) {
684
+ if (!board.reorderableLanes) return;
685
+ if ((e.key !== "ArrowLeft" && e.key !== "ArrowRight") || !(e.ctrlKey || e.metaKey)) return;
686
+ e.preventDefault();
687
+ const ids = lanes.map((l) => l.id);
688
+ const from = ids.indexOf(laneId);
689
+ if (from < 0) return;
690
+ const to = from + (e.key === "ArrowRight" ? 1 : -1);
691
+ const moved = reorderLaneTo(laneId, to);
692
+ if (moved) {
693
+ const pos = moved.indexOf(laneId) + 1;
694
+ announce(`Moved ${laneTitleById(laneId)} to position ${pos} of ${moved.length}.`);
695
+ }
696
+ }
697
+
674
698
  // Runtime group-by change: the position overlay (laneOf/orderOf/laneOrder) is
675
699
  // keyed to the old field, so reset it when `groupBy` changes. Edits and
676
700
  // sub-tasks (keyed by row) are kept. Skips the initial run so a restored
@@ -874,6 +898,19 @@
874
898
  openEditor(row);
875
899
  return;
876
900
  }
901
+ // Keyboard multi-select: Ctrl/Cmd+Space toggles this card's selection,
902
+ // mirroring onCardClick's ctrl/meta-click branch. Kept on a modifier so it
903
+ // never collides with plain Space, which grabs the card for a move.
904
+ if (board.selectable && e.key === " " && (e.ctrlKey || e.metaKey) && grabbed == null) {
905
+ e.preventDefault();
906
+ const k = key(row);
907
+ selected[k] = !selected[k];
908
+ emitSelection();
909
+ announce(
910
+ `${selected[k] ? "Selected" : "Deselected"} ${cardLabel(row)}. ${selectedRows().length} selected.`,
911
+ );
912
+ return;
913
+ }
877
914
  if (e.key === " " || e.key === "Enter") {
878
915
  e.preventDefault();
879
916
  if (grabbed === row) {
@@ -1214,10 +1251,15 @@
1214
1251
  class:is-reorderable={board.reorderableLanes}
1215
1252
  class:is-lane-dropover={overLaneId === lane.id && dragLaneId !== lane.id}
1216
1253
  draggable={board.reorderableLanes ? true : undefined}
1254
+ tabindex={board.reorderableLanes ? 0 : undefined}
1255
+ role={board.reorderableLanes ? "group" : undefined}
1256
+ aria-roledescription={board.reorderableLanes ? "Reorderable lane" : undefined}
1257
+ title={board.reorderableLanes ? "Ctrl+Left/Right arrow to reorder this lane" : undefined}
1217
1258
  ondragstart={board.reorderableLanes ? (e) => onLaneHeaderDragStart(e, lane.id) : undefined}
1218
1259
  ondragover={board.reorderableLanes ? (e) => onLaneHeaderDragOver(e, lane.id) : undefined}
1219
1260
  ondrop={board.reorderableLanes ? (e) => onLaneHeaderDrop(e, lane.id) : undefined}
1220
1261
  ondragend={board.reorderableLanes ? onLaneHeaderDragEnd : undefined}
1262
+ onkeydown={board.reorderableLanes ? (e) => onLaneHeaderKeydown(e, lane.id) : undefined}
1221
1263
  oncontextmenu={board.laneMenu ? (e) => openLaneMenu(e, lane.id, cards) : undefined}
1222
1264
  >
1223
1265
  {#if board.collapsibleLanes}
@@ -1241,14 +1283,21 @@
1241
1283
  }}
1242
1284
  onblur={() => commitRename(lane.id)}
1243
1285
  />
1286
+ {:else if board.onLaneRename}
1287
+ <button
1288
+ type="button"
1289
+ class="sv-board-lane-title is-renamable"
1290
+ ondblclick={() => startRename(lane)}
1291
+ onkeydown={(e) => {
1292
+ if (e.key === "Enter" || e.key === "F2") {
1293
+ e.preventDefault();
1294
+ startRename(lane);
1295
+ }
1296
+ }}
1297
+ title="Double-click, Enter, or F2 to rename"
1298
+ >{laneTitle(lane)}</button>
1244
1299
  {:else}
1245
- <!-- svelte-ignore a11y_no_static_element_interactions -->
1246
- <span
1247
- class="sv-board-lane-title"
1248
- class:is-renamable={board.onLaneRename}
1249
- ondblclick={board.onLaneRename ? () => startRename(lane) : undefined}
1250
- title={board.onLaneRename ? "Double-click to rename" : undefined}
1251
- >{laneTitle(lane)}</span>
1300
+ <span class="sv-board-lane-title">{laneTitle(lane)}</span>
1252
1301
  {/if}
1253
1302
  <span
1254
1303
  class="sv-board-lane-count"
@@ -1630,7 +1679,16 @@
1630
1679
  color: color-mix(in srgb, var(--sg-fg, #101828) 60%, transparent);
1631
1680
  }
1632
1681
  .sv-board-lane-title.is-renamable {
1682
+ all: unset;
1633
1683
  cursor: text;
1684
+ font-weight: 600;
1685
+ font-size: 0.82rem;
1686
+ flex: 1 1 auto;
1687
+ min-width: 0;
1688
+ white-space: nowrap;
1689
+ overflow: hidden;
1690
+ text-overflow: ellipsis;
1691
+ text-align: left;
1634
1692
  }
1635
1693
  .sv-board-lane-rename {
1636
1694
  flex: 1 1 auto;
@@ -7,7 +7,7 @@
7
7
  * focus tooltips, a clickable legend that toggles series, optional data
8
8
  * labels, and an `onSelect` drill hook. No external charting dependency.
9
9
  */
10
- import { buildChart, DEFAULT_PALETTE, type ChartSpec, type ChartSelection } from './chart'
10
+ import { buildChart, DEFAULT_PALETTE, formatChartValue, type ChartSpec, type ChartSelection } from './chart'
11
11
 
12
12
  type Props = {
13
13
  spec: ChartSpec
@@ -38,6 +38,10 @@
38
38
  * export (PNG / SVG / copy). Default true when zoomable or onDrill is
39
39
  * set, otherwise false. Set to `false` to hide explicitly. */
40
40
  toolbar?: boolean
41
+ /** Explicit chart pixel size (viewBox). When set, the chart lays out to
42
+ * fit exactly - used by the docked panel to size the chart to its body. */
43
+ width?: number
44
+ height?: number
41
45
  }
42
46
  let {
43
47
  spec,
@@ -51,15 +55,19 @@
51
55
  brush = false,
52
56
  brushHeight = 88,
53
57
  toolbar,
58
+ width,
59
+ height,
54
60
  }: Props = $props()
55
61
  const showToolbar = $derived(toolbar ?? (zoomable || !!onDrill))
56
62
 
57
63
  const fmt = (v: number) =>
58
64
  formatValue
59
65
  ? formatValue(v)
60
- : Number.isFinite(v)
61
- ? v.toLocaleString(undefined, { maximumFractionDigits: 2 })
62
- : String(v)
66
+ : spec.valueFormat
67
+ ? formatChartValue(v, spec.valueFormat)
68
+ : Number.isFinite(v)
69
+ ? v.toLocaleString(undefined, { maximumFractionDigits: 2 })
70
+ : String(v)
63
71
 
64
72
  let hidden = $state(new Set<string>())
65
73
  // Isolate: double-clicking a legend chip shows ONLY that series/slice.
@@ -290,7 +298,9 @@
290
298
  // chartEl) and hand buildChart a light/dark hint so low-value cells render as
291
299
  // "cold" rather than glaring white rectangles on a dark grid.
292
300
  let isDark = $state(false)
293
- const geo = $derived(buildChart(visibleSpec, isDark ? 'dark' : 'light'))
301
+ const geo = $derived(
302
+ buildChart(width || height ? { ...visibleSpec, width, height } : visibleSpec, isDark ? 'dark' : 'light'),
303
+ )
294
304
  const isCartesian = $derived(
295
305
  spec.type !== 'pie' && spec.type !== 'heatmap' &&
296
306
  spec.type !== 'funnel' && spec.type !== 'radar' &&
@@ -727,6 +737,15 @@
727
737
  <text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 16} text-anchor="middle">{truncate(t.label, 16)}</text>
728
738
  {/if}
729
739
  {/each}
740
+ <!-- Grouped (nested) category axis: parent tier under the leaf labels. -->
741
+ {#each geo.categoryGroupTicks as g (g.label + g.x0)}
742
+ {@const gy = geo.plot.y + geo.plot.h + (geo.xLabelRotated ? 46 : 32)}
743
+ <line class="sv-grid-chart-gridline" x1={g.x0 + 3} y1={gy - 9} x2={g.x1 - 3} y2={gy - 9} />
744
+ {#if g.x0 > geo.plot.x + 1}
745
+ <line class="sv-grid-chart-gridline" x1={g.x0} y1={geo.plot.y + geo.plot.h} x2={g.x0} y2={gy - 9} opacity="0.5" />
746
+ {/if}
747
+ <text class="sv-grid-chart-axis sv-grid-chart-group-label" x={g.xCenter} y={gy} text-anchor="middle">{truncate(g.label, Math.max(4, Math.floor((g.x1 - g.x0) / 7)))}</text>
748
+ {/each}
730
749
  {#if spec.yAxisTitle}
731
750
  <text class="sv-grid-chart-axis-title" x={13} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(-90 13 ${geo.plot.y + geo.plot.h / 2})`}>{spec.yAxisTitle}</text>
732
751
  {/if}
@@ -850,6 +869,7 @@
850
869
  onmousemove={(e) => hoverCell(e.clientX, e.clientY, cell)}
851
870
  onmouseleave={clearActive}
852
871
  onclick={() => select(cell.colLabel, cell.rowLabel, cell.value)}
872
+ onkeydown={(e) => { if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(cell.colLabel, cell.rowLabel, cell.value) } }}
853
873
  />
854
874
  {#if dataLabels && cell.w > 22 && cell.h > 14}
855
875
  <text class="sv-grid-chart-heatlabel" x={cell.x + cell.w / 2} y={cell.y + cell.h / 2 + 3} text-anchor="middle" fill={cell.textColor}>{fmt(cell.value)}</text>
@@ -887,6 +907,7 @@
887
907
  onmousemove={(e) => hoverFunnel(e.clientX, e.clientY, seg)}
888
908
  onmouseleave={clearActive}
889
909
  onclick={() => select(seg.label, seg.label, seg.value, fi)}
910
+ onkeydown={(e) => { if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(seg.label, seg.label, seg.value, fi) } }}
890
911
  />
891
912
  <text class="sv-grid-chart-funnel-label" x={seg.cx} y={seg.cy - 3} text-anchor="middle" fill={seg.textColor}>{truncate(seg.label, 24)}</text>
892
913
  <text class="sv-grid-chart-funnel-value" x={seg.cx} y={seg.cy + 11} text-anchor="middle" fill={seg.textColor}>
@@ -997,15 +1018,18 @@
997
1018
 
998
1019
  {#if isTreemap}
999
1020
  {#each geo.treemapCells as cell, ti (ti)}
1000
- <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events -->
1021
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
1001
1022
  <rect
1002
1023
  class="sv-grid-chart-treemap-cell"
1003
1024
  x={cell.x} y={cell.y} width={cell.w} height={cell.h}
1004
1025
  fill={cell.color}
1026
+ role={onSelect ? 'button' : 'presentation'}
1027
+ tabindex={onSelect ? 0 : undefined}
1005
1028
  aria-label={`${cell.name}: ${fmt(cell.value)}`}
1006
1029
  onmousemove={(e) => showTip(e.clientX, e.clientY, cell.name, [{ color: cell.color, value: fmt(cell.value) }])}
1007
1030
  onmouseleave={clearActive}
1008
1031
  onclick={() => select(cell.name, '', cell.value)}
1032
+ onkeydown={(e) => { if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(cell.name, '', cell.value) } }}
1009
1033
  />
1010
1034
  {#if cell.w > 36 && cell.h > 18}
1011
1035
  <text class="sv-grid-chart-treemap-label" x={cell.x + 4} y={cell.y + 12} fill={cell.textColor}>{truncate(cell.name, Math.max(3, Math.floor(cell.w / 7)))}</text>
@@ -1034,17 +1058,23 @@
1034
1058
  {/each}
1035
1059
  <!-- Node rectangles + labels. -->
1036
1060
  {#each geo.sankeyNodes as node, ni (ni)}
1037
- <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events -->
1061
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
1038
1062
  <rect
1039
1063
  class="sv-grid-chart-sankey-node"
1040
1064
  x={node.x} y={node.y} width={node.w} height={node.h}
1041
1065
  fill={node.color}
1066
+ tabindex={0}
1042
1067
  aria-label={`${node.label}: in ${fmt(node.totalIn)}, out ${fmt(node.totalOut)}`}
1043
1068
  onmousemove={(e) => showTip(e.clientX, e.clientY, node.label, [
1044
1069
  { label: 'in', value: fmt(node.totalIn) },
1045
1070
  { label: 'out', value: fmt(node.totalOut) },
1046
1071
  ])}
1047
1072
  onmouseleave={clearActive}
1073
+ onfocus={(e) => { const b = e.currentTarget.getBoundingClientRect(); showTip(b.left + b.width / 2, b.top, node.label, [
1074
+ { label: 'in', value: fmt(node.totalIn) },
1075
+ { label: 'out', value: fmt(node.totalOut) },
1076
+ ]) }}
1077
+ onblur={clearActive}
1048
1078
  />
1049
1079
  <text class="sv-grid-chart-sankey-label"
1050
1080
  x={node.column === 0 ? node.x + node.w + 4 : node.x - 4}
@@ -1270,6 +1300,10 @@
1270
1300
  font-weight: 600;
1271
1301
  font-family: inherit;
1272
1302
  }
1303
+ .sv-grid-chart-group-label {
1304
+ fill: var(--sg-fg, #0f172a);
1305
+ font-weight: 600;
1306
+ }
1273
1307
  .sv-grid-chart-crosshair {
1274
1308
  stroke: var(--sg-accent, #2563eb);
1275
1309
  stroke-width: 1;
@@ -36,6 +36,10 @@ type Props = {
36
36
  * export (PNG / SVG / copy). Default true when zoomable or onDrill is
37
37
  * set, otherwise false. Set to `false` to hide explicitly. */
38
38
  toolbar?: boolean;
39
+ /** Explicit chart pixel size (viewBox). When set, the chart lays out to
40
+ * fit exactly - used by the docked panel to size the chart to its body. */
41
+ width?: number;
42
+ height?: number;
39
43
  };
40
44
  declare const SvGridChart: import("svelte").Component<Props, {}, "">;
41
45
  type SvGridChart = ReturnType<typeof SvGridChart>;