From 579edb38af9e175ff80ba02bfe11abf2f9dee8f6 Mon Sep 17 00:00:00 2001
From: 5affron
Date: Sun, 30 Aug 2026 13:00:04 +0530
Subject: [PATCH 1/2] fix: improve chart and tab accessibility
Signed-off-by: 5affron
---
web/src/components/ChartSectionCard.tsx | 63 ++++++++++++++++---------
web/src/components/TabBar.tsx | 1 +
web/src/test/app.test.tsx | 14 ++++--
3 files changed, 53 insertions(+), 25 deletions(-)
diff --git a/web/src/components/ChartSectionCard.tsx b/web/src/components/ChartSectionCard.tsx
index c34f7d5ae..44bb2e03e 100644
--- a/web/src/components/ChartSectionCard.tsx
+++ b/web/src/components/ChartSectionCard.tsx
@@ -26,6 +26,34 @@ import { VariantTabs } from './VariantTabs';
*/
const axisOf = (chart: ChartSpec) => JSON.stringify(chart.variants.map((variant) => variant.label));
+const sharedAxesFor = (charts: ChartSpec[]) => {
+ const counts = new Map();
+
+ for (const chart of charts) {
+ if (chart.variants.length > 1) {
+ const key = axisOf(chart);
+ counts.set(key, (counts.get(key) ?? 0) + 1);
+ }
+ }
+
+ return charts
+ .filter((chart) => chart.variants.length > 1 && (counts.get(axisOf(chart)) ?? 0) > 1)
+ .map((chart) => ({
+ key: axisOf(chart),
+ labels: chart.variants.map((variant) => variant.label),
+ }))
+ .filter((axis, index, all) => all.findIndex((other) => other.key === axis.key) === index);
+};
+
+function activeVariant(chart: ChartSpec, variant: number) {
+ return chart.variants[Math.min(variant, chart.variants.length - 1)];
+}
+
+function chartAlt(chart: ChartSpec, variant: number) {
+ const active = activeVariant(chart, variant);
+ return chart.variants.length > 1 ? `${chart.title} — ${active.label}` : chart.title;
+}
+
function Figure({
chart,
onZoom,
@@ -40,11 +68,12 @@ function Figure({
stretch?: boolean;
/** Set when the card owns this chart's axis: it renders one tab row for all
* the charts that share it, so this figure shows none of its own. */
- axis?: { index: number; onSelect: (index: number) => void };
+ axis?: { index: number };
}) {
const [own, setOwn] = useState(0);
const variant = Math.min(axis ? axis.index : own, chart.variants.length - 1);
- const active = chart.variants[variant];
+ const active = activeVariant(chart, variant);
+ const alt = chartAlt(chart, variant);
// A tall/square chart (a heatmap) in a ~340px gallery cell is illegible, but
// the `wide` scroll-box treatment would shrink it to the box height instead.
// It gets the full row with natural page flow: the dimensions ship with the
@@ -56,7 +85,7 @@ function Figure({
const img = (
>({});
- const onZoom = (chart: ChartSpec, variant: number) =>
+ const onZoom = (chart: ChartSpec, variant: number) => {
+ const active = activeVariant(chart, variant);
+
setZoom({
- src: chartUrl(chart.variants[variant].file),
- alt: chart.title,
+ src: chartUrl(active.file),
+ alt: chartAlt(chart, variant),
// The tab's own text where it has one: on a role-tabbed chart the
// chart-level note describes maintainers, and showing it on the
// Committers tab misdescribes the population the reader is looking at.
note: chart.variants[variant].note ?? chart.note,
- methodology: chart.variants[variant].methodology ?? chart.methodology,
+ methodology: active.methodology ?? chart.methodology,
});
+ };
const count = section.charts.length;
// An axis belongs to the card once two charts offer the same labels; a lone
// multi-variant chart keeps its own tabs where they sit, under its caption.
- const axisCounts = new Map();
- for (const chart of section.charts) {
- if (chart.variants.length > 1) {
- const axis = axisOf(chart);
- axisCounts.set(axis, (axisCounts.get(axis) ?? 0) + 1);
- }
- }
- const sharedAxes = section.charts
- .filter((chart) => chart.variants.length > 1 && (axisCounts.get(axisOf(chart)) ?? 0) > 1)
- .map((chart) => ({ key: axisOf(chart), labels: chart.variants.map((v) => v.label) }))
- .filter((axis, index, all) => all.findIndex((other) => other.key === axis.key) === index);
+ const sharedAxes = sharedAxesFor(section.charts);
const axisFor = (chart: ChartSpec) =>
sharedAxes.some((axis) => axis.key === axisOf(chart))
- ? {
- index: shared[axisOf(chart)] ?? 0,
- onSelect: (index: number) => setShared({ ...shared, [axisOf(chart)]: index }),
- }
+ ? { index: shared[axisOf(chart)] ?? 0 }
: undefined;
// The gallery lays half-width charts out in pairs; full-row charts break the
diff --git a/web/src/components/TabBar.tsx b/web/src/components/TabBar.tsx
index 0421b47b8..f769c8b70 100644
--- a/web/src/components/TabBar.tsx
+++ b/web/src/components/TabBar.tsx
@@ -21,6 +21,7 @@ export function TabBar({
}
>
diff --git a/web/src/components/InteractiveChart.tsx b/web/src/components/InteractiveChart.tsx
index daf298de9..2d0713da7 100644
--- a/web/src/components/InteractiveChart.tsx
+++ b/web/src/components/InteractiveChart.tsx
@@ -8,6 +8,7 @@ import { RotateCwIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { ChartDocument, ChartVariant, Manifest } from '../api';
import { fetchChartDocument } from '../chartData';
+import { chartViewName } from '../format';
import { EventsView } from './charts/EventsView';
import { MatrixView } from './charts/MatrixView';
import { NetworkView } from './charts/NetworkView';
@@ -83,7 +84,7 @@ export default function InteractiveChart({
data-print-pending
className="flex h-[340px] items-center justify-center text-sm text-muted-foreground"
>
- Loading interactive chart: {title} ({variant.label})…
+ Loading interactive chart: {chartViewName(title, variant.label)}…
>
);
@@ -96,7 +97,7 @@ export default function InteractiveChart({
data-print-error
className="print-chart-status mb-4 flex flex-wrap items-center gap-3 rounded-lg border p-4 text-sm"
>
- Could not load chart data: {title} ({variant.label}).
+ Could not load chart data: {chartViewName(title, variant.label)}.