@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
@@ -444,6 +444,74 @@ function makeInteractive(mark, hasClick, hasTooltip) {
444
444
  mark.setAttribute("focusable", "true");
445
445
  }
446
446
  }
447
+ function setupKeyboardNavigation(svg, markGroup) {
448
+ svg.addEventListener("keydown", (event) => {
449
+ const active = document.activeElement;
450
+ if (!active || !markGroup.contains(active)) return;
451
+ const marks = Array.from(markGroup.querySelectorAll('[tabindex="0"]'));
452
+ if (!marks.length) return;
453
+ const currentIndex = marks.indexOf(active);
454
+ if (currentIndex === -1) return;
455
+ let targetIndex = -1;
456
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
457
+ event.preventDefault();
458
+ targetIndex = (currentIndex + 1) % marks.length;
459
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
460
+ event.preventDefault();
461
+ targetIndex = (currentIndex - 1 + marks.length) % marks.length;
462
+ } else if (event.key === "Home") {
463
+ event.preventDefault();
464
+ targetIndex = 0;
465
+ } else if (event.key === "End") {
466
+ event.preventDefault();
467
+ targetIndex = marks.length - 1;
468
+ } else if (event.key === "Escape") {
469
+ event.preventDefault();
470
+ active.blur();
471
+ }
472
+ if (targetIndex !== -1 && marks[targetIndex]) {
473
+ marks[targetIndex].focus();
474
+ }
475
+ });
476
+ }
477
+ function renderDataTable(instance, tableData) {
478
+ const { target, options } = instance;
479
+ if (options.dataTable === false) return;
480
+ if (!tableData || !tableData.rows || !tableData.rows.length) return;
481
+ const table = document.createElement("table");
482
+ table.className = "vd-chart-data-table";
483
+ if (options.dataTable !== "visible") {
484
+ table.classList.add("vd-chart-sr-only");
485
+ }
486
+ const caption = document.createElement("caption");
487
+ caption.textContent = tableData.caption || options.title || options.ariaLabel || `${instance.kind} chart data`;
488
+ table.appendChild(caption);
489
+ if (tableData.headers && tableData.headers.length) {
490
+ const thead = document.createElement("thead");
491
+ const headerRow = document.createElement("tr");
492
+ tableData.headers.forEach((h2) => {
493
+ const th = document.createElement("th");
494
+ th.setAttribute("scope", "col");
495
+ th.textContent = String(h2);
496
+ headerRow.appendChild(th);
497
+ });
498
+ thead.appendChild(headerRow);
499
+ table.appendChild(thead);
500
+ }
501
+ const tbody = document.createElement("tbody");
502
+ tableData.rows.forEach((row) => {
503
+ const tr = document.createElement("tr");
504
+ row.forEach((cell, idx) => {
505
+ const el = idx === 0 ? document.createElement("th") : document.createElement("td");
506
+ if (idx === 0) el.setAttribute("scope", "row");
507
+ el.textContent = cell == null ? "" : String(cell);
508
+ tr.appendChild(el);
509
+ });
510
+ tbody.appendChild(tr);
511
+ });
512
+ table.appendChild(tbody);
513
+ target.appendChild(table);
514
+ }
447
515
  function createSvgShell(instance) {
448
516
  const { target, options } = instance;
449
517
  const size = measureTarget(target, options);
@@ -460,7 +528,8 @@ function createSvgShell(instance) {
460
528
  width: size.width,
461
529
  height: size.height,
462
530
  viewBox: `0 0 ${size.width} ${size.height}`,
463
- role: "img"
531
+ role: options.role || "graphics-document document",
532
+ "aria-roledescription": options.ariaRoleDescription || `${instance.kind} chart`
464
533
  });
465
534
  const titleId = nextId("vd-chart-title");
466
535
  const descId = nextId("vd-chart-desc");
