From 4dea3cf7a3ba7ae4c7c4d8db7f666e7b9506090d Mon Sep 17 00:00:00 2001 From: alfredorubin96 Date: Sat, 20 Jun 2026 20:12:37 +0200 Subject: [PATCH 1/2] fix(charts): descriptive aria labels for all chart types (a11y) General-quality review finding (not branding): pie, radar, gauge, sankey, treemap, sunburst, circle-packing, gantt and choropleth never computed an ariaDescription, so BaseChart fell back to the generic "Chart visualization" for screen readers (bar/line/graph already do this). Each now derives a description from its data shape (e.g. "Pie chart with 5 segments", "Sankey diagram with 8 nodes and 12 links"), with the caller still able to override. New parametrized chart-aria test covers every chart + the override path. tsc + 561 chart tests + lint clean. Co-Authored-By: Claude Opus 4.8 --- .../src/charts/__tests__/chart-aria.test.tsx | 120 ++++++++++++++++++ component/src/charts/choropleth-chart.tsx | 11 +- component/src/charts/circle-packing-chart.tsx | 10 +- component/src/charts/gantt-chart.tsx | 11 +- component/src/charts/gauge-chart.tsx | 10 +- component/src/charts/pie-chart.tsx | 9 +- component/src/charts/radar-chart.tsx | 10 +- component/src/charts/sankey-chart.tsx | 27 +++- component/src/charts/sunburst-chart.tsx | 10 +- component/src/charts/treemap-chart.tsx | 9 +- 10 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 component/src/charts/__tests__/chart-aria.test.tsx diff --git a/component/src/charts/__tests__/chart-aria.test.tsx b/component/src/charts/__tests__/chart-aria.test.tsx new file mode 100644 index 00000000..5fbd1ca2 --- /dev/null +++ b/component/src/charts/__tests__/chart-aria.test.tsx @@ -0,0 +1,120 @@ +import { render, screen, cleanup } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; + +// One place that verifies every chart gives BaseChart a descriptive aria-label +// (was the generic "Chart visualization" fallback) and honours a caller +// override. echarts/core + container size are stubbed so the charts mount. +vi.mock("echarts/core", () => { + const init = vi.fn(() => ({ + setOption: vi.fn(), + resize: vi.fn(), + dispose: vi.fn(), + on: vi.fn(), + off: vi.fn(), + showLoading: vi.fn(), + hideLoading: vi.fn(), + })); + const stub = { + use: vi.fn(), + init, + registerTheme: vi.fn(), + registerMap: vi.fn(), + getMap: vi.fn(() => ({})), + format: { encodeHTML: (s: string) => s }, + }; + return { ...stub, default: stub }; +}); +vi.mock("@/hooks/useContainerSize", () => ({ + useContainerSize: () => ({ width: 600, height: 400, containerRef: vi.fn() }), +})); + +import { PieChart } from "../pie-chart"; +import { RadarChart } from "../radar-chart"; +import { GaugeChart } from "../gauge-chart"; +import { SankeyChart } from "../sankey-chart"; +import { TreemapChart } from "../treemap-chart"; +import { SunburstChart } from "../sunburst-chart"; +import { CirclePackingChart } from "../circle-packing-chart"; +import { GanttChart } from "../gantt-chart"; +import { ChoroplethChart } from "../choropleth-chart"; + +const cases: Array<[string, React.ReactElement, RegExp]> = [ + [ + "pie", + , + /Pie chart with 1 segments/, + ], + [ + "radar", + , + /Radar chart comparing 1 series across 1 axes/, + ], + [ + "gauge", + , + /Gauge showing 50 of 0 to 100/, + ], + [ + "sankey", + , + /Sankey diagram with 2 nodes and 1 links/, + ], + [ + "treemap", + , + /Treemap with 1 top-level items/, + ], + [ + "sunburst", + , + /Sunburst chart with 1 top-level segments/, + ], + [ + "circle-packing", + , + /Circle-packing chart with 1 top-level groups/, + ], + [ + "gantt", + , + /Gantt chart with 1 tasks/, + ], + [ + "choropleth", + , + /Choropleth map with 1 regions/, + ], +]; + +afterEach(cleanup); + +describe("chart aria descriptions", () => { + it.each(cases)("%s gets a descriptive aria-label", (_name, el, re) => { + render(el); + expect(screen.getByTestId("base-chart").getAttribute("aria-label")).toMatch( + re, + ); + }); + + it("honours a caller-provided ariaDescription override", () => { + render( + , + ); + expect(screen.getByTestId("base-chart")).toHaveAttribute( + "aria-label", + "Custom", + ); + }); +}); diff --git a/component/src/charts/choropleth-chart.tsx b/component/src/charts/choropleth-chart.tsx index 1bdb38df..01119bef 100644 --- a/component/src/charts/choropleth-chart.tsx +++ b/component/src/charts/choropleth-chart.tsx @@ -68,6 +68,7 @@ function ChoroplethChart({ minColor = "#fff7d6", maxColor = "#993404", showLabels = false, + ariaDescription, ...rest }: ChoroplethChartProps) { const [mapRegistered, setMapRegistered] = useState(false); @@ -206,7 +207,15 @@ function ChoroplethChart({ showLabels, ]); - return ; + return ( + + ); } export { ChoroplethChart }; diff --git a/component/src/charts/circle-packing-chart.tsx b/component/src/charts/circle-packing-chart.tsx index 9efe0093..8598094d 100644 --- a/component/src/charts/circle-packing-chart.tsx +++ b/component/src/charts/circle-packing-chart.tsx @@ -59,6 +59,7 @@ function CirclePackingChart({ padding = 3, stylingRules, paramValues, + ariaDescription, ...rest }: CirclePackingChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -266,7 +267,14 @@ function CirclePackingChart({ return (
- +
); } diff --git a/component/src/charts/gantt-chart.tsx b/component/src/charts/gantt-chart.tsx index 75eabbc0..5696eedb 100644 --- a/component/src/charts/gantt-chart.tsx +++ b/component/src/charts/gantt-chart.tsx @@ -68,6 +68,7 @@ function GanttChart({ showGridLines = true, stylingRules, paramValues, + ariaDescription, ...rest }: GanttChartProps) { const options = useMemo((): EChartsOption => { @@ -323,7 +324,15 @@ function GanttChart({ paramValues, ]); - return ; + return ( + + ); } export { GanttChart }; diff --git a/component/src/charts/gauge-chart.tsx b/component/src/charts/gauge-chart.tsx index 319ca2d8..ed2abc37 100644 --- a/component/src/charts/gauge-chart.tsx +++ b/component/src/charts/gauge-chart.tsx @@ -64,6 +64,7 @@ function GaugeChart({ thresholdZones: thresholdZonesJson, stylingRules, paramValues, + ariaDescription, ...rest }: GaugeChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -205,7 +206,14 @@ function GaugeChart({ return (
- +
); } diff --git a/component/src/charts/pie-chart.tsx b/component/src/charts/pie-chart.tsx index f5e976d1..4b7d2f19 100644 --- a/component/src/charts/pie-chart.tsx +++ b/component/src/charts/pie-chart.tsx @@ -60,6 +60,7 @@ function PieChart({ donutCenterText, stylingRules, paramValues, + ariaDescription, ...rest }: PieChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -187,7 +188,13 @@ function PieChart({ return (
- +
); } diff --git a/component/src/charts/radar-chart.tsx b/component/src/charts/radar-chart.tsx index 1c0da98c..d56cb1a0 100644 --- a/component/src/charts/radar-chart.tsx +++ b/component/src/charts/radar-chart.tsx @@ -69,6 +69,7 @@ function RadarChart({ showValues = false, stylingRules, paramValues, + ariaDescription, ...rest }: RadarChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -152,7 +153,14 @@ function RadarChart({ return (
- +
); } diff --git a/component/src/charts/sankey-chart.tsx b/component/src/charts/sankey-chart.tsx index a298b166..65ca55a1 100644 --- a/component/src/charts/sankey-chart.tsx +++ b/component/src/charts/sankey-chart.tsx @@ -56,6 +56,7 @@ function SankeyChart({ nodeGap = 8, stylingRules, paramValues, + ariaDescription, ...rest }: SankeyChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -76,7 +77,11 @@ function SankeyChart({ data: data.nodes, links: stylingRules?.length ? data.links.map((link) => { - const resolvedColor = resolveItemColor(link.value, stylingRules, paramValues); + const resolvedColor = resolveItemColor( + link.value, + stylingRules, + paramValues, + ); return { ...link, lineStyle: resolvedColor ? { color: resolvedColor } : {}, @@ -99,11 +104,27 @@ function SankeyChart({ }, ], }; - }, [data, orient, showLabels, nodeWidth, nodeGap, compact, stylingRules, paramValues]); + }, [ + data, + orient, + showLabels, + nodeWidth, + nodeGap, + compact, + stylingRules, + paramValues, + ]); return (
- +
); } diff --git a/component/src/charts/sunburst-chart.tsx b/component/src/charts/sunburst-chart.tsx index 8d8ffb13..c76357b3 100644 --- a/component/src/charts/sunburst-chart.tsx +++ b/component/src/charts/sunburst-chart.tsx @@ -54,6 +54,7 @@ function SunburstChart({ highlightOnHover = true, stylingRules, paramValues, + ariaDescription, ...rest }: SunburstChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -196,7 +197,14 @@ function SunburstChart({ return (
- +
); } diff --git a/component/src/charts/treemap-chart.tsx b/component/src/charts/treemap-chart.tsx index 6a05f040..381802be 100644 --- a/component/src/charts/treemap-chart.tsx +++ b/component/src/charts/treemap-chart.tsx @@ -60,6 +60,7 @@ function TreemapChart({ colorSaturation = "medium", stylingRules, paramValues, + ariaDescription, ...rest }: TreemapChartProps) { const { width, height, containerRef } = useContainerSize(); @@ -174,7 +175,13 @@ function TreemapChart({ return (
- +
); } From 331b0a5bcdc00f60df6392b114218977cb7be43a Mon Sep 17 00:00:00 2001 From: alfredorubin96 Date: Sat, 20 Jun 2026 20:25:29 +0200 Subject: [PATCH 2/2] test(charts): fix gantt test data types in chart-aria test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GanttDataItem is { task: string; start: number; end: number } — the aria test used name/string dates. CI's full tsc (includes tests) caught it. Co-Authored-By: Claude Opus 4.8 --- component/src/charts/__tests__/chart-aria.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/component/src/charts/__tests__/chart-aria.test.tsx b/component/src/charts/__tests__/chart-aria.test.tsx index 5fbd1ca2..be66963e 100644 --- a/component/src/charts/__tests__/chart-aria.test.tsx +++ b/component/src/charts/__tests__/chart-aria.test.tsx @@ -86,9 +86,7 @@ const cases: Array<[string, React.ReactElement, RegExp]> = [ ], [ "gantt", - , + , /Gantt chart with 1 tasks/, ], [