@vanduo-oss/vd3-cbun 1.3.2 → 1.4.1

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 (40) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +24 -9
  3. package/SKILL.md +42 -15
  4. package/dist/charts/core.d.ts +4 -0
  5. package/dist/charts/index.cjs +197 -10
  6. package/dist/charts/index.cjs.map +3 -3
  7. package/dist/charts/index.js +197 -10
  8. package/dist/charts/index.js.map +3 -3
  9. package/dist/charts/vd3-charts.css +82 -0
  10. package/dist/charts/vue.d.ts +4 -0
  11. package/dist/code-editor/core.d.ts +19 -2
  12. package/dist/code-editor/highlight.cjs +1242 -0
  13. package/dist/code-editor/highlight.cjs.map +7 -0
  14. package/dist/code-editor/highlight.d.ts +6 -0
  15. package/dist/code-editor/highlight.js +1219 -0
  16. package/dist/code-editor/highlight.js.map +7 -0
  17. package/dist/code-editor/index.cjs +694 -70
  18. package/dist/code-editor/index.cjs.map +4 -4
  19. package/dist/code-editor/index.d.ts +2 -0
  20. package/dist/code-editor/index.js +694 -70
  21. package/dist/code-editor/index.js.map +4 -4
  22. package/dist/code-editor/vd3-code-editor.css +5 -0
  23. package/dist/draw/core.d.ts +7 -1
  24. package/dist/draw/index.cjs +470 -52
  25. package/dist/draw/index.cjs.map +2 -2
  26. package/dist/draw/index.js +470 -52
  27. package/dist/draw/index.js.map +2 -2
  28. package/dist/draw/vd3-draw.css +45 -0
  29. package/dist/draw/vue.d.ts +4 -0
  30. package/dist/hex-grid/core.d.ts +41 -0
  31. package/dist/hex-grid/index.cjs +308 -13
  32. package/dist/hex-grid/index.cjs.map +3 -3
  33. package/dist/hex-grid/index.d.ts +1 -0
  34. package/dist/hex-grid/index.js +308 -13
  35. package/dist/hex-grid/index.js.map +3 -3
  36. package/dist/hex-grid/vue.d.ts +4 -0
  37. package/dist/index.js +2 -2
  38. package/dist/index.js.map +2 -2
  39. package/dist/meta.json +177 -66
  40. package/package.json +15 -10
package/CHANGELOG.md CHANGED
@@ -4,6 +4,89 @@ All notable changes to `@vanduo-oss/vd3-cbun` are documented here. This file
4
4
  tracks the PACKAGE only — never docs-site content. Per-component VERSION
5
5
  constants are listed alongside each bundle release.
6
6
 
