@optilogic/charts 1.3.11 → 1.4.0

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/dist/index.cjs CHANGED
@@ -232,6 +232,13 @@ function resolveLegendConfig(legend) {
232
232
  if (legend === true) return { position: "top", align: "right" };
233
233
  return legend;
234
234
  }
235
+ function formatCell(value) {
236
+ if (value == null) return "";
237
+ if (typeof value === "number") return value.toLocaleString();
238
+ if (value instanceof Date) return value.toLocaleString();
239
+ if (typeof value === "object") return JSON.stringify(value);
240
+ return String(value);
241
+ }
235
242
  var ChartContainer = React17__namespace.forwardRef(function ChartContainer2({
236
243
  children,
237
244
  className,
@@ -240,8 +247,18 @@ var ChartContainer = React17__namespace.forwardRef(function ChartContainer2({
240
247
  empty,
241
248
  emptyMessage = "No data available",
242
249
  title,
243
- description
250
+ description,
251
+ ariaLabel,
252
+ ariaDescription,
253
+ tableData,
254
+ tableColumns,
255
+ onRowActivate,
256
+ rowActionLabel
244
257
  }, ref) {
258
+ const descriptionId = React17__namespace.useId();
259
+ const accessibleName = ariaLabel ?? title;
260
+ const accessibleDescription = ariaDescription ?? description;
261
+ const hasTable = !loading && !empty && !!tableData && tableData.length > 0 && !!tableColumns && tableColumns.length > 0;
245
262
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: core.cn("w-full", className), style: { height }, children: [
246
263
  (title || description) && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: 8 }, children: [
247
264
  title && /* @__PURE__ */ jsxRuntime.jsx(
@@ -304,12 +321,51 @@ var ChartContainer = React17__namespace.forwardRef(function ChartContainer2({
304
321
  },
305
322
  children: emptyMessage
306
323
  }
307
- ) : /* @__PURE__ */ jsxRuntime.jsx(
308
- recharts.ResponsiveContainer,
324
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
325
+ "div",
309
326
  {
310
- width: "100%",
311
- height: title || description ? "calc(100% - 40px)" : "100%",
312
- children
327
+ role: "img",
328
+ "aria-label": accessibleName,
329
+ "aria-describedby": accessibleDescription ? descriptionId : void 0,
330
+ className: "relative",
331
+ style: {
332
+ width: "100%",
333
+ height: title || description ? "calc(100% - 40px)" : "100%"
334
+ },
335
+ children: [
336
+ accessibleDescription && /* @__PURE__ */ jsxRuntime.jsx(core.VisuallyHidden, { as: "span", id: descriptionId, children: accessibleDescription }),
337
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children }),
338
+ hasTable && /* @__PURE__ */ jsxRuntime.jsx(core.VisuallyHidden, { as: "div", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
339
+ accessibleName && /* @__PURE__ */ jsxRuntime.jsx("caption", { children: accessibleName }),
340
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: tableColumns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", children: col.label }, col.key)) }) }),
341
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: tableData.map((row, rowIndex) => {
342
+ const record = row;
343
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: tableColumns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("td", { children: formatCell(record[col.key]) }, col.key)) }, rowIndex);
344
+ }) })
345
+ ] }) }),
346
+ hasTable && onRowActivate && /* @__PURE__ */ jsxRuntime.jsx(
347
+ "nav",
348
+ {
349
+ "aria-label": `${accessibleName ?? "Chart"} data point links`,
350
+ className: "sr-only focus-within:not-sr-only focus-within:absolute focus-within:z-20 focus-within:top-1 focus-within:left-1 focus-within:flex focus-within:max-h-[calc(100%-0.5rem)] focus-within:flex-col focus-within:gap-1 focus-within:overflow-auto focus-within:rounded-md focus-within:border focus-within:border-border focus-within:bg-popover focus-within:p-2 focus-within:shadow-md",
351
+ children: tableData.map((row, rowIndex) => {
352
+ const record = row;
353
+ const firstKey = tableColumns[0]?.key;
354
+ const label = rowActionLabel?.(row, rowIndex) ?? (firstKey ? formatCell(record[firstKey]) : `Row ${rowIndex + 1}`);
355
+ return /* @__PURE__ */ jsxRuntime.jsx(
356
+ "button",
357
+ {
358
+ type: "button",
359
+ onClick: () => onRowActivate(row, rowIndex),
360
+ className: "rounded px-1 py-0.5 text-left text-sm text-foreground underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
361
+ children: label
362
+ },
363
+ rowIndex
364
+ );
365
+ })
366
+ }
367
+ )
368
+ ]
313
369
  }