@@ -531,7 +600,7 @@ function drawAxisLine(svg, x1, y1, x2, y2, theme) {
531
600
  }
532
601
  function drawCartesianAxes(svg, config) {
533
602
  const { plot, xScale, yScale, xTicks, yTicks, theme, options, categoricalX } = config;
534
- const axisGroup = append(svg, svgEl("g", { class: "vd-chart-axes" }));
603
+ const axisGroup = append(svg, svgEl("g", { class: "vd-chart-axes", "aria-hidden": "true" }));
535
604
  yTicks.forEach((tick) => {
536
605
  const y = yScale(tick);
537
606
  if (!isFiniteNumber(y)) return;
@@ -774,7 +843,14 @@ function drawDataLabel(svg, x, y, value, cfg, theme, anchor = "middle") {
774
843
  function drawAnnotations(svg, options, plot, xScale, yScale, theme) {
775
844
  const annotations = toArray(options.annotations);
776
845
  if (!annotations.length) return;
777
- const group = append(svg, svgEl("g", { class: "vd-chart-annotations" }));
846
+ const group = append(
847
+ svg,
848
+ svgEl("g", {
849
+ class: "vd-chart-annotations",
850
+ role: "graphics-object",
851
+ "aria-roledescription": "annotations"
852
+ })
853
+ );
778
854
  annotations.forEach((ann) => {
779
855
  const color = ann.color || theme.mutedTextColor;
780
856
  const dash = ann.dash === false ? null : "4 3";
@@ -894,7 +970,14 @@ function renderBarChart(instance) {
894
970
  });
895
971
  drawAnnotations(svg, options, plot, xScale, yScale, theme);
896
972
  const labels = dataLabelConfig(options);
897
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-bars" }));
973
+ const markGroup = append(
974
+ svg,
975
+ svgEl("g", {
976
+ class: "vd-chart-marks vd-chart-bars",
977
+ role: "graphics-object",
978
+ "aria-roledescription": "data points"
979
+ })
980
+ );
898
981
  const baseline = yScale(Math.min(Math.max(0, yDomain[0]), yDomain[1]));
899
982
  rows.forEach((row) => {
900
983
  const x = xScale(row.x);
@@ -912,6 +995,7 @@ function renderBarChart(instance) {
912
995
  rx: 3,
913
996
  fill,
914
997
  role: "graphics-symbol",
998
+ "aria-roledescription": "bar",
915
999
  "aria-label": `${formatCategory(row.x)}: ${formatNumber(row.y)}`
916
1000
  });
917
1001
  makeInteractive(rect, typeof options.onBarClick === "function", options.tooltip !== false);
@@ -933,6 +1017,12 @@ function renderBarChart(instance) {
933
1017
  if (labels) drawDataLabel(svg, x + xScale.bandwidth() / 2, rectY - 4, row.y, labels, theme);
934
1018
  append(markGroup, rect);
935
1019
  });
1020
+ setupKeyboardNavigation(svg, markGroup);
1021
+ renderDataTable(instance, {
1022
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1023
+ headers: [options.xAxis?.label || "Category", options.yAxis?.label || "Value"],
1024
+ rows: rows.map((r) => [formatCategory(r.x), formatNumber(r.y)])
1025
+ });
936
1026
  if (options.legend && color && color.scale) {
937
1027
  renderTopLegend(
938
1028
  svg,
@@ -986,7 +1076,14 @@ function renderMultiBarChart(instance) {
986
1076
  });
987
1077
  drawAnnotations(svg, options, plot, xScale, yScale, theme);
988
1078
  const labels = dataLabelConfig(options);
989
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-bars" }));
1079
+ const markGroup = append(
1080
+ svg,
1081
+ svgEl("g", {
1082
+ class: "vd-chart-marks vd-chart-bars",
1083
+ role: "graphics-object",
1084
+ "aria-roledescription": "grouped bars"
1085
+ })
1086
+ );
990
1087
  const baseline = yScale(Math.min(Math.max(0, yDomain[0]), yDomain[1]));
991
1088
  seriesList.forEach((series) => {
992
1089
  const fill = seriesColor(series, theme);
@@ -1006,6 +1103,7 @@ function renderMultiBarChart(instance) {
1006
1103
  rx: 3,
1007
1104
  fill,
1008
1105
  role: "graphics-symbol",
1106
+ "aria-roledescription": "bar",
1009
1107
  "aria-label": `${series.name} \u2014 ${formatCategory(row.x)}: ${formatNumber(row.y)}`
1010
1108
  });
1011
1109
  makeInteractive(rect, typeof options.onBarClick === "function", options.tooltip !== false);
@@ -1038,6 +1136,20 @@ function renderMultiBarChart(instance) {
1038
1136
  append(markGroup, rect);
1039
1137
  });
1040
1138
  });
1139
+ setupKeyboardNavigation(svg, markGroup);
1140
+ const tableRows = categories.map((cat) => {
1141
+ const rowCells = [cat];
1142
+ seriesList.forEach((s) => {
1143
+ const match = s.rows.find((r) => String(r.x) === cat);
1144
+ rowCells.push(match && isFiniteNumber(match.y) ? formatNumber(match.y) : "");
1145
+ });
1146
+ return rowCells;
1147
+ });
1148
+ renderDataTable(instance, {
1149
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1150
+ headers: [options.xAxis?.label || "Category", ...seriesList.map((s) => s.name)],
1151
+ rows: tableRows
1152
+ });
1041
1153
  if (options.legend !== false) {
1042
1154
  renderTopLegend(
1043
1155
  svg,
@@ -1098,7 +1210,14 @@ function renderLineLikeChart(instance, mode) {
1098
1210
  });
1099
1211
  drawAnnotations(svg, options, plot, xInfo.scale, yScale, theme);
1100
1212
  const labels = dataLabelConfig(options);
1101
- const markGroup = append(svg, svgEl("g", { class: `vd-chart-marks vd-chart-${mode}` }));
1213
+ const markGroup = append(
1214
+ svg,
1215
+ svgEl("g", {
1216
+ class: `vd-chart-marks vd-chart-${mode}`,
1217
+ role: "graphics-object",
1218
+ "aria-roledescription": `${mode} series`
1219
+ })
1220
+ );
1102
1221
  if (mode === "area") {
1103
1222
  const baseline = yScale(Math.max(0, yDomain[0]));
1104
1223
  append(
@@ -1135,6 +1254,7 @@ function renderLineLikeChart(instance, mode) {
1135
1254
  stroke: color,
1136
1255
  "stroke-width": 2,
1137
1256
  role: "graphics-symbol",
1257
+ "aria-roledescription": "data point",
1138
1258
  "aria-label": `${formatCategory(point.xValue)}: ${formatNumber(point.yValue)}`
1139
1259
  });
1140
1260
  makeInteractive(
@@ -1164,6 +1284,12 @@ function renderLineLikeChart(instance, mode) {
1164
1284
  (point) => drawDataLabel(svg, point.x, point.y - 8, point.yValue, labels, theme)
1165
1285
  );
1166
1286
  }
1287
+ setupKeyboardNavigation(svg, markGroup);
1288
+ renderDataTable(instance, {
1289
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1290
+ headers: [options.xAxis?.label || "X", options.yAxis?.label || "Y"],
1291
+ rows: points.map((p) => [formatCategory(p.xValue), formatNumber(p.yValue)])
1292
+ });
1167
1293
  }
1168
1294
  function renderMultiLineChart(instance, mode) {
1169
1295
  const shell = createSvgShell(instance);
@@ -1199,7 +1325,14 @@ function renderMultiLineChart(instance, mode) {
1199
1325
  });
1200
1326
  drawAnnotations(svg, options, plot, xInfo.scale, yScale, theme);
1201
1327
  const labels = dataLabelConfig(options);
1202
- const markGroup = append(svg, svgEl("g", { class: `vd-chart-marks vd-chart-${mode}` }));
1328
+ const markGroup = append(
1329
+ svg,
1330
+ svgEl("g", {
1331
+ class: `vd-chart-marks vd-chart-${mode}`,
1332
+ role: "graphics-object",
1333
+ "aria-roledescription": `multi-series ${mode}`
1334
+ })
1335
+ );
1203
1336
  const baseline = yScale(Math.max(0, yDomain[0]));
1204
1337
  seriesList.forEach((series) => {
1205
1338
  const stroke = seriesColor(series, theme);
@@ -1246,6 +1379,7 @@ function renderMultiLineChart(instance, mode) {
1246
1379
  stroke,
1247
1380
  "stroke-width": 2,
1248
1381
  role: "graphics-symbol",
1382
+ "aria-roledescription": "data point",
1249
1383
  "aria-label": `${series.name} ${formatCategory(point.xValue)}: ${formatNumber(point.yValue)}`
1250
1384
  });
1251
1385
  makeInteractive(
@@ -1278,6 +1412,21 @@ function renderMultiLineChart(instance, mode) {
1278
1412
  );
1279
1413
  }
1280
1414
  });
1415
+ setupKeyboardNavigation(svg, markGroup);
1416
+ const uniqueX = unique(allRows.map((r) => r.x));
1417
+ const tableRows = uniqueX.map((xVal) => {
1418
+ const rowCells = [formatCategory(xVal)];
1419
+ seriesList.forEach((s) => {
1420
+ const match = s.rows.find((r) => r.x === xVal);
1421
+ rowCells.push(match && isFiniteNumber(match.y) ? formatNumber(match.y) : "");
1422
+ });
1423
+ return rowCells;
1424
+ });
1425
+ renderDataTable(instance, {
1426
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1427
+ headers: [options.xAxis?.label || "X", ...seriesList.map((s) => s.name)],
1428
+ rows: tableRows
1429
+ });
1281
1430
  if (options.legend !== false) {
1282
1431
  renderTopLegend(
1283
1432
  svg,
@@ -1329,7 +1478,14 @@ function renderScatterChart(instance) {
1329
1478
  });
1330
1479
  drawAnnotations(svg, options, plot, xInfo.scale, yScale, theme);
1331
1480
  const labels = dataLabelConfig(options);
1332
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-scatter" }));
1481
+ const markGroup = append(
1482
+ svg,
1483
+ svgEl("g", {
1484
+ class: "vd-chart-marks vd-chart-scatter",
1485
+ role: "graphics-object",
1486
+ "aria-roledescription": "data points"
1487
+ })
1488
+ );
1333
1489
  rows.forEach((row) => {
1334
1490
  const cx = xInfo.scale(xInfo.mapValue(row.x));
1335
1491
  const cy = yScale(row.y);
@@ -1343,6 +1499,7 @@ function renderScatterChart(instance) {
1343
1499
  fill,
1344
1500
  opacity: options.pointOpacity ?? 0.88,
1345
1501
  role: "graphics-symbol",
1502
+ "aria-roledescription": "data point",
1346
1503
  "aria-label": `${formatCategory(row.x)}: ${formatNumber(row.y)}`
1347
1504
  });
1348
1505
  makeInteractive(circle, typeof options.onPointClick === "function", options.tooltip !== false);
@@ -1363,6 +1520,12 @@ function renderScatterChart(instance) {
1363
1520
  if (labels) drawDataLabel(svg, cx, cy - 8, row.y, labels, theme);
1364
1521
  append(markGroup, circle);
1365
1522
  });
1523
+ setupKeyboardNavigation(svg, markGroup);
1524
+ renderDataTable(instance, {
1525
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1526
+ headers: [options.xAxis?.label || "X", options.yAxis?.label || "Y"],
1527
+ rows: rows.map((r) => [formatCategory(r.x), formatNumber(r.y)])
1528
+ });
1366
1529
  if (options.legend && color && color.scale) {
1367
1530
  renderTopLegend(
1368
1531
  svg,
@@ -1431,7 +1594,14 @@ function renderDonutChart(instance) {
1431
1594
  const innerRadius = outerRadius * ratio;
1432
1595
  const colorScale = scaleOrdinal({ domain: rows.map((row) => row.label), range: theme.colors });
1433
1596
  const labels = dataLabelConfig(options);
1434
- const markGroup = append(svg, svgEl("g", { class: "vd-chart-marks vd-chart-slices" }));
1597
+ const markGroup = append(
1598
+ svg,
1599
+ svgEl("g", {
1600
+ class: "vd-chart-marks vd-chart-slices",
1601
+ role: "graphics-object",
1602
+ "aria-roledescription": "pie slices"
1603
+ })
1604
+ );
1435
1605
  let cursor = -Math.PI / 2;
1436
1606
  rows.forEach((row) => {
1437
1607
  const angle = row.value / total * TAU;
@@ -1445,6 +1615,7 @@ function renderDonutChart(instance) {
1445
1615
  stroke: theme.backgroundColor,
1446
1616
  "stroke-width": 2,
1447
1617
  role: "graphics-symbol",
1618
+ "aria-roledescription": "slice",
1448
1619
  "aria-label": `${formatCategory(row.label)}: ${formatNumber(row.value)}`
1449
1620
  });
1450
1621
  makeInteractive(path, typeof options.onSliceClick === "function", options.tooltip !== false);
@@ -1469,6 +1640,16 @@ function renderDonutChart(instance) {
1469
1640
  }
1470
1641
  append(markGroup, path);
1471
1642
  });
1643
+ setupKeyboardNavigation(svg, markGroup);
1644
+ renderDataTable(instance, {
1645
+ caption: options.title || options.ariaLabel || `${instance.kind} chart data`,
1646
+ headers: ["Category", "Value", "Percentage"],
1647
+ rows: rows.map((r) => [
1648
+ formatCategory(r.label),
1649
+ formatNumber(r.value),
1650
+ total > 0 ? `${Math.round(r.value / total * 100)}%` : "0%"
1651
+ ])
1652
+ });
1472
1653
  if (innerRadius > 16 && options.centerLabel !== false) {
1473
1654
  append(
1474
1655
  svg,
@@ -1651,7 +1832,11 @@ var CHART_PROPS = {
1651
1832
  xFormat: { type: Function, default: void 0 },
1652
1833
  yFormat: { type: Function, default: void 0 },
1653
1834
  xAxis: { type: Object, default: void 0 },
1654
- yAxis: { type: Object, default: void 0 }
1835
+ yAxis: { type: Object, default: void 0 },
1836
+ // Accessible data table: `true` (sr-only, default) / `false` / `'visible'`.
1837
+ dataTable: { type: [Boolean, String], default: void 0 },
1838
+ // Override the SVG `aria-roledescription` (defaults to `${kind} chart`).
1839
+ ariaRoleDescription: { type: String, default: void 0 }
1655
1840
  };
1656
1841
  function optionsFrom(target, props, emit) {
1657
1842
  return {
@@ -1685,6 +1870,8 @@ function optionsFrom(target, props, emit) {
1685
1870
  yFormat: props.yFormat,
1686
1871
  xAxis: props.xAxis,
1687
1872
  yAxis: props.yAxis,
1873
+ dataTable: props.dataTable,
1874
+ ariaRoleDescription: props.ariaRoleDescription,
1688
1875
  // Forward the core's mark-click callbacks as the Vue emit family. Passing
1689
1876
  // all three unconditionally is safe: the core only fires the callback
1690
1877
  // relevant to the rendered chart type (bars → onBarClick, points →