7
+ ## 1.4.1 — 2026-09-12
8
+
9
+ Patch release: hex-grid adaptive rendering, draw vector polish, charts
10
+ accessibility, and a code-editor wrap-mode caret fix. Additive and
11
+ backward-compatible; only `VD_HEX_VERSION` takes a minor (`1.0.1 → 1.1.0`).
12
+ `VD_CHARTS_VERSION`, `VD_CODE_EDITOR_VERSION`, and `VD_DRAW_VERSION` stay
13
+ `1.1.0` (no serialization change).
14
+
15
+ ### Changed
16
+
17
+ - **hex-grid** — adaptive, DPR-aware, culled rendering. New `pixelRatio`
18
+ (`number | 'auto'`, default `'auto'` = `min(devicePixelRatio, 2)`) sizes the
19
+ canvas backing store to `CSS size × ratio` with a seeded
20
+ `setTransform(ratio, …)` so world/CSS coordinates and hit-testing are
21
+ unchanged (`pixelRatio: 1` restores the old buffer). New `cull` (default
22
+ `true`) draws only cells intersecting the viewport via a spatial bucket index
23
+ rebuilt on grid regeneration; with culling on, `setCustomRender` now fires
24
+ only for visible cells (`cull: false` restores whole-grid iteration). Pan and
25
+ zoom rAF-coalesce to one frame; above 8000 visible cells a gesture blits the
26
+ last sharp frame and a sharp culled re-render follows when idle (~120 ms) or
27
+ on pointer up. Adds `getVisibleHexes()`, `getRenderStats()`,
28
+ `setPixelRatio()`, `setCull()`; `destroy()` now also cancels pending
29
+ rAF/timeouts and releases the frame canvas. Vue wrapper gains `pixelRatio` /
30
+ `cull` props. Takes `VD_HEX_VERSION` `1.0.1 → 1.1.0`.
31
+
32
+ - **draw** — vector polish without a schema bump. Freehand input is EMA-smoothed
33
+ (`smoothPoint`) and progressively simplified (Ramer–Douglas–Peucker) so
34
+ strokes stay smooth without point bloat. SVG nodes are cached and dirtied
35
+ per-shape; renders rAF-batch. Two-finger pinch-to-zoom (midpoint-anchored)
36
+ and two-finger pan land for touch. Text wraps to the shape width via SVG
37
+ `<tspan>`s and is inline-editable on double-click in select mode; arrow
38
+ marker heads follow the stroke color (sanitized `<defs>` ids, including
39
+ `toSVG()`). Tool-aware canvas cursors, panning grab state, and compass
40
+ resize handles. O(1) `_shapesById` lookup. Vue option props (`readonly`,
41
+ `snap`, `history`, `historyLimit`) update through surgical setters
42
+ (`setReadonly`, `setSnap`, `setHistoryEnabled`, `setHistoryLimit`) instead
43
+ of destroying the editor. `fitView(padding?)` takes an optional padding
44
+ (default `40`). `VD_DRAW_VERSION` stays `1.1.0`.
45
+
46
+ - **charts** — WAI-ARIA Graphics Module 1.0 on the SVG shell
47
+ (`role="graphics-document document"`, `graphics-object` / `graphics-symbol`
48
+ on marks, contextual `aria-roledescription`). Arrow-key navigation moves
49
+ focus between interactive marks with a roving tooltip. An accessible HTML
50
+ data table ships by default as `sr-only` (WCAG 1.1.1 / G73–G74);
51
+ `dataTable: false` suppresses it and `dataTable: 'visible'` shows it.
52
+ `ariaRoleDescription` overrides the SVG role description (default
53
+ `` `${kind} chart` ``). Dark-mode and `:focus-visible` chrome. Vue wrapper
54
+ forwards `dataTable` and `ariaRoleDescription`. `VD_CHARTS_VERSION` stays
55
+ `1.1.0`.
56
+
57
+ - **code-editor** — wrap mode reserves a shared `scrollbar-gutter: stable` on
58
+ both the textarea and the highlight `<pre>` so a visible scrollbar no longer
59
+ shrinks only the editable layer and misaligns the caret on wrapped lines.
60
+ OpenSpec synced; standalone core / editing / tokenizer / Vue / Playwright
61
+ suites added. `VD_CODE_EDITOR_VERSION` stays `1.1.0`.
62
+
63
+ - OpenSpec: archived `sync-shipped-cbun-specs`, `hex-grid-render-perf`, and
64
+ `draw-vector-polish-perf` — Catmull-Rom `line.smooth`, `VdDraw` template-ref
65
+ CRUD, flowchart `--vd-bg-*` chrome, corrected CI `test:types`-after-build
66
+ order, and real Purpose lines on all seven capabilities. No additional
67
+ component API or serialization change beyond the bullets above.
68
+
69
+ Component versions in this release: **charts 1.1.0, code-editor 1.1.0, draw
70
+ 1.1.0, flowchart 1.2.0, hex-grid 1.1.0, music-player 1.0.1**.
71
+
72
+ ## 1.4.0 — 2026-08-16
73
+
74
+ **code-editor** — snippet-safe first-party highlighter so docs snippets can drop
75
+ highlight.js. `highlight(source, lang, { trailingNewline? })` defaults
76
+ `trailingNewline` to `false` (no extra `\n` inside `<pre>`); pass `true` only
77
+ for the textarea overlay line-count. New tokenizer-only subpath
78
+ `@vanduo-oss/vd3-cbun/code-editor/highlight` exports `highlight`, `tokenize`,
79
+ `renderTokensToHtml`, and `LANGUAGES` without evaluating the editor core or Vue
80
+ wrapper. `vue` is a real SFC tokenizer (HTML `<script>` / `<style>` also embed
81
+ JS/TS/CSS). JS/TS emit `${}` interpolations and `regex` tokens; CSS highlights
82
+ selectors, pseudos, and `--vd-*` custom properties and no longer marks `a` in
83
+ `.a:hover` as a property. `jsx` / `tsx` / `jsonc` stay JS / TS / JSON aliases
84
+ (not dedicated grammars). The editor DOM overlay path is unchanged. Takes
85
+ `VD_CODE_EDITOR_VERSION` `1.0.1 → 1.1.0`.
86
+
87
+ Component versions in this release: **charts 1.1.0, code-editor 1.1.0, draw
88
+ 1.1.0, flowchart 1.2.0, hex-grid 1.0.1, music-player 1.0.1**.
89
+
7
90
  ## 1.3.2 — 2026-08-13
8
91
 
9
92
  **draw** — line shapes accept an optional `smooth` boolean; when true and there
package/README.md CHANGED
@@ -8,13 +8,12 @@ tree-shakeable subpath exports. It replaces the four per-repo publishes
8
8
  (`@vanduo-oss/charts`, `@vanduo-oss/flowchart`, `@vanduo-oss/hex-grid`,
9
9
  `@vanduo-oss/music-player`) of the old line.
10
10
 
11
- **Status: 1.3.2.** Six Vue3-only component families — charts, code-editor, draw,
11
+ **Status: 1.4.1.** Six Vue3-only component families — charts, code-editor, draw,
12
12
  flowchart, hex-grid, music-player — each on its own tree-shakeable subpath, with
13
- the esbuild metafile isolation guard in place. (1.3.0 adds chart mark-click
14
- emits + bounded draw/flowchart deserialization; 1.3.1 dark-aware flowchart
15
- chrome, icon toolbar, and short-parent sizing; 1.3.2 Catmull-Rom smooth lines
16
- and Vue shape CRUD on `VdDraw`; see the
17
- [changelog](./CHANGELOG.md).)
13
+ the esbuild metafile isolation guard in place. (1.4.1 adds hex-grid DPR/culling,
14
+ draw vector polish and multi-touch, charts WAI-ARIA + keyboard + data table, and
15
+ a wrap-mode scrollbar guard; 1.4.0 the snippet-safe `./code-editor/highlight`
16
+ tokenizer; see the [changelog](./CHANGELOG.md).)
18
17
 