314
370
  )
315
371
  ] });
@@ -362,6 +418,14 @@ function useLiveData(config = {}) {
362
418
  }
363
419
 
364
420
  // src/shared/formatters.ts
421
+ function toCellValue(value) {
422
+ if (value == null) return null;
423
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
424
+ return value;
425
+ }
426
+ if (value instanceof Date) return value;
427
+ return null;
428
+ }
365
429
  var compactThresholds = [
366
430
  [1e9, "B"],
367
431
  [1e6, "M"],
@@ -428,6 +492,13 @@ var LineChart = React17__namespace.forwardRef(
428
492
  const legendConfig = resolveLegendConfig(legend);
429
493
  const tooltipProps = resolveTooltipProps(tooltip);
430
494
  const isAnimated = live ? false : animate;
495
+ const tableColumns = React17__namespace.useMemo(
496
+ () => [
497
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
498
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
499
+ ],
500
+ [xAxis.dataKey, xAxis.label, series]
501
+ );
431
502
  return /* @__PURE__ */ jsxRuntime.jsx(
432
503
  ChartContainer,
433
504
  {
@@ -436,6 +507,9 @@ var LineChart = React17__namespace.forwardRef(
436
507
  height,
437
508
  loading,
438
509
  empty: !data?.length,
510
+ ariaLabel: "Line chart",
511
+ tableData: data,
512
+ tableColumns,
439
513
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.LineChart, { data, margin, children: [
440
514
  gridConfig && /* @__PURE__ */ jsxRuntime.jsx(
441
515
  recharts.CartesianGrid,
@@ -530,7 +604,8 @@ var BarChart = React17__namespace.forwardRef(
530
604
  className,
531
605
  height = "100%",
532
606
  margin = { top: 8, right: 12, left: 0, bottom: 4 },
533
- loading
607
+ loading,
608
+ onDataPointClick
534
609
  }, ref) => {
535
610
  const gridConfig = React17__namespace.useMemo(() => {
536
611
  if (grid === false) return null;
@@ -541,6 +616,36 @@ var BarChart = React17__namespace.forwardRef(
541
616
  const tooltipProps = resolveTooltipProps(tooltip);
542
617
  const isAnimated = live ? false : animate;
543
618
  const isHorizontal = layout === "horizontal";
619
+ const tableColumns = React17__namespace.useMemo(
620
+ () => [
621
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
622
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
623
+ ],
624
+ [xAxis.dataKey, xAxis.label, series]
625
+ );
626
+ const seriesScaleDomains = React17__namespace.useMemo(() => {
627
+ const domains = {};
628
+ for (const s of series) {
629
+ const cf = s.conditionalFormat;
630
+ if (cf?.type === "colorScale" && !cf.domain) {
631
+ domains[s.dataKey] = core.computeScaleDomain(
632
+ data.map((d) => toCellValue(d[s.dataKey]))
633
+ );
634
+ }
635
+ }
636
+ return domains;
637
+ }, [series, data]);
638
+ const resolveBarFill = (s, datum, fallback) => {
639
+ if (!s.conditionalFormat) return fallback;
640
+ const fmt = core.resolveCellFormat(
641
+ s.conditionalFormat,
642
+ toCellValue(datum[s.dataKey]),
643
+ datum,
644
+ seriesScaleDomains[s.dataKey]
645
+ );
646
+ const color = fmt?.background ?? fmt?.color;
647
+ return color ? core.resolveFormatColor(color) : fallback;
648
+ };
544
649
  return /* @__PURE__ */ jsxRuntime.jsx(
545
650
  ChartContainer,
546
651
  {
@@ -549,6 +654,13 @@ var BarChart = React17__namespace.forwardRef(
549
654
  height,
550
655
  loading,
551
656
  empty: !data?.length,
657
+ ariaLabel: "Bar chart",
658
+ tableData: data,
659
+ tableColumns,
660
+ onRowActivate: onDataPointClick ? (_row, index) => {
661
+ const datum = data[index];
662
+ if (datum) onDataPointClick(datum, index);
663
+ } : void 0,
552
664
  children: /* @__PURE__ */ jsxRuntime.jsxs(
553
665
  recharts.BarChart,
554
666
  {
@@ -647,19 +759,36 @@ var BarChart = React17__namespace.forwardRef(
647
759
  content: /* @__PURE__ */ jsxRuntime.jsx(ChartLegendContent, {})
648
760
  }
649
761
  ),
650
- series.map((s, index) => /* @__PURE__ */ jsxRuntime.jsx(
651
- recharts.Bar,
652
- {
653
- dataKey: s.dataKey,
654
- name: s.name,
655
- fill: getChartColor(index, s.color),
656
- stackId: s.stackId,
657
- radius: s.radius ?? 0,
658
- isAnimationActive: isAnimated,
659
- animationDuration: isAnimated ? 300 : 0
660
- },
661
- s.dataKey
662
- ))
762
+ series.map((s, index) => {
763
+ const baseFill = getChartColor(index, s.color);
764
+ const interactive = !!onDataPointClick;
765
+ const perDatum = !!s.conditionalFormat || interactive;
766
+ return /* @__PURE__ */ jsxRuntime.jsx(
767
+ recharts.Bar,
768
+ {
769
+ dataKey: s.dataKey,
770
+ name: s.name,
771
+ fill: baseFill,
772
+ stackId: s.stackId,
773
+ radius: s.radius ?? 0,
774
+ isAnimationActive: isAnimated,
775
+ animationDuration: isAnimated ? 300 : 0,
776
+ onClick: interactive ? (_data, i) => {
777
+ const datum = data[i];
778
+ if (datum) onDataPointClick?.(datum, i);
779
+ } : void 0,
780
+ children: perDatum ? data.map((d, i) => /* @__PURE__ */ jsxRuntime.jsx(
781
+ recharts.Cell,
782
+ {
783
+ fill: resolveBarFill(s, d, baseFill),
784
+ cursor: interactive ? "pointer" : void 0
785
+ },
786
+ `${s.dataKey}-${i}`
787
+ )) : null
788
+ },
789
+ s.dataKey
790
+ );
791
+ })
663
792
  ]
664
793
  }
665
794
  )
@@ -692,6 +821,13 @@ var AreaChart = React17__namespace.forwardRef(
692
821
  const legendConfig = resolveLegendConfig(legend);
693
822
  const tooltipProps = resolveTooltipProps(tooltip);
694
823
  const isAnimated = live ? false : animate;
824
+ const tableColumns = React17__namespace.useMemo(
825
+ () => [
826
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
827
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
828
+ ],
829
+ [xAxis.dataKey, xAxis.label, series]
830
+ );
695
831
  return /* @__PURE__ */ jsxRuntime.jsx(
696
832
  ChartContainer,
697
833
  {
@@ -700,6 +836,9 @@ var AreaChart = React17__namespace.forwardRef(
700
836
  height,
701
837
  loading,
702
838
  empty: !data?.length,
839
+ ariaLabel: "Area chart",
840
+ tableData: data,
841
+ tableColumns,
703
842
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.AreaChart, { data, margin, children: [
704
843
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: series.map((s, index) => {
705
844
  const color = getChartColor(index, s.color);
@@ -820,6 +959,22 @@ var ScatterChart = React17__namespace.forwardRef(
820
959
  const tooltipProps = resolveTooltipProps(tooltip);
821
960
  const isAnimated = live ? false : animate;
822
961
  const hasZ = series.some((s) => s.zDataKey);
962
+ const zDataKey = series.find((s) => s.zDataKey)?.zDataKey;
963
+ const tableColumns = React17__namespace.useMemo(() => {
964
+ const cols = [
965
+ { key: "__series", label: "Series" },
966
+ { key: xAxis.dataKey, label: xAxis.name ?? xAxis.label ?? xAxis.dataKey },
967
+ { key: "y", label: yAxis?.name ?? yAxis?.label ?? "y" }
968
+ ];
969
+ if (zDataKey) cols.push({ key: zDataKey, label: zDataKey });
970
+ return cols;
971
+ }, [xAxis.dataKey, xAxis.name, xAxis.label, yAxis?.name, yAxis?.label, zDataKey]);
972
+ const tableData = React17__namespace.useMemo(
973
+ () => series.flatMap(
974
+ (s) => (s.data ?? []).map((point) => ({ __series: s.name, ...point }))
975
+ ),
976
+ [series]
977
+ );
823
978
  return /* @__PURE__ */ jsxRuntime.jsx(
824
979
  ChartContainer,
825
980
  {
@@ -828,6 +983,9 @@ var ScatterChart = React17__namespace.forwardRef(
828
983
  height,
829
984
  loading,
830
985
  empty: !series.some((s) => s.data?.length),
986
+ ariaLabel: "Scatter chart",
987
+ tableData,
988
+ tableColumns,
831
989
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.ScatterChart, { margin, children: [
832
990
  gridConfig && /* @__PURE__ */ jsxRuntime.jsx(
833
991
  recharts.CartesianGrid,
@@ -929,6 +1087,13 @@ var ComposedChart = React17__namespace.forwardRef(
929
1087
  const legendConfig = resolveLegendConfig(legend);
930
1088
  const tooltipProps = resolveTooltipProps(tooltip);
931
1089
  const isAnimated = live ? false : animate;
1090
+ const tableColumns = React17__namespace.useMemo(
1091
+ () => [
1092
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
1093
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
1094
+ ],
1095
+ [xAxis.dataKey, xAxis.label, series]
1096
+ );
932
1097
  return /* @__PURE__ */ jsxRuntime.jsx(
933
1098
  ChartContainer,
934
1099
  {
@@ -937,6 +1102,9 @@ var ComposedChart = React17__namespace.forwardRef(
937
1102
  height,
938
1103
  loading,
939
1104
  empty: !data?.length,
1105
+ ariaLabel: "Composed chart",
1106
+ tableData: data,
1107
+ tableColumns,
940
1108
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.ComposedChart, { data, margin, children: [
941
1109
  gridConfig && /* @__PURE__ */ jsxRuntime.jsx(
942
1110
  recharts.CartesianGrid,
@@ -1115,6 +1283,13 @@ var WaterfallChart = React17__namespace.forwardRef(
1115
1283
  const posColor = positiveColor ?? getSemanticColor("positive");
1116
1284
  const negColor = negativeColor ?? getSemanticColor("negative");
1117
1285
  const totColor = totalColor ?? "hsl(var(--chart-3))";
1286
+ const tableColumns = React17__namespace.useMemo(
1287
+ () => [
1288
+ { key: "name", label: xAxis?.label ?? "Name" },
1289
+ { key: "value", label: yAxis?.label ?? "Value" }
1290
+ ],
1291
+ [xAxis?.label, yAxis?.label]
1292
+ );
1118
1293
  return /* @__PURE__ */ jsxRuntime.jsx(
1119
1294
  ChartContainer,
1120
1295
  {
@@ -1123,6 +1298,9 @@ var WaterfallChart = React17__namespace.forwardRef(
1123
1298
  height,
1124
1299
  loading,
1125
1300
  empty: !data?.length,
1301
+ ariaLabel: "Waterfall chart",
1302
+ tableData: data,
1303
+ tableColumns,
1126
1304
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.BarChart, { data: rows, margin, children: [
1127
1305
  /* @__PURE__ */ jsxRuntime.jsx(
1128
1306
  recharts.CartesianGrid,
@@ -1251,6 +1429,12 @@ var PieChart = React17__namespace.forwardRef(
1251
1429
  height,
1252
1430
  loading,
1253
1431
  empty: !data?.length,
1432
+ ariaLabel: "Pie chart",
1433
+ tableData: data,
1434
+ tableColumns: [
1435
+ { key: "name", label: "Name" },
1436
+ { key: "value", label: "Value" }
1437
+ ],
1254
1438
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.PieChart, { margin, children: [
1255
1439
  /* @__PURE__ */ jsxRuntime.jsx(
1256
1440
  recharts.Pie,
@@ -1323,6 +1507,12 @@ var DonutChart = React17__namespace.forwardRef(
1323
1507
  height,
1324
1508
  loading,
1325
1509
  empty: !data?.length,
1510
+ ariaLabel: centerLabel ?? "Donut chart",
1511
+ tableData: data,
1512
+ tableColumns: [
1513
+ { key: "name", label: "Name" },
1514
+ { key: "value", label: "Value" }
1515
+ ],
1326
1516
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.PieChart, { margin, children: [
1327
1517
  /* @__PURE__ */ jsxRuntime.jsx(
1328
1518
  recharts.Pie,
@@ -1415,6 +1605,13 @@ var RadarChart = React17__namespace.forwardRef(
1415
1605
  const legendConfig = resolveLegendConfig(legend);
1416
1606
  const tooltipProps = resolveTooltipProps(tooltip);
1417
1607
  const isAnimated = live ? false : animate;
1608
+ const tableColumns = React17__namespace.useMemo(
1609
+ () => [
1610
+ { key: categoryKey, label: categoryKey },
1611
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
1612
+ ],
1613
+ [categoryKey, series]
1614
+ );
1418
1615
  return /* @__PURE__ */ jsxRuntime.jsx(
1419
1616
  ChartContainer,
1420
1617
  {
@@ -1423,6 +1620,9 @@ var RadarChart = React17__namespace.forwardRef(
1423
1620
  height,
1424
1621
  loading,
1425
1622
  empty: !data?.length,
1623
+ ariaLabel: "Radar chart",
1624
+ tableData: data,
1625
+ tableColumns,
1426
1626
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.RadarChart, { data, margin, cx: "50%", cy: "50%", children: [
1427
1627
  /* @__PURE__ */ jsxRuntime.jsx(recharts.PolarGrid, { stroke: "hsl(var(--divider))" }),
1428
1628
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1508,6 +1708,12 @@ var RadialBarChart = React17__namespace.forwardRef(
1508
1708
  height,
1509
1709
  loading,
1510
1710
  empty: !data?.length,
1711
+ ariaLabel: "Radial bar chart",
1712
+ tableData: data,
1713
+ tableColumns: [
1714
+ { key: "name", label: "Name" },
1715
+ { key: "value", label: "Value" }
1716
+ ],
1511
1717
  children: /* @__PURE__ */ jsxRuntime.jsxs(
1512
1718
  recharts.RadialBarChart,
1513
1719
  {
@@ -1625,6 +1831,14 @@ var SankeyChart = React17__namespace.forwardRef(
1625
1831
  loading
1626
1832
  }, ref) => {
1627
1833
  const tooltipProps = resolveTooltipProps(tooltip);
1834
+ const tableData = React17__namespace.useMemo(
1835
+ () => (data?.links ?? []).map((link) => ({
1836
+ source: data.nodes[link.source]?.name ?? String(link.source),
1837
+ target: data.nodes[link.target]?.name ?? String(link.target),
1838
+ value: link.value
1839
+ })),
1840
+ [data]
1841
+ );
1628
1842
  return /* @__PURE__ */ jsxRuntime.jsx(
1629
1843
  ChartContainer,
1630
1844
  {
@@ -1633,6 +1847,13 @@ var SankeyChart = React17__namespace.forwardRef(
1633
1847
  height,
1634
1848
  loading,
1635
1849
  empty: !data?.nodes?.length,
1850
+ ariaLabel: "Sankey diagram",
1851
+ tableData,
1852
+ tableColumns: [
1853
+ { key: "source", label: "Source" },
1854
+ { key: "target", label: "Target" },
1855
+ { key: "value", label: "Value" }
1856
+ ],
1636
1857
  children: /* @__PURE__ */ jsxRuntime.jsx(
1637
1858
  recharts.Sankey,
1638
1859
  {
@@ -1650,6 +1871,20 @@ var SankeyChart = React17__namespace.forwardRef(
1650
1871
  }
1651
1872
  );
1652
1873
  SankeyChart.displayName = "SankeyChart";
1874
+ function flattenTreemap(nodes) {
1875
+ const rows = [];
1876
+ const walk = (items) => {
1877
+ for (const item of items) {
1878
+ if (item.children && item.children.length > 0) {
1879
+ walk(item.children);
1880
+ } else {
1881
+ rows.push({ name: item.name, value: item.value ?? null });
1882
+ }
1883
+ }
1884
+ };
1885
+ walk(nodes);
1886
+ return rows;
1887
+ }
1653
1888
  function TreemapContent({
1654
1889
  x,
1655
1890
  y,
@@ -1663,6 +1898,7 @@ function TreemapContent({
1663
1898
  const fill = color ?? getChartColor(index);
1664
1899
  const showLabel = width > 40 && height > 20;
1665
1900
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
1901
+ name && /* @__PURE__ */ jsxRuntime.jsx("title", { children: name }),
1666
1902
  /* @__PURE__ */ jsxRuntime.jsx(
1667
1903
  "rect",
1668
1904
  {
@@ -1703,11 +1939,11 @@ var TreemapChart = React17__namespace.forwardRef(
1703
1939
  live = false,
1704
1940
  className,
1705
1941
  height = "100%",
1706
- margin,
1707
1942
  loading
1708
1943
  }, ref) => {
1709
1944
  const tooltipProps = resolveTooltipProps(tooltip);
1710
1945
  const isAnimated = live ? false : animate;
1946
+ const tableData = React17__namespace.useMemo(() => flattenTreemap(data), [data]);
1711
1947
  return /* @__PURE__ */ jsxRuntime.jsx(
1712
1948
  ChartContainer,
1713
1949
  {
@@ -1716,6 +1952,12 @@ var TreemapChart = React17__namespace.forwardRef(
1716
1952
  height,
1717
1953
  loading,
1718
1954
  empty: !data?.length,
1955
+ ariaLabel: "Treemap chart",
1956
+ tableData,
1957
+ tableColumns: [
1958
+ { key: "name", label: "Name" },
1959
+ { key: "value", label: "Value" }
1960
+ ],
1719
1961
  children: /* @__PURE__ */ jsxRuntime.jsx(
1720
1962
  recharts.Treemap,
1721
1963
  {
@@ -1755,6 +1997,12 @@ var FunnelChart = React17__namespace.forwardRef(
1755
1997
  height,
1756
1998
  loading,
1757
1999
  empty: !data?.length,
2000
+ ariaLabel: "Funnel chart",
2001
+ tableData: data,
2002
+ tableColumns: [
2003
+ { key: "name", label: "Stage" },
2004
+ { key: "value", label: "Value" }
2005
+ ],
1758
2006
  children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.FunnelChart, { margin, children: [
1759
2007
  /* @__PURE__ */ jsxRuntime.jsxs(
1760
2008
  recharts.Funnel,
@@ -1995,9 +2243,13 @@ var GanttChart = React17__namespace.forwardRef(
1995
2243
  const color = task.color ?? getChartColor(i);
1996
2244
  const isHovered = hoveredId === task.id;
1997
2245
  const progress = task.progress ?? 0;
2246
+ const taskLabel = `${task.name}, ${task.start.toLocaleDateString()} to ${task.end.toLocaleDateString()}${task.progress != null ? `, ${Math.round((task.progress ?? 0) * 100)}% complete` : ""}`;
1998
2247
  return /* @__PURE__ */ jsxRuntime.jsxs(
1999
2248
  "g",
2000
2249
  {
2250
+ tabIndex: 0,
2251
+ role: onTaskClick ? "button" : "img",
2252
+ "aria-label": taskLabel,
2001
2253
  onMouseEnter: (e) => {
2002
2254
  setHoveredId(task.id);
2003
2255
  const rect = containerRef.current?.getBoundingClientRect();
@@ -2011,7 +2263,22 @@ var GanttChart = React17__namespace.forwardRef(
2011
2263
  setHoveredId(null);
2012
2264
  setTooltipInfo(null);
2013
2265
  },
2266
+ onFocus: () => {
2267
+ setHoveredId(task.id);
2268
+ setTooltipInfo({ task, x: barX, y: barY });
2269
+ },
2270
+ onBlur: () => {
2271
+ setHoveredId(null);
2272
+ setTooltipInfo(null);
2273
+ },
2274
+ onKeyDown: (e) => {
2275
+ if (onTaskClick && (e.key === "Enter" || e.key === " ")) {
2276
+ e.preventDefault();
2277
+ onTaskClick(task);
2278
+ }
2279
+ },
2014
2280
  onClick: () => onTaskClick?.(task),
2281
+ className: "focus:outline-none focus-visible:[outline:2px_solid_hsl(var(--ring))]",
2015
2282
  style: { cursor: onTaskClick ? "pointer" : "default" },
2016
2283
  children: [
2017
2284
  i % 2 === 0 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -2220,6 +2487,7 @@ var HeatmapChart = React17__namespace.forwardRef(
2220
2487
  }, ref) {
2221
2488
  const containerRef = React17__namespace.useRef(null);
2222
2489
  const [hoveredCell, setHoveredCell] = React17__namespace.useState(null);
2490
+ const [focusedKey, setFocusedKey] = React17__namespace.useState(null);
2223
2491
  const [tooltipPos, setTooltipPos] = React17__namespace.useState({ x: 0, y: 0 });
2224
2492
  const { valueMap, minVal, maxVal } = React17__namespace.useMemo(() => {
2225
2493
  const map = /* @__PURE__ */ new Map();
@@ -2302,9 +2570,15 @@ var HeatmapChart = React17__namespace.forwardRef(
2302
2570
  const cx = labelColWidth + xi * cellSize;
2303
2571
  const cy = 28 + yi * cellSize;
2304
2572
  const cellData = { x: xLabel, y: yLabel, value: val ?? 0 };
2573
+ const isInteractive = !!onCellClick;
2574
+ const valueText = val != null ? valueFormatter ? valueFormatter(val) : val.toLocaleString() : "no value";
2575
+ const cellLabel = `${yLabel}, ${xLabel}: ${valueText}`;
2305
2576
  return /* @__PURE__ */ jsxRuntime.jsxs(
2306
2577
  "g",
2307
2578
  {
2579
+ role: isInteractive ? "button" : "img",
2580
+ tabIndex: 0,
2581
+ "aria-label": cellLabel,
2308
2582
  onMouseEnter: (e) => {
2309
2583
  setHoveredCell(cellData);
2310
2584
  const rect = containerRef.current?.getBoundingClientRect();
@@ -2314,8 +2588,26 @@ var HeatmapChart = React17__namespace.forwardRef(
2314
2588
  });
2315
2589
  },
2316
2590
  onMouseLeave: () => setHoveredCell(null),
2591
+ onFocus: () => {
2592
+ setHoveredCell(cellData);
2593
+ setFocusedKey(key);
2594
+ setTooltipPos({
2595
+ x: cx + cellSize / 2,
2596
+ y: cy
2597
+ });
2598
+ },
2599
+ onBlur: () => {
2600
+ setHoveredCell(null);
2601
+ setFocusedKey(null);
2602
+ },
2317
2603
  onClick: () => onCellClick?.(cellData),
2318
- style: { cursor: onCellClick ? "pointer" : "default" },
2604
+ onKeyDown: (e) => {
2605
+ if (isInteractive && (e.key === "Enter" || e.key === " ")) {
2606
+ e.preventDefault();
2607
+ onCellClick?.(cellData);
2608
+ }
2609
+ },
2610
+ style: { cursor: onCellClick ? "pointer" : "default", outline: "none" },
2319
2611
  children: [
2320
2612
  /* @__PURE__ */ jsxRuntime.jsx(
2321
2613
  "rect",
@@ -2327,8 +2619,8 @@ var HeatmapChart = React17__namespace.forwardRef(
2327
2619
  rx: 3,
2328
2620
  fill: val != null ? interpolateColor(t, colorScale) : "hsl(var(--muted))",
2329
2621
  fillOpacity: val != null ? 0.85 : 0.3,
2330
- stroke: hoveredCell?.x === xLabel && hoveredCell?.y === yLabel ? "hsl(var(--foreground))" : "none",
2331
- strokeWidth: 1.5
2622
+ stroke: focusedKey === key ? "hsl(var(--ring))" : hoveredCell?.x === xLabel && hoveredCell?.y === yLabel ? "hsl(var(--foreground))" : "none",
2623
+ strokeWidth: focusedKey === key ? 2.5 : 1.5
2332
2624
  }
2333
2625
  ),
2334
2626
  showValues && val != null && cellSize >= 32 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -2958,6 +3250,18 @@ var labelStyle = {
2958
3250
  marginBottom: 3,
2959
3251
  display: "block"
2960
3252
  };
3253
+ var FOCUS_STYLE_ID = "opti-chart-builder-focus";
3254
+ var focusStyles = `
3255
+ .opti-chart-builder-control:focus-visible {
3256
+ outline: 2px solid hsl(var(--ring));
3257
+ outline-offset: 1px;
3258
+ border-radius: 4px;
3259
+ }
3260
+ `;
3261
+ function FocusStyles() {
3262
+ return /* @__PURE__ */ jsxRuntime.jsx("style", { id: FOCUS_STYLE_ID, children: focusStyles });
3263
+ }
3264
+ var FOCUS_CLASS = "opti-chart-builder-control";
2961
3265
  var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
2962
3266
  columns,
2963
3267
  data,
@@ -3000,6 +3304,7 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3000
3304
  className: core.cn("flex gap-4", className),
3001
3305
  style: { minHeight: 400 },
3002
3306
  children: [
3307
+ /* @__PURE__ */ jsxRuntime.jsx(FocusStyles, {}),
3003
3308
  /* @__PURE__ */ jsxRuntime.jsxs(
3004
3309
  "div",
3005
3310
  {
@@ -3014,10 +3319,12 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3014
3319
  children: [
3015
3320
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "hsl(var(--foreground))" }, children: "Chart Builder" }),
3016
3321
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3017
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Chart Type" }),
3322
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "cb-chart-type", style: labelStyle, children: "Chart Type" }),
3018
3323
  /* @__PURE__ */ jsxRuntime.jsx(
3019
3324
  "select",
3020
3325
  {
3326
+ id: "cb-chart-type",
3327
+ className: FOCUS_CLASS,
3021
3328
  style: selectStyle,
3022
3329
  value: config.type,
3023
3330
  onChange: (e) => setConfig((prev) => ({ ...prev, type: e.target.value })),
@@ -3026,10 +3333,12 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3026
3333
  )
3027
3334
  ] }),
3028
3335
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3029
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Category Axis" }),
3336
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "cb-category-axis", style: labelStyle, children: "Category Axis" }),
3030
3337
  /* @__PURE__ */ jsxRuntime.jsx(
3031
3338
  "select",
3032
3339
  {
3340
+ id: "cb-category-axis",
3341
+ className: FOCUS_CLASS,
3033
3342
  style: selectStyle,
3034
3343
  value: config.categoryKey,
3035
3344
  onChange: (e) => setConfig((prev) => ({ ...prev, categoryKey: e.target.value })),
@@ -3037,8 +3346,8 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3037
3346
  }
3038
3347
  )
3039
3348
  ] }),
3040
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3041
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Series" }),
3349
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "group", "aria-label": "Series", children: [
3350
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: labelStyle, children: "Series" }),
3042
3351
  config.series.map((s, i) => /* @__PURE__ */ jsxRuntime.jsxs(
3043
3352
  "div",
3044
3353
  {
@@ -3048,6 +3357,8 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3048
3357
  /* @__PURE__ */ jsxRuntime.jsx(
3049
3358
  "select",
3050
3359
  {
3360
+ className: FOCUS_CLASS,
3361
+ "aria-label": `Series ${i + 1} data column`,
3051
3362
  style: { ...selectStyle, flex: 1 },
3052
3363
  value: s.dataKey,
3053
3364
  onChange: (e) => updateSeries(i, "dataKey", e.target.value),
@@ -3057,6 +3368,9 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3057
3368
  /* @__PURE__ */ jsxRuntime.jsx(
3058
3369
  "button",
3059
3370
  {
3371
+ type: "button",
3372
+ "aria-label": `Remove series ${i + 1}`,
3373
+ className: FOCUS_CLASS,
3060
3374
  onClick: () => removeSeries(i),
3061
3375
  style: {
3062
3376
  padding: "2px 6px",
@@ -3078,6 +3392,8 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3078
3392
  /* @__PURE__ */ jsxRuntime.jsx(
3079
3393
  "button",
3080
3394
  {
3395
+ type: "button",
3396
+ className: FOCUS_CLASS,
3081
3397
  onClick: addSeries,
3082
3398
  style: {
3083
3399
  width: "100%",
@@ -3095,10 +3411,12 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3095
3411
  )
3096
3412
  ] }),
3097
3413
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3098
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Legend" }),
3414
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "cb-legend", style: labelStyle, children: "Legend" }),
3099
3415
  /* @__PURE__ */ jsxRuntime.jsx(
3100
3416
  "select",
3101
3417
  {
3418
+ id: "cb-legend",
3419
+ className: FOCUS_CLASS,
3102
3420
  style: selectStyle,
3103
3421
  value: config.legend,
3104
3422
  onChange: (e) => setConfig((prev) => ({ ...prev, legend: e.target.value })),
@@ -3112,6 +3430,8 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3112
3430
  {
3113
3431
  type: "checkbox",
3114
3432
  id: "grid-toggle",
3433
+ "aria-label": "Show Grid",
3434
+ className: FOCUS_CLASS,
3115
3435
  checked: config.grid,
3116
3436
  onChange: (e) => setConfig((prev) => ({ ...prev, grid: e.target.checked }))
3117
3437
  }
@@ -3124,6 +3444,8 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3124
3444
  {
3125
3445
  type: "checkbox",
3126
3446
  id: "animate-toggle",
3447
+ "aria-label": "Animate",
3448
+ className: FOCUS_CLASS,
3127
3449
  checked: config.animate,
3128
3450
  onChange: (e) => setConfig((prev) => ({ ...prev, animate: e.target.checked }))
3129
3451
  }
@@ -3133,6 +3455,8 @@ var ChartBuilder = React17__namespace.memo(function ChartBuilder2({
3133
3455
  onSave && /* @__PURE__ */ jsxRuntime.jsx(
3134
3456
  "button",
3135
3457
  {
3458
+ type: "button",
3459
+ className: FOCUS_CLASS,
3136
3460
  onClick: () => onSave(config),
3137
3461
  style: {
3138
3462
  marginTop: 8,