@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.
- package/CHANGELOG.md +83 -0
- package/README.md +24 -9
- package/SKILL.md +42 -15
- package/dist/charts/core.d.ts +4 -0
- package/dist/charts/index.cjs +197 -10
- package/dist/charts/index.cjs.map +3 -3
- package/dist/charts/index.js +197 -10
- package/dist/charts/index.js.map +3 -3
- package/dist/charts/vd3-charts.css +82 -0
- package/dist/charts/vue.d.ts +4 -0
- package/dist/code-editor/core.d.ts +19 -2
- package/dist/code-editor/highlight.cjs +1242 -0
- package/dist/code-editor/highlight.cjs.map +7 -0
- package/dist/code-editor/highlight.d.ts +6 -0
- package/dist/code-editor/highlight.js +1219 -0
- package/dist/code-editor/highlight.js.map +7 -0
- package/dist/code-editor/index.cjs +694 -70
- package/dist/code-editor/index.cjs.map +4 -4
- package/dist/code-editor/index.d.ts +2 -0
- package/dist/code-editor/index.js +694 -70
- package/dist/code-editor/index.js.map +4 -4
- package/dist/code-editor/vd3-code-editor.css +5 -0
- package/dist/draw/core.d.ts +7 -1
- package/dist/draw/index.cjs +470 -52
- package/dist/draw/index.cjs.map +2 -2
- package/dist/draw/index.js +470 -52
- package/dist/draw/index.js.map +2 -2
- package/dist/draw/vd3-draw.css +45 -0
- package/dist/draw/vue.d.ts +4 -0
- package/dist/hex-grid/core.d.ts +41 -0
- package/dist/hex-grid/index.cjs +308 -13
- package/dist/hex-grid/index.cjs.map +3 -3
- package/dist/hex-grid/index.d.ts +1 -0
- package/dist/hex-grid/index.js +308 -13
- package/dist/hex-grid/index.js.map +3 -3
- package/dist/hex-grid/vue.d.ts +4 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +2 -2
- package/dist/meta.json +177 -66
- 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.
|
|
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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
88
|
-
|
|
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`.
|
|
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,
|
|
60
|
-
Python) — with a line-number gutter + active-line highlight,
|
|
61
|
-
bracket/quote auto-close, read-only mode, a copy button, a
|
|
62
|
-
large-input performance guard. No third-party editor and no
|
|
63
|
-
beyond `vue`; rendering is escaping-safe and `innerHTML`-free,
|
|
64
|
-
SSR-safe (DOM touched only on mount).
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
`
|
|
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`.
|
|
97
|
-
|
|
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
|
|
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
|
|
package/dist/charts/core.d.ts
CHANGED
|
@@ -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> {
|
package/dist/charts/index.cjs
CHANGED
|
@@ -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: "
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 →
|