19
18
  ## Install
20
19
 
@@ -36,6 +35,7 @@ import { VdChart } from '@vanduo-oss/vd3-cbun/charts';
36
35
  import '@vanduo-oss/vd3-cbun/charts/css';
37
36
 
38
37
  import { VdCodeEditor } from '@vanduo-oss/vd3-cbun/code-editor';
38
+ import { highlight, tokenize, LANGUAGES } from '@vanduo-oss/vd3-cbun/code-editor/highlight';
39
39
  import '@vanduo-oss/vd3-cbun/code-editor/css';
40
40
 
41
41
  import { VdDraw } from '@vanduo-oss/vd3-cbun/draw';
@@ -51,6 +51,20 @@ import { VdMusicPlayer } from '@vanduo-oss/vd3-cbun/music-player';
51
51
  import '@vanduo-oss/vd3-cbun/music-player/css';
52
52
  ```
53
53
 
54
+ `VdHexGrid` / `VdHexGridCore` accept `pixelRatio` (`number | 'auto'`, default
55
+ `'auto'` = `min(devicePixelRatio, 2)`) and `cull` (default `true`, viewport
56
+ culling). Large grids coalesce pan/zoom into one frame and blit the last sharp
57
+ frame while gesturing, then re-render sharply when idle. `getVisibleHexes()` and
58
+ `getRenderStats()` expose the culled/render state; `setPixelRatio()` and
59
+ `setCull()` update them in place. `pixelRatio: 1` and `cull: false` restore the
60
+ pre-1.1 buffer size and whole-grid iteration.
61
+
62
+ `highlight(source, lang, { trailingNewline? })` defaults `trailingNewline` to
63
+ `false` (snippet-safe inside `<pre>`). `./code-editor/highlight` does not load
64
+ the editor or Vue wrapper. First-party languages include `vue` (SFC);
65
+ `jsx` / `tsx` / `jsonc` are aliases that fall back to the JS / TS / JSON
66
+ tokenizers — they are not dedicated grammars.
67
+
54
68
  The root import exposes only the per-component VERSION-constants map:
55
69
 
56
70
  ```js
@@ -84,8 +98,8 @@ Notes:
84
98
  The bundle's own semver (`package.json` version) is decoupled from the
85
99
  components inside it. Each component carries a VERSION constant tracked in
86
100
  `component-versions.json` at the repo root and asserted in tests against the
87
- exported `VD3_CBUN_VERSIONS` map. charts, code-editor, hex-grid, and music-player
88
- sit at `1.0.1`, draw at `1.1.0`; **flowchart continues its
101
+ exported `VD3_CBUN_VERSIONS` map. code-editor sits at `1.1.0`; charts at
102
+ `1.1.0`; hex-grid at `1.1.0`; music-player at `1.0.1`; draw at `1.1.0`; **flowchart continues its
89
103
  old-line lineage at `1.2.0`** because `VD_FLOWCHART_VERSION` is load-bearing —
90
104
  it is serialized into user documents via `toJSON().version`. A new component
91
105
  enters the manifest at `1.0.0` (`draw` did, in bundle 1.2.0).
@@ -132,7 +146,8 @@ tokens works — but it is the canonical source.)
132
146
  | `@vanduo-oss/vd3-cbun` | Root — the `VD3_CBUN_VERSIONS` per-component version map |
133
147
  | `@vanduo-oss/vd3-cbun/charts` | `VdChart` + typed chart wrappers, factories, scales, `resolveTheme` |
134
148
  | `@vanduo-oss/vd3-cbun/charts/css` | Charts stylesheet (`dist/charts/vd3-charts.css`) |
135
- | `@vanduo-oss/vd3-cbun/code-editor` | `VdCodeEditor` + `VdCodeEditorCore`, `tokenize`, `highlight`, `LANGUAGES` |
149
+ | `@vanduo-oss/vd3-cbun/code-editor` | `VdCodeEditor` + `VdCodeEditorCore`, `tokenize`, `highlight`, `renderTokensToHtml`, `LANGUAGES` |
150
+ | `@vanduo-oss/vd3-cbun/code-editor/highlight` | Tokenizer-only: `highlight`, `tokenize`, `renderTokensToHtml`, `LANGUAGES` (no editor / Vue) |
136
151
  | `@vanduo-oss/vd3-cbun/code-editor/css` | Code-editor stylesheet (`dist/code-editor/vd3-code-editor.css`) |
137
152
  | `@vanduo-oss/vd3-cbun/draw` | `VdDraw` + `VdDrawCore`, `BRUSH_PRESETS`, `DRAW_TOOLS`, `VD_DRAW_VERSION` |
138
153
  | `@vanduo-oss/vd3-cbun/draw/css` | Draw stylesheet (`dist/draw/vd3-draw.css`) |
package/SKILL.md CHANGED
@@ -43,30 +43,47 @@ the scales (`scaleLinear`, `scaleTime`, `scaleBand`, `scalePoint`,
43
43
  `scaleOrdinal`), accessor/tick helpers (`createAccessor`, `ticks`,
44
44
  `niceDomain`), the path builders (`linePath`, `areaPath`, `arcPath`), and
45
45
  `resolveTheme`. No name collision, so the core factories keep their own names.
46
- CSS ships at `@vanduo-oss/vd3-cbun/charts/css`. `VD_CHARTS_VERSION` is `1.0.1`.
46
+ CSS ships at `@vanduo-oss/vd3-cbun/charts/css`. Charts expose WAI-ARIA Graphics
47
+ roles, arrow-key mark navigation, and an accessible data table (`dataTable`:
48
+ `true` / `false` / `'visible'`; `ariaRoleDescription` overrides the SVG
49
+ description). `VD_CHARTS_VERSION` is `1.1.0`.
47
50
 
48
51
  ## Code-editor
49
52
 
50
53
  ```js
51
54
  import { VdCodeEditor, VdCodeEditorCore } from '@vanduo-oss/vd3-cbun/code-editor';
55
+ import { highlight, tokenize, LANGUAGES } from '@vanduo-oss/vd3-cbun/code-editor/highlight';
52
56
  import '@vanduo-oss/vd3-cbun/code-editor/css';
57
+
58
+ const html = highlight(source, 'vue'); // snippet-safe; no extra trailing newline
59
+ const overlay = highlight(source, 'javascript', { trailingNewline: true });
53
60
  ```
54
61
 
55
62
  `VdCodeEditor` is the Vue 3 wrapper (`v-model` binds the contents); the
56
63
  framework-agnostic editor core shares the class name, so it is re-exported as
57
64
  `VdCodeEditorCore`. It is a lightweight, first-party **textarea-overlay** editor
58
65
  — a native `<textarea>` over a `<pre>` highlight layer painted by homegrown,
59
- ReDoS-safe tokenizers (JavaScript/TypeScript, HTML, CSS, JSON, Markdown, Shell,
60
- Python) — with a line-number gutter + active-line highlight, auto-indent and
61
- bracket/quote auto-close, read-only mode, a copy button, a placeholder, and a
62
- large-input performance guard. No third-party editor and no runtime dependency
63
- beyond `vue`; rendering is escaping-safe and `innerHTML`-free, and the wrapper is
64
- SSR-safe (DOM touched only on mount). The subpath also re-exports the pure
65
- `tokenize`, `highlight`, and `LANGUAGES` helpers plus `VD_CODE_EDITOR_VERSION`.
66
- Key props: `language`, `readOnly`, `lineNumbers`, `tabSize`, `wrap`,
67
- `placeholder`, `maxLength`; events: `update:modelValue`, `change`, `focus`,
68
- `blur`, `ready`. CSS ships at `@vanduo-oss/vd3-cbun/code-editor/css`.
69
- `VD_CODE_EDITOR_VERSION` is `1.0.0`.
66
+ ReDoS-safe tokenizers (JavaScript/TypeScript, HTML, Vue SFC, CSS, JSON,
67
+ Markdown, Shell, Python) — with a line-number gutter + active-line highlight,
68
+ auto-indent and bracket/quote auto-close, read-only mode, a copy button, a
69
+ placeholder, and a large-input performance guard. No third-party editor and no
70
+ runtime dependency beyond `vue`; rendering is escaping-safe and `innerHTML`-free,
71
+ and the wrapper is SSR-safe (DOM touched only on mount).
72
+
73
+ `./code-editor/highlight` is the **tokenizer-only** entry (`highlight`,
74
+ `tokenize`, `renderTokensToHtml`, `LANGUAGES`) — it does not evaluate the editor
75
+ core or Vue wrapper. `highlight(source, lang, { trailingNewline? })` defaults
76
+ `trailingNewline` to `false` (safe inside `<pre>`); pass `true` only for the
77
+ textarea overlay line-count. `jsx` / `tsx` / `jsonc` are aliases that fall back
78
+ to the JS / TS / JSON tokenizers — they are not dedicated grammars. `vue` is a
79
+ real SFC tokenizer (`<script>` / `<style>` embed, `{{ }}`, `v-*` / `@` / `:`).
80
+
81
+ The main `./code-editor` subpath also re-exports those helpers plus
82
+ `VD_CODE_EDITOR_VERSION`. Key props: `language`, `readOnly`, `lineNumbers`,
83
+ `tabSize`, `wrap`, `placeholder`, `maxLength`; events: `update:modelValue`,
84
+ `change`, `focus`, `blur`, `ready`. Wrap mode shares `scrollbar-gutter: stable`
85
+ on the textarea and highlight layer so the caret stays aligned. CSS ships at
86
+ `@vanduo-oss/vd3-cbun/code-editor/css`. `VD_CODE_EDITOR_VERSION` is `1.1.0`.
70
87
 
71
88
  ## Draw
72
89
 
@@ -93,8 +110,12 @@ carry `brush` / `color` / `opacity` / per-point `pressure`) via a
93
110
  also re-exports `BRUSH_PRESETS`, `DRAW_TOOLS`, `DRAW_SHAPE_TYPES`, and
94
111
  `VD_DRAW_VERSION`. Key props: `data`, `tool`, `readonly`, `gridSize`, `snap`,
95
112
  `autoFit`, `history`, `historyLimit`; events: `change`, `select`, `viewport`,
96
- `ready`. CSS ships at `@vanduo-oss/vd3-cbun/draw/css`. `VD_DRAW_VERSION` is
97
- `1.1.0`.
113
+ `ready`. Live EMA brush smoothing + RDP simplification, incremental SVG cache,
114
+ two-finger pinch/pan, multi-line text wrap, double-click text edit, and surgical
115
+ Vue setters (`setReadonly` / `setSnap` / `setHistoryEnabled` /
116
+ `setHistoryLimit`) so option props do not recreate the editor.
117
+ `fitView(padding?)` defaults padding to `40`. CSS ships at
118
+ `@vanduo-oss/vd3-cbun/draw/css`. `VD_DRAW_VERSION` is `1.1.0`.
98
119
 
99
120
  ## Flowchart
100
121
 
@@ -136,7 +157,13 @@ subpath (coordinate transforms, rotation, distance, plus the terrain tables:
136
157
  and the `isPassable` / `getMovementCost` / `getTerrainYields` / `getTerrainColor`
137
158
  helpers). Hex-grid ships **no CSS** — it renders to a canvas and reads `--vd-*`
138
159
  theme tokens off the host element via `getComputedStyle`. `VD_HEX_VERSION` is
139
- `1.0.1`.
160
+ `1.1.0`. The core accepts `pixelRatio` (`number | 'auto'`, default `'auto'` =
161
+ `min(devicePixelRatio, 2)`) and `cull` (default `true`, viewport culling).
162
+ Pan/zoom gestures rAF-coalesce to one frame and, above ~8000 visible cells,
163
+ blit the last sharp frame before a sharp culled re-render when idle; data APIs
164
+ stay synchronous. `getVisibleHexes()`, `getRenderStats()`, `setPixelRatio()`,
165
+ and `setCull()` expose/update this state; `pixelRatio: 1` and `cull: false`
166
+ restore the pre-1.1 behavior.
140
167
 
141
168
  ## Music-player
142
169
 
@@ -105,6 +105,10 @@ export interface BaseChartOptions<T = Row> {
105
105
  legend?: boolean | LegendOptions;
106
106
  /** Draw value labels on each mark. */
107
107
  dataLabels?: boolean | DataLabelsOption;
108
+ /** Accessible data table fallback for screen readers (WCAG 1.1.1). Set `false` to disable or `'visible'` to display visually. Default is `true` (visually hidden). */
109
+ dataTable?: boolean | 'visible';
110
+ /** Custom ARIA role description override for the chart root (defaults to `${kind} chart`). */
111
+ ariaRoleDescription?: string;
108
112
  }
109
113
 
110
114
  export interface CartesianChartOptions<T = Row> extends BaseChartOptions<T> {
@@ -495,6 +495,74 @@ function makeInteractive(mark, hasClick, hasTooltip) {
495
495
  mark.setAttribute("focusable", "true");
496
496
  }
497
497
  }
498
+ function setupKeyboardNavigation(svg, markGroup) {
499
+ svg.addEventListener("keydown", (event) => {
500
+ const active = document.activeElement;
501
+ if (!active || !markGroup.contains(active)) return;
502
+ const marks = Array.from(markGroup.querySelectorAll('[tabindex="0"]'));
503
+ if (!marks.length) return;
504
+ const currentIndex = marks.indexOf(active);
505
+ if (currentIndex === -1) return;
506
+ let targetIndex = -1;
507
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
508
+ event.preventDefault();
509
+ targetIndex = (currentIndex + 1) % marks.length;
510
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
511
+ event.preventDefault();
512
+ targetIndex = (currentIndex - 1 + marks.length) % marks.length;
513
+ } else if (event.key === "Home") {
514
+ event.preventDefault();
515
+ targetIndex = 0;
516
+ } else if (event.key === "End") {
517
+ event.preventDefault();
518
+ targetIndex = marks.length - 1;
519
+ } else if (event.key === "Escape") {
520
+ event.preventDefault();
521
+ active.blur();
522
+ }
523
+ if (targetIndex !== -1 && marks[targetIndex]) {
524
+ marks[targetIndex].focus();
525
+ }
526
+ });
527
+ }
528
+ function renderDataTable(instance, tableData) {
529
+ const { target, options } = instance;
530
+ if (options.dataTable === false) return;
531
+ if (!tableData || !tableData.rows || !tableData.rows.length) return;
532
+ const table = document.createElement("table");
533
+ table.className = "vd-chart-data-table";
534
+ if (options.dataTable !== "visible") {
535
+ table.classList.add("vd-chart-sr-only");
536
+ }
537
+ const caption = document.createElement("caption");
538
+ caption.textContent = tableData.caption || options.title || options.ariaLabel || `${instance.kind} chart data`;
539
+ table.appendChild(caption);
540
+ if (tableData.headers && tableData.headers.length) {
541
+ const thead = document.createElement("thead");
542
+ const headerRow = document.createElement("tr");
543
+ tableData.headers.forEach((h2) => {
544
+ const th = document.createElement("th");
545
+ th.setAttribute("scope", "col");
546
+ th.textContent = String(h2);
547
+ headerRow.appendChild(th);
548
+ });
549
+ thead.appendChild(headerRow);
550
+ table.appendChild(thead);
551
+ }
552
+ const tbody = document.createElement("tbody");
553
+ tableData.rows.forEach((row) => {
554
+ const tr = document.createElement("tr");
555
+ row.forEach((cell, idx) => {
556
+ const el = idx === 0 ? document.createElement("th") : document.createElement("td");
557
+ if (idx === 0) el.setAttribute("scope", "row");
558
+ el.textContent = cell == null ? "" : String(cell);
559
+ tr.appendChild(el);
560
+ });
561
+ tbody.appendChild(tr);
562
+ });
563
+ table.appendChild(tbody);
564
+ target.appendChild(table);
565
+ }
498
566
  function createSvgShell(instance) {
499
567
  const { target, options } = instance;
500
568
  const size = measureTarget(target, options);
@@ -511,7 +579,8 @@ function createSvgShell(instance) {
511
579
  width: size.width,
512
580
  height: size.height,
513
581
  viewBox: `0 0 ${size.width} ${size.height}`,
514
- role: "img"
582
+ role: options.role || "graphics-document document",
583
+ "aria-roledescription": options.ariaRoleDescription || `${instance.kind} chart`
515
584
  });
516
585
  const titleId = nextId("vd-chart-title");
517
586
  const descId = nextId("vd-chart-desc");
@@ -582,7 +651,7 @@ function drawAxisLine(svg, x1, y1, x2, y2, theme) {
582
651
  }
583
652
  function drawCartesianAxes(svg, config) {
584
653
  const { plot, xScale, yScale, xTicks, yTicks, theme, options, categoricalX } = config;
585
- const axisGroup = append(svg, svgEl("g", { class: "vd-chart-axes" }));
654
+ const axisGroup = append(svg, svgEl("g", { class: "vd-chart-axes", "aria-hidden": "true" }));
586
655
  yTicks.forEach((tick) => {
587
656
  const y = yScale(tick);
588
657
  if (!isFiniteNumber(y)) return;
@@ -825,7 +894,14 @@ function drawDataLabel(svg, x, y, value, cfg, theme, anchor = "middle") {
825
894
  function drawAnnotations(svg, options, plot, xScale, yScale, theme) {
826
895
  const annotations = toArray(options.annotations);
827
896
  if (!annotations.length) return;
828
- const group = append(svg, svgEl("g", { class: "vd-chart-annotations" }));
897
+ const group = append(
898
+ svg,
899
+ svgEl("g", {
900
+ class: "vd-chart-annotations",
901
+ role: "graphics-object",
902
+ "aria-roledescription": "annotations"
903
+ })
904
+ );
829
905
  annotations.forEach((ann) => {
830
906
  const color = ann.color || theme.mutedTextColor;
831
907
  const dash = ann.dash === false ? null : "4 3";
@@ -945,7 +1021,14 @@ function renderBarChart(instance) {
945
1021
  });
946
1022
  drawAnnotations(svg, options, plot, xScale, yScale, theme);
947
1023
  const labels = dataLabelConfig(options);
948
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-bars" }));
1024
+ const markGroup = append(
1025
+ svg,
1026
+ svgEl("g", {
1027
+ class: "vd-chart-marks vd-chart-bars",
1028
+ role: "graphics-object",
1029
+ "aria-roledescription": "data points"
1030
+ })
1031
+ );
949
1032
  const baseline = yScale(Math.min(Math.max(0, yDomain[0]), yDomain[1]));
950
1033
  rows.forEach((row) => {
951
1034
  const x = xScale(row.x);
@@ -963,6 +1046,7 @@ function renderBarChart(instance) {
963
1046
  rx: 3,
964
1047
  fill,
965
1048
  role: "graphics-symbol",
1049
+ "aria-roledescription": "bar",
966
1050
  "aria-label": `${formatCategory(row.x)}: ${formatNumber(row.y)}`
967
1051
  });
968
1052
  makeInteractive(rect, typeof options.onBarClick === "function", options.tooltip !== false);
@@ -984,6 +1068,12 @@ function renderBarChart(instance) {
984
1068
  if (labels) drawDataLabel(svg, x + xScale.bandwidth() / 2, rectY - 4, row.y, labels, theme);
985
1069
  append(markGroup, rect);
986
1070
  });
1071
+ setupKeyboardNavigation(svg, markGroup);
1072
+ renderDataTable(instance, {
1073
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1074
+ headers: [options.xAxis?.label || "Category", options.yAxis?.label || "Value"],
1075
+ rows: rows.map((r) => [formatCategory(r.x), formatNumber(r.y)])
1076
+ });
987
1077
  if (options.legend && color && color.scale) {
988
1078
  renderTopLegend(
989
1079
  svg,
@@ -1037,7 +1127,14 @@ function renderMultiBarChart(instance) {
1037
1127
  });
1038
1128
  drawAnnotations(svg, options, plot, xScale, yScale, theme);
1039
1129
  const labels = dataLabelConfig(options);
1040
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-bars" }));
1130
+ const markGroup = append(
1131
+ svg,
1132
+ svgEl("g", {
1133
+ class: "vd-chart-marks vd-chart-bars",
1134
+ role: "graphics-object",
1135
+ "aria-roledescription": "grouped bars"
1136
+ })
1137
+ );
1041
1138
  const baseline = yScale(Math.min(Math.max(0, yDomain[0]), yDomain[1]));
1042
1139
  seriesList.forEach((series) => {
1043
1140
  const fill = seriesColor(series, theme);
@@ -1057,6 +1154,7 @@ function renderMultiBarChart(instance) {
1057
1154
  rx: 3,
1058
1155
  fill,
1059
1156
  role: "graphics-symbol",
1157
+ "aria-roledescription": "bar",
1060
1158
  "aria-label": `${series.name} \u2014 ${formatCategory(row.x)}: ${formatNumber(row.y)}`
1061
1159
  });
1062
1160
  makeInteractive(rect, typeof options.onBarClick === "function", options.tooltip !== false);
@@ -1089,6 +1187,20 @@ function renderMultiBarChart(instance) {
1089
1187
  append(markGroup, rect);
1090
1188
  });
1091
1189
  });
1190
+ setupKeyboardNavigation(svg, markGroup);
1191
+ const tableRows = categories.map((cat) => {
1192
+ const rowCells = [cat];
1193
+ seriesList.forEach((s) => {
1194
+ const match = s.rows.find((r) => String(r.x) === cat);
1195
+ rowCells.push(match && isFiniteNumber(match.y) ? formatNumber(match.y) : "");
1196
+ });
1197
+ return rowCells;
1198
+ });
1199
+ renderDataTable(instance, {
1200
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1201
+ headers: [options.xAxis?.label || "Category", ...seriesList.map((s) => s.name)],
1202
+ rows: tableRows
1203
+ });
1092
1204
  if (options.legend !== false) {
1093
1205
  renderTopLegend(
1094
1206
  svg,
@@ -1149,7 +1261,14 @@ function renderLineLikeChart(instance, mode) {
1149
1261
  });
1150
1262
  drawAnnotations(svg, options, plot, xInfo.scale, yScale, theme);
1151
1263
  const labels = dataLabelConfig(options);
1152
- const markGroup = append(svg, svgEl("g", { class: `vd-chart-marks vd-chart-${mode}` }));
1264
+ const markGroup = append(
1265
+ svg,
1266
+ svgEl("g", {
1267
+ class: `vd-chart-marks vd-chart-${mode}`,
1268
+ role: "graphics-object",
1269
+ "aria-roledescription": `${mode} series`
1270
+ })
1271
+ );
1153
1272
  if (mode === "area") {
1154
1273
  const baseline = yScale(Math.max(0, yDomain[0]));
1155
1274
  append(
@@ -1186,6 +1305,7 @@ function renderLineLikeChart(instance, mode) {
1186
1305
  stroke: color,
1187
1306
  "stroke-width": 2,
1188
1307
  role: "graphics-symbol",
1308
+ "aria-roledescription": "data point",
1189
1309
  "aria-label": `${formatCategory(point.xValue)}: ${formatNumber(point.yValue)}`
1190
1310
  });
1191
1311
  makeInteractive(
@@ -1215,6 +1335,12 @@ function renderLineLikeChart(instance, mode) {
1215
1335
  (point) => drawDataLabel(svg, point.x, point.y - 8, point.yValue, labels, theme)
1216
1336
  );
1217
1337
  }
1338
+ setupKeyboardNavigation(svg, markGroup);
1339
+ renderDataTable(instance, {
1340
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1341
+ headers: [options.xAxis?.label || "X", options.yAxis?.label || "Y"],
1342
+ rows: points.map((p) => [formatCategory(p.xValue), formatNumber(p.yValue)])
1343
+ });
1218
1344
  }
1219
1345
  function renderMultiLineChart(instance, mode) {
1220
1346
  const shell = createSvgShell(instance);
@@ -1250,7 +1376,14 @@ function renderMultiLineChart(instance, mode) {
1250
1376
  });
1251
1377
  drawAnnotations(svg, options, plot, xInfo.scale, yScale, theme);
1252
1378
  const labels = dataLabelConfig(options);
1253
- const markGroup = append(svg, svgEl("g", { class: `vd-chart-marks vd-chart-${mode}` }));
1379
+ const markGroup = append(
1380
+ svg,
1381
+ svgEl("g", {
1382
+ class: `vd-chart-marks vd-chart-${mode}`,
1383
+ role: "graphics-object",
1384
+ "aria-roledescription": `multi-series ${mode}`
1385
+ })
1386
+ );
1254
1387
  const baseline = yScale(Math.max(0, yDomain[0]));
1255
1388
  seriesList.forEach((series) => {
1256
1389
  const stroke = seriesColor(series, theme);
@@ -1297,6 +1430,7 @@ function renderMultiLineChart(instance, mode) {
1297
1430
  stroke,
1298
1431
  "stroke-width": 2,
1299
1432
  role: "graphics-symbol",
1433
+ "aria-roledescription": "data point",
1300
1434
  "aria-label": `${series.name} ${formatCategory(point.xValue)}: ${formatNumber(point.yValue)}`
1301
1435
  });
1302
1436
  makeInteractive(
@@ -1329,6 +1463,21 @@ function renderMultiLineChart(instance, mode) {
1329
1463
  );
1330
1464
  }
1331
1465
  });
1466
+ setupKeyboardNavigation(svg, markGroup);
1467
+ const uniqueX = unique(allRows.map((r) => r.x));
1468
+ const tableRows = uniqueX.map((xVal) => {
1469
+ const rowCells = [formatCategory(xVal)];
1470
+ seriesList.forEach((s) => {
1471
+ const match = s.rows.find((r) => r.x === xVal);
1472
+ rowCells.push(match && isFiniteNumber(match.y) ? formatNumber(match.y) : "");
1473
+ });
1474
+ return rowCells;
1475
+ });
1476
+ renderDataTable(instance, {
1477
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1478
+ headers: [options.xAxis?.label || "X", ...seriesList.map((s) => s.name)],
1479
+ rows: tableRows
1480
+ });
1332
1481
  if (options.legend !== false) {
1333
1482
  renderTopLegend(
1334
1483
  svg,
@@ -1380,7 +1529,14 @@ function renderScatterChart(instance) {
1380
1529
  });
1381
1530
  drawAnnotations(svg, options, plot, xInfo.scale, yScale, theme);
1382
1531
  const labels = dataLabelConfig(options);
1383
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-scatter" }));
1532
+ const markGroup = append(
1533
+ svg,
1534
+ svgEl("g", {
1535
+ class: "vd-chart-marks vd-chart-scatter",
1536
+ role: "graphics-object",
1537
+ "aria-roledescription": "data points"
1538
+ })
1539
+ );
1384
1540
  rows.forEach((row) => {
1385
1541
  const cx = xInfo.scale(xInfo.mapValue(row.x));
1386
1542
  const cy = yScale(row.y);
@@ -1394,6 +1550,7 @@ function renderScatterChart(instance) {
1394
1550
  fill,
1395
1551
  opacity: options.pointOpacity ?? 0.88,
1396
1552
  role: "graphics-symbol",
1553
+ "aria-roledescription": "data point",
1397
1554
  "aria-label": `${formatCategory(row.x)}: ${formatNumber(row.y)}`
1398
1555
  });
1399
1556
  makeInteractive(circle, typeof options.onPointClick === "function", options.tooltip !== false);
@@ -1414,6 +1571,12 @@ function renderScatterChart(instance) {
1414
1571
  if (labels) drawDataLabel(svg, cx, cy - 8, row.y, labels, theme);
1415
1572
  append(markGroup, circle);
1416
1573
  });
1574
+ setupKeyboardNavigation(svg, markGroup);
1575
+ renderDataTable(instance, {
1576
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1577
+ headers: [options.xAxis?.label || "X", options.yAxis?.label || "Y"],
1578
+ rows: rows.map((r) => [formatCategory(r.x), formatNumber(r.y)])
1579
+ });
1417
1580
  if (options.legend && color && color.scale) {
1418
1581
  renderTopLegend(
1419
1582
  svg,
@@ -1482,7 +1645,14 @@ function renderDonutChart(instance) {
1482
1645
  const innerRadius = outerRadius * ratio;
1483
1646
  const colorScale = scaleOrdinal({ domain: rows.map((row) => row.label), range: theme.colors });
1484
1647
  const labels = dataLabelConfig(options);
1485
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-slices" }));
1648
+ const markGroup = append(
1649
+ svg,
1650
+ svgEl("g", {
1651
+ class: "vd-chart-marks vd-chart-slices",
1652
+ role: "graphics-object",
1653
+ "aria-roledescription": "pie slices"
1654
+ })
1655
+ );
1486
1656
  let cursor = -Math.PI / 2;
1487
1657
  rows.forEach((row) => {
1488
1658
  const angle = row.value / total * TAU;
@@ -1496,6 +1666,7 @@ function renderDonutChart(instance) {
1496
1666
  stroke: theme.backgroundColor,
1497
1667
  "stroke-width": 2,
1498
1668
  role: "graphics-symbol",
1669
+ "aria-roledescription": "slice",
1499
1670
  "aria-label": `${formatCategory(row.label)}: ${formatNumber(row.value)}`
1500
1671
  });
1501
1672
  makeInteractive(path, typeof options.onSliceClick === "function", options.tooltip !== false);
@@ -1520,6 +1691,16 @@ function renderDonutChart(instance) {
1520
1691
  }
1521
1692
  append(markGroup, path);
1522
1693
  });
1694
+ setupKeyboardNavigation(svg, markGroup);
1695
+ renderDataTable(instance, {
1696
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1697
+ headers: ["Category", "Value", "Percentage"],
1698
+ rows: rows.map((r) => [
1699
+ formatCategory(r.label),
1700
+ formatNumber(r.value),
1701
+ total > 0 ? `${Math.round(r.value / total * 100)}%` : "0%"
1702
+ ])
1703
+ });
1523
1704
  if (innerRadius > 16 && options.centerLabel !== false) {
1524
1705
  append(
1525
1706
  svg,
@@ -1702,7 +1883,11 @@ var CHART_PROPS = {
1702
1883
  xFormat: { type: Function, default: void 0 },
1703
1884
  yFormat: { type: Function, default: void 0 },
1704
1885
  xAxis: { type: Object, default: void 0 },
1705
- yAxis: { type: Object, default: void 0 }
1886
+ yAxis: { type: Object, default: void 0 },
1887
+ // Accessible data table: `true` (sr-only, default) / `false` / `'visible'`.
1888
+ dataTable: { type: [Boolean, String], default: void 0 },
1889
+ // Override the SVG `aria-roledescription` (defaults to `${kind} chart`).
1890
+ ariaRoleDescription: { type: String, default: void 0 }
1706
1891
  };
1707
1892
  function optionsFrom(target, props, emit) {
1708
1893
  return {
@@ -1736,6 +1921,8 @@ function optionsFrom(target, props, emit) {
1736
1921
  yFormat: props.yFormat,
1737
1922
  xAxis: props.xAxis,
1738
1923
  yAxis: props.yAxis,
1924
+ dataTable: props.dataTable,
1925
+ ariaRoleDescription: props.ariaRoleDescription,
1739
1926
  // Forward the core's mark-click callbacks as the Vue emit family. Passing
1740
1927
  // all three unconditionally is safe: the core only fires the callback
1741
1928
  // relevant to the rendered chart type (bars → onBarClick, points →