@optilogic/charts 1.3.10 → 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.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as React17 from 'react';
2
2
  import { useState, useRef, useCallback, useEffect } from 'react';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
- import { ResponsiveContainer, LineChart as LineChart$1, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Line, BarChart as BarChart$1, Bar, AreaChart as AreaChart$1, Area, ScatterChart as ScatterChart$1, ZAxis, Scatter, ComposedChart as ComposedChart$1, ReferenceLine, Cell, PieChart as PieChart$1, Pie, RadarChart as RadarChart$1, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, RadialBarChart as RadialBarChart$1, RadialBar, Sankey, Treemap, FunnelChart as FunnelChart$1, Funnel, LabelList, Layer, Rectangle } from 'recharts';
5
- import { cn } from '@optilogic/core';
4
+ import { ResponsiveContainer, LineChart as LineChart$1, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Line, BarChart as BarChart$1, Bar, Cell, AreaChart as AreaChart$1, Area, ScatterChart as ScatterChart$1, ZAxis, Scatter, ComposedChart as ComposedChart$1, ReferenceLine, PieChart as PieChart$1, Pie, RadarChart as RadarChart$1, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, RadialBarChart as RadialBarChart$1, RadialBar, Sankey, Treemap, FunnelChart as FunnelChart$1, Funnel, LabelList, Layer, Rectangle } from 'recharts';
5
+ import { cn, VisuallyHidden, computeScaleDomain, resolveCellFormat, resolveFormatColor } from '@optilogic/core';
6
6
 
7
7
  // src/shared/colors.ts
8
8
  var CHART_COLORS = [
@@ -211,6 +211,13 @@ function resolveLegendConfig(legend) {
211
211
  if (legend === true) return { position: "top", align: "right" };
212
212
  return legend;
213
213
  }
214
+ function formatCell(value) {
215
+ if (value == null) return "";
216
+ if (typeof value === "number") return value.toLocaleString();
217
+ if (value instanceof Date) return value.toLocaleString();
218
+ if (typeof value === "object") return JSON.stringify(value);
219
+ return String(value);
220
+ }
214
221
  var ChartContainer = React17.forwardRef(function ChartContainer2({
215
222
  children,
216
223
  className,
@@ -219,8 +226,18 @@ var ChartContainer = React17.forwardRef(function ChartContainer2({
219
226
  empty,
220
227
  emptyMessage = "No data available",
221
228
  title,
222
- description
229
+ description,
230
+ ariaLabel,
231
+ ariaDescription,
232
+ tableData,
233
+ tableColumns,
234
+ onRowActivate,
235
+ rowActionLabel
223
236
  }, ref) {
237
+ const descriptionId = React17.useId();
238
+ const accessibleName = ariaLabel ?? title;
239
+ const accessibleDescription = ariaDescription ?? description;
240
+ const hasTable = !loading && !empty && !!tableData && tableData.length > 0 && !!tableColumns && tableColumns.length > 0;
224
241
  return /* @__PURE__ */ jsxs("div", { ref, className: cn("w-full", className), style: { height }, children: [
225
242
  (title || description) && /* @__PURE__ */ jsxs("div", { style: { marginBottom: 8 }, children: [
226
243
  title && /* @__PURE__ */ jsx(
@@ -283,12 +300,51 @@ var ChartContainer = React17.forwardRef(function ChartContainer2({
283
300
  },
284
301
  children: emptyMessage
285
302
  }
286
- ) : /* @__PURE__ */ jsx(
287
- ResponsiveContainer,
303
+ ) : /* @__PURE__ */ jsxs(
304
+ "div",
288
305
  {
289
- width: "100%",
290
- height: title || description ? "calc(100% - 40px)" : "100%",
291
- children
306
+ role: "img",
307
+ "aria-label": accessibleName,
308
+ "aria-describedby": accessibleDescription ? descriptionId : void 0,
309
+ className: "relative",
310
+ style: {
311
+ width: "100%",
312
+ height: title || description ? "calc(100% - 40px)" : "100%"
313
+ },
314
+ children: [
315
+ accessibleDescription && /* @__PURE__ */ jsx(VisuallyHidden, { as: "span", id: descriptionId, children: accessibleDescription }),
316
+ /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children }),
317
+ hasTable && /* @__PURE__ */ jsx(VisuallyHidden, { as: "div", children: /* @__PURE__ */ jsxs("table", { children: [
318
+ accessibleName && /* @__PURE__ */ jsx("caption", { children: accessibleName }),
319
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: tableColumns.map((col) => /* @__PURE__ */ jsx("th", { scope: "col", children: col.label }, col.key)) }) }),
320
+ /* @__PURE__ */ jsx("tbody", { children: tableData.map((row, rowIndex) => {
321
+ const record = row;
322
+ return /* @__PURE__ */ jsx("tr", { children: tableColumns.map((col) => /* @__PURE__ */ jsx("td", { children: formatCell(record[col.key]) }, col.key)) }, rowIndex);
323
+ }) })
324
+ ] }) }),
325
+ hasTable && onRowActivate && /* @__PURE__ */ jsx(
326
+ "nav",
327
+ {
328
+ "aria-label": `${accessibleName ?? "Chart"} data point links`,
329
+ 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",
330
+ children: tableData.map((row, rowIndex) => {
331
+ const record = row;
332
+ const firstKey = tableColumns[0]?.key;
333
+ const label = rowActionLabel?.(row, rowIndex) ?? (firstKey ? formatCell(record[firstKey]) : `Row ${rowIndex + 1}`);
334
+ return /* @__PURE__ */ jsx(
335
+ "button",
336
+ {
337
+ type: "button",
338
+ onClick: () => onRowActivate(row, rowIndex),
339
+ 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",
340
+ children: label
341
+ },
342
+ rowIndex
343
+ );
344
+ })
345
+ }
346
+ )
347
+ ]
292
348
  }
293
349
  )
294
350
  ] });
@@ -341,6 +397,14 @@ function useLiveData(config = {}) {
341
397
  }
342
398
 
343
399
  // src/shared/formatters.ts
400
+ function toCellValue(value) {
401
+ if (value == null) return null;
402
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
403
+ return value;
404
+ }
405
+ if (value instanceof Date) return value;
406
+ return null;
407
+ }
344
408
  var compactThresholds = [
345
409
  [1e9, "B"],
346
410
  [1e6, "M"],
@@ -407,6 +471,13 @@ var LineChart = React17.forwardRef(
407
471
  const legendConfig = resolveLegendConfig(legend);
408
472
  const tooltipProps = resolveTooltipProps(tooltip);
409
473
  const isAnimated = live ? false : animate;
474
+ const tableColumns = React17.useMemo(
475
+ () => [
476
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
477
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
478
+ ],
479
+ [xAxis.dataKey, xAxis.label, series]
480
+ );
410
481
  return /* @__PURE__ */ jsx(
411
482
  ChartContainer,
412
483
  {
@@ -415,6 +486,9 @@ var LineChart = React17.forwardRef(
415
486
  height,
416
487
  loading,
417
488
  empty: !data?.length,
489
+ ariaLabel: "Line chart",
490
+ tableData: data,
491
+ tableColumns,
418
492
  children: /* @__PURE__ */ jsxs(LineChart$1, { data, margin, children: [
419
493
  gridConfig && /* @__PURE__ */ jsx(
420
494
  CartesianGrid,
@@ -509,7 +583,8 @@ var BarChart = React17.forwardRef(
509
583
  className,
510
584
  height = "100%",
511
585
  margin = { top: 8, right: 12, left: 0, bottom: 4 },
512
- loading
586
+ loading,
587
+ onDataPointClick
513
588
  }, ref) => {
514
589
  const gridConfig = React17.useMemo(() => {
515
590
  if (grid === false) return null;
@@ -520,6 +595,36 @@ var BarChart = React17.forwardRef(
520
595
  const tooltipProps = resolveTooltipProps(tooltip);
521
596
  const isAnimated = live ? false : animate;
522
597
  const isHorizontal = layout === "horizontal";
598
+ const tableColumns = React17.useMemo(
599
+ () => [
600
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
601
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
602
+ ],
603
+ [xAxis.dataKey, xAxis.label, series]
604
+ );
605
+ const seriesScaleDomains = React17.useMemo(() => {
606
+ const domains = {};
607
+ for (const s of series) {
608
+ const cf = s.conditionalFormat;
609
+ if (cf?.type === "colorScale" && !cf.domain) {
610
+ domains[s.dataKey] = computeScaleDomain(
611
+ data.map((d) => toCellValue(d[s.dataKey]))
612
+ );
613
+ }
614
+ }
615
+ return domains;
616
+ }, [series, data]);
617
+ const resolveBarFill = (s, datum, fallback) => {
618
+ if (!s.conditionalFormat) return fallback;
619
+ const fmt = resolveCellFormat(
620
+ s.conditionalFormat,
621
+ toCellValue(datum[s.dataKey]),
622
+ datum,
623
+ seriesScaleDomains[s.dataKey]
624
+ );
625
+ const color = fmt?.background ?? fmt?.color;
626
+ return color ? resolveFormatColor(color) : fallback;
627
+ };
523
628
  return /* @__PURE__ */ jsx(
524
629
  ChartContainer,
525
630
  {
@@ -528,6 +633,13 @@ var BarChart = React17.forwardRef(
528
633
  height,
529
634
  loading,
530
635
  empty: !data?.length,
636
+ ariaLabel: "Bar chart",
637
+ tableData: data,
638
+ tableColumns,
639
+ onRowActivate: onDataPointClick ? (_row, index) => {
640
+ const datum = data[index];
641
+ if (datum) onDataPointClick(datum, index);
642
+ } : void 0,
531
643
  children: /* @__PURE__ */ jsxs(
532
644
  BarChart$1,
533
645
  {
@@ -626,19 +738,36 @@ var BarChart = React17.forwardRef(
626
738
  content: /* @__PURE__ */ jsx(ChartLegendContent, {})
627
739
  }
628
740
  ),
629
- series.map((s, index) => /* @__PURE__ */ jsx(
630
- Bar,
631
- {
632
- dataKey: s.dataKey,
633
- name: s.name,
634
- fill: getChartColor(index, s.color),
635
- stackId: s.stackId,
636
- radius: s.radius ?? 0,
637
- isAnimationActive: isAnimated,
638
- animationDuration: isAnimated ? 300 : 0
639
- },
640
- s.dataKey
641
- ))
741
+ series.map((s, index) => {
742
+ const baseFill = getChartColor(index, s.color);
743
+ const interactive = !!onDataPointClick;
744
+ const perDatum = !!s.conditionalFormat || interactive;
745
+ return /* @__PURE__ */ jsx(
746
+ Bar,
747
+ {
748
+ dataKey: s.dataKey,
749
+ name: s.name,
750
+ fill: baseFill,
751
+ stackId: s.stackId,
752
+ radius: s.radius ?? 0,
753
+ isAnimationActive: isAnimated,
754
+ animationDuration: isAnimated ? 300 : 0,
755
+ onClick: interactive ? (_data, i) => {
756
+ const datum = data[i];
757
+ if (datum) onDataPointClick?.(datum, i);
758
+ } : void 0,
759
+ children: perDatum ? data.map((d, i) => /* @__PURE__ */ jsx(
760
+ Cell,
761
+ {
762
+ fill: resolveBarFill(s, d, baseFill),
763
+ cursor: interactive ? "pointer" : void 0
764
+ },
765
+ `${s.dataKey}-${i}`
766
+ )) : null
767
+ },
768
+ s.dataKey
769
+ );
770
+ })
642
771
  ]
643
772
  }
644
773
  )
@@ -671,6 +800,13 @@ var AreaChart = React17.forwardRef(
671
800
  const legendConfig = resolveLegendConfig(legend);
672
801
  const tooltipProps = resolveTooltipProps(tooltip);
673
802
  const isAnimated = live ? false : animate;
803
+ const tableColumns = React17.useMemo(
804
+ () => [
805
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
806
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
807
+ ],
808
+ [xAxis.dataKey, xAxis.label, series]
809
+ );
674
810
  return /* @__PURE__ */ jsx(
675
811
  ChartContainer,
676
812
  {
@@ -679,6 +815,9 @@ var AreaChart = React17.forwardRef(
679
815
  height,
680
816
  loading,
681
817
  empty: !data?.length,
818
+ ariaLabel: "Area chart",
819
+ tableData: data,
820
+ tableColumns,
682
821
  children: /* @__PURE__ */ jsxs(AreaChart$1, { data, margin, children: [
683
822
  /* @__PURE__ */ jsx("defs", { children: series.map((s, index) => {
684
823
  const color = getChartColor(index, s.color);
@@ -799,6 +938,22 @@ var ScatterChart = React17.forwardRef(
799
938
  const tooltipProps = resolveTooltipProps(tooltip);
800
939
  const isAnimated = live ? false : animate;
801
940
  const hasZ = series.some((s) => s.zDataKey);
941
+ const zDataKey = series.find((s) => s.zDataKey)?.zDataKey;
942
+ const tableColumns = React17.useMemo(() => {
943
+ const cols = [
944
+ { key: "__series", label: "Series" },
945
+ { key: xAxis.dataKey, label: xAxis.name ?? xAxis.label ?? xAxis.dataKey },
946
+ { key: "y", label: yAxis?.name ?? yAxis?.label ?? "y" }
947
+ ];
948
+ if (zDataKey) cols.push({ key: zDataKey, label: zDataKey });
949
+ return cols;
950
+ }, [xAxis.dataKey, xAxis.name, xAxis.label, yAxis?.name, yAxis?.label, zDataKey]);
951
+ const tableData = React17.useMemo(
952
+ () => series.flatMap(
953
+ (s) => (s.data ?? []).map((point) => ({ __series: s.name, ...point }))
954
+ ),
955
+ [series]
956
+ );
802
957
  return /* @__PURE__ */ jsx(
803
958
  ChartContainer,
804
959
  {
@@ -807,6 +962,9 @@ var ScatterChart = React17.forwardRef(
807
962
  height,
808
963
  loading,
809
964
  empty: !series.some((s) => s.data?.length),
965
+ ariaLabel: "Scatter chart",
966
+ tableData,
967
+ tableColumns,
810
968
  children: /* @__PURE__ */ jsxs(ScatterChart$1, { margin, children: [
811
969
  gridConfig && /* @__PURE__ */ jsx(
812
970
  CartesianGrid,
@@ -908,6 +1066,13 @@ var ComposedChart = React17.forwardRef(
908
1066
  const legendConfig = resolveLegendConfig(legend);
909
1067
  const tooltipProps = resolveTooltipProps(tooltip);
910
1068
  const isAnimated = live ? false : animate;
1069
+ const tableColumns = React17.useMemo(
1070
+ () => [
1071
+ { key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
1072
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
1073
+ ],
1074
+ [xAxis.dataKey, xAxis.label, series]
1075
+ );
911
1076
  return /* @__PURE__ */ jsx(
912
1077
  ChartContainer,
913
1078
  {
@@ -916,6 +1081,9 @@ var ComposedChart = React17.forwardRef(
916
1081
  height,
917
1082
  loading,
918
1083
  empty: !data?.length,
1084
+ ariaLabel: "Composed chart",
1085
+ tableData: data,
1086
+ tableColumns,
919
1087
  children: /* @__PURE__ */ jsxs(ComposedChart$1, { data, margin, children: [
920
1088
  gridConfig && /* @__PURE__ */ jsx(
921
1089
  CartesianGrid,
@@ -1094,6 +1262,13 @@ var WaterfallChart = React17.forwardRef(
1094
1262
  const posColor = positiveColor ?? getSemanticColor("positive");
1095
1263
  const negColor = negativeColor ?? getSemanticColor("negative");
1096
1264
  const totColor = totalColor ?? "hsl(var(--chart-3))";
1265
+ const tableColumns = React17.useMemo(
1266
+ () => [
1267
+ { key: "name", label: xAxis?.label ?? "Name" },
1268
+ { key: "value", label: yAxis?.label ?? "Value" }
1269
+ ],
1270
+ [xAxis?.label, yAxis?.label]
1271
+ );
1097
1272
  return /* @__PURE__ */ jsx(
1098
1273
  ChartContainer,
1099
1274
  {
@@ -1102,6 +1277,9 @@ var WaterfallChart = React17.forwardRef(
1102
1277
  height,
1103
1278
  loading,
1104
1279
  empty: !data?.length,
1280
+ ariaLabel: "Waterfall chart",
1281
+ tableData: data,
1282
+ tableColumns,
1105
1283
  children: /* @__PURE__ */ jsxs(BarChart$1, { data: rows, margin, children: [
1106
1284
  /* @__PURE__ */ jsx(
1107
1285
  CartesianGrid,
@@ -1230,6 +1408,12 @@ var PieChart = React17.forwardRef(
1230
1408
  height,
1231
1409
  loading,
1232
1410
  empty: !data?.length,
1411
+ ariaLabel: "Pie chart",
1412
+ tableData: data,
1413
+ tableColumns: [
1414
+ { key: "name", label: "Name" },
1415
+ { key: "value", label: "Value" }
1416
+ ],
1233
1417
  children: /* @__PURE__ */ jsxs(PieChart$1, { margin, children: [
1234
1418
  /* @__PURE__ */ jsx(
1235
1419
  Pie,
@@ -1302,6 +1486,12 @@ var DonutChart = React17.forwardRef(
1302
1486
  height,
1303
1487
  loading,
1304
1488
  empty: !data?.length,
1489
+ ariaLabel: centerLabel ?? "Donut chart",
1490
+ tableData: data,
1491
+ tableColumns: [
1492
+ { key: "name", label: "Name" },
1493
+ { key: "value", label: "Value" }
1494
+ ],
1305
1495
  children: /* @__PURE__ */ jsxs(PieChart$1, { margin, children: [
1306
1496
  /* @__PURE__ */ jsx(
1307
1497
  Pie,
@@ -1394,6 +1584,13 @@ var RadarChart = React17.forwardRef(
1394
1584
  const legendConfig = resolveLegendConfig(legend);
1395
1585
  const tooltipProps = resolveTooltipProps(tooltip);
1396
1586
  const isAnimated = live ? false : animate;
1587
+ const tableColumns = React17.useMemo(
1588
+ () => [
1589
+ { key: categoryKey, label: categoryKey },
1590
+ ...series.map((s) => ({ key: s.dataKey, label: s.name }))
1591
+ ],
1592
+ [categoryKey, series]
1593
+ );
1397
1594
  return /* @__PURE__ */ jsx(
1398
1595
  ChartContainer,
1399
1596
  {
@@ -1402,6 +1599,9 @@ var RadarChart = React17.forwardRef(
1402
1599
  height,
1403
1600
  loading,
1404
1601
  empty: !data?.length,
1602
+ ariaLabel: "Radar chart",
1603
+ tableData: data,
1604
+ tableColumns,
1405
1605
  children: /* @__PURE__ */ jsxs(RadarChart$1, { data, margin, cx: "50%", cy: "50%", children: [
1406
1606
  /* @__PURE__ */ jsx(PolarGrid, { stroke: "hsl(var(--divider))" }),
1407
1607
  /* @__PURE__ */ jsx(
@@ -1487,6 +1687,12 @@ var RadialBarChart = React17.forwardRef(
1487
1687
  height,
1488
1688
  loading,
1489
1689
  empty: !data?.length,
1690
+ ariaLabel: "Radial bar chart",
1691
+ tableData: data,
1692
+ tableColumns: [
1693
+ { key: "name", label: "Name" },
1694
+ { key: "value", label: "Value" }
1695
+ ],
1490
1696
  children: /* @__PURE__ */ jsxs(
1491
1697
  RadialBarChart$1,
1492
1698
  {
@@ -1604,6 +1810,14 @@ var SankeyChart = React17.forwardRef(
1604
1810
  loading
1605
1811
  }, ref) => {
1606
1812
  const tooltipProps = resolveTooltipProps(tooltip);
1813
+ const tableData = React17.useMemo(
1814
+ () => (data?.links ?? []).map((link) => ({
1815
+ source: data.nodes[link.source]?.name ?? String(link.source),
1816
+ target: data.nodes[link.target]?.name ?? String(link.target),
1817
+ value: link.value
1818
+ })),
1819
+ [data]
1820
+ );
1607
1821
  return /* @__PURE__ */ jsx(
1608
1822
  ChartContainer,
1609
1823
  {
@@ -1612,6 +1826,13 @@ var SankeyChart = React17.forwardRef(
1612
1826
  height,
1613
1827
  loading,
1614
1828
  empty: !data?.nodes?.length,
1829
+ ariaLabel: "Sankey diagram",
1830
+ tableData,
1831
+ tableColumns: [
1832
+ { key: "source", label: "Source" },
1833
+ { key: "target", label: "Target" },
1834
+ { key: "value", label: "Value" }
1835
+ ],
1615
1836
  children: /* @__PURE__ */ jsx(
1616
1837
  Sankey,
1617
1838
  {
@@ -1629,6 +1850,20 @@ var SankeyChart = React17.forwardRef(
1629
1850
  }
1630
1851
  );
1631
1852
  SankeyChart.displayName = "SankeyChart";
1853
+ function flattenTreemap(nodes) {
1854
+ const rows = [];
1855
+ const walk = (items) => {
1856
+ for (const item of items) {
1857
+ if (item.children && item.children.length > 0) {
1858
+ walk(item.children);
1859
+ } else {
1860
+ rows.push({ name: item.name, value: item.value ?? null });
1861
+ }
1862
+ }
1863
+ };
1864
+ walk(nodes);
1865
+ return rows;
1866
+ }
1632
1867
  function TreemapContent({
1633
1868
  x,
1634
1869
  y,
@@ -1642,6 +1877,7 @@ function TreemapContent({
1642
1877
  const fill = color ?? getChartColor(index);
1643
1878
  const showLabel = width > 40 && height > 20;
1644
1879
  return /* @__PURE__ */ jsxs("g", { children: [
1880
+ name && /* @__PURE__ */ jsx("title", { children: name }),
1645
1881
  /* @__PURE__ */ jsx(
1646
1882
  "rect",
1647
1883
  {
@@ -1682,11 +1918,11 @@ var TreemapChart = React17.forwardRef(
1682
1918
  live = false,
1683
1919
  className,
1684
1920
  height = "100%",
1685
- margin,
1686
1921
  loading
1687
1922
  }, ref) => {
1688
1923
  const tooltipProps = resolveTooltipProps(tooltip);
1689
1924
  const isAnimated = live ? false : animate;
1925
+ const tableData = React17.useMemo(() => flattenTreemap(data), [data]);
1690
1926
  return /* @__PURE__ */ jsx(
1691
1927
  ChartContainer,
1692
1928
  {
@@ -1695,6 +1931,12 @@ var TreemapChart = React17.forwardRef(
1695
1931
  height,
1696
1932
  loading,
1697
1933
  empty: !data?.length,
1934
+ ariaLabel: "Treemap chart",
1935
+ tableData,
1936
+ tableColumns: [
1937
+ { key: "name", label: "Name" },
1938
+ { key: "value", label: "Value" }
1939
+ ],
1698
1940
  children: /* @__PURE__ */ jsx(
1699
1941
  Treemap,
1700
1942
  {
@@ -1734,6 +1976,12 @@ var FunnelChart = React17.forwardRef(
1734
1976
  height,
1735
1977
  loading,
1736
1978
  empty: !data?.length,
1979
+ ariaLabel: "Funnel chart",
1980
+ tableData: data,
1981
+ tableColumns: [
1982
+ { key: "name", label: "Stage" },
1983
+ { key: "value", label: "Value" }
1984
+ ],
1737
1985
  children: /* @__PURE__ */ jsxs(FunnelChart$1, { margin, children: [
1738
1986
  /* @__PURE__ */ jsxs(
1739
1987
  Funnel,
@@ -1974,9 +2222,13 @@ var GanttChart = React17.forwardRef(
1974
2222
  const color = task.color ?? getChartColor(i);
1975
2223
  const isHovered = hoveredId === task.id;
1976
2224
  const progress = task.progress ?? 0;
2225
+ const taskLabel = `${task.name}, ${task.start.toLocaleDateString()} to ${task.end.toLocaleDateString()}${task.progress != null ? `, ${Math.round((task.progress ?? 0) * 100)}% complete` : ""}`;
1977
2226
  return /* @__PURE__ */ jsxs(
1978
2227
  "g",
1979
2228
  {
2229
+ tabIndex: 0,
2230
+ role: onTaskClick ? "button" : "img",
2231
+ "aria-label": taskLabel,
1980
2232
  onMouseEnter: (e) => {
1981
2233
  setHoveredId(task.id);
1982
2234
  const rect = containerRef.current?.getBoundingClientRect();
@@ -1990,7 +2242,22 @@ var GanttChart = React17.forwardRef(
1990
2242
  setHoveredId(null);
1991
2243
  setTooltipInfo(null);
1992
2244
  },
2245
+ onFocus: () => {
2246
+ setHoveredId(task.id);
2247
+ setTooltipInfo({ task, x: barX, y: barY });
2248
+ },
2249
+ onBlur: () => {
2250
+ setHoveredId(null);
2251
+ setTooltipInfo(null);
2252
+ },
2253
+ onKeyDown: (e) => {
2254
+ if (onTaskClick && (e.key === "Enter" || e.key === " ")) {
2255
+ e.preventDefault();
2256
+ onTaskClick(task);
2257
+ }
2258
+ },
1993
2259
  onClick: () => onTaskClick?.(task),
2260
+ className: "focus:outline-none focus-visible:[outline:2px_solid_hsl(var(--ring))]",
1994
2261
  style: { cursor: onTaskClick ? "pointer" : "default" },
1995
2262
  children: [
1996
2263
  i % 2 === 0 && /* @__PURE__ */ jsx(
@@ -2199,6 +2466,7 @@ var HeatmapChart = React17.forwardRef(
2199
2466
  }, ref) {
2200
2467
  const containerRef = React17.useRef(null);
2201
2468
  const [hoveredCell, setHoveredCell] = React17.useState(null);
2469
+ const [focusedKey, setFocusedKey] = React17.useState(null);
2202
2470
  const [tooltipPos, setTooltipPos] = React17.useState({ x: 0, y: 0 });
2203
2471
  const { valueMap, minVal, maxVal } = React17.useMemo(() => {
2204
2472
  const map = /* @__PURE__ */ new Map();
@@ -2281,9 +2549,15 @@ var HeatmapChart = React17.forwardRef(
2281
2549
  const cx = labelColWidth + xi * cellSize;
2282
2550
  const cy = 28 + yi * cellSize;
2283
2551
  const cellData = { x: xLabel, y: yLabel, value: val ?? 0 };
2552
+ const isInteractive = !!onCellClick;
2553
+ const valueText = val != null ? valueFormatter ? valueFormatter(val) : val.toLocaleString() : "no value";
2554
+ const cellLabel = `${yLabel}, ${xLabel}: ${valueText}`;
2284
2555
  return /* @__PURE__ */ jsxs(
2285
2556
  "g",
2286
2557
  {
2558
+ role: isInteractive ? "button" : "img",
2559
+ tabIndex: 0,
2560
+ "aria-label": cellLabel,
2287
2561
  onMouseEnter: (e) => {
2288
2562
  setHoveredCell(cellData);
2289
2563
  const rect = containerRef.current?.getBoundingClientRect();
@@ -2293,8 +2567,26 @@ var HeatmapChart = React17.forwardRef(
2293
2567
  });
2294
2568
  },
2295
2569
  onMouseLeave: () => setHoveredCell(null),
2570
+ onFocus: () => {
2571
+ setHoveredCell(cellData);
2572
+ setFocusedKey(key);
2573
+ setTooltipPos({
2574
+ x: cx + cellSize / 2,
2575
+ y: cy
2576
+ });
2577
+ },
2578
+ onBlur: () => {
2579
+ setHoveredCell(null);
2580
+ setFocusedKey(null);
2581
+ },
2296
2582
  onClick: () => onCellClick?.(cellData),
2297
- style: { cursor: onCellClick ? "pointer" : "default" },
2583
+ onKeyDown: (e) => {
2584
+ if (isInteractive && (e.key === "Enter" || e.key === " ")) {
2585
+ e.preventDefault();
2586
+ onCellClick?.(cellData);
2587
+ }
2588
+ },
2589
+ style: { cursor: onCellClick ? "pointer" : "default", outline: "none" },
2298
2590
  children: [
2299
2591
  /* @__PURE__ */ jsx(
2300
2592
  "rect",
@@ -2306,8 +2598,8 @@ var HeatmapChart = React17.forwardRef(
2306
2598
  rx: 3,
2307
2599
  fill: val != null ? interpolateColor(t, colorScale) : "hsl(var(--muted))",
2308
2600
  fillOpacity: val != null ? 0.85 : 0.3,
2309
- stroke: hoveredCell?.x === xLabel && hoveredCell?.y === yLabel ? "hsl(var(--foreground))" : "none",
2310
- strokeWidth: 1.5
2601
+ stroke: focusedKey === key ? "hsl(var(--ring))" : hoveredCell?.x === xLabel && hoveredCell?.y === yLabel ? "hsl(var(--foreground))" : "none",
2602
+ strokeWidth: focusedKey === key ? 2.5 : 1.5
2311
2603
  }
2312
2604
  ),
2313
2605
  showValues && val != null && cellSize >= 32 && /* @__PURE__ */ jsx(
@@ -2937,6 +3229,18 @@ var labelStyle = {
2937
3229
  marginBottom: 3,
2938
3230
  display: "block"
2939
3231
  };
3232
+ var FOCUS_STYLE_ID = "opti-chart-builder-focus";
3233
+ var focusStyles = `
3234
+ .opti-chart-builder-control:focus-visible {
3235
+ outline: 2px solid hsl(var(--ring));
3236
+ outline-offset: 1px;
3237
+ border-radius: 4px;
3238
+ }
3239
+ `;
3240
+ function FocusStyles() {
3241
+ return /* @__PURE__ */ jsx("style", { id: FOCUS_STYLE_ID, children: focusStyles });
3242
+ }
3243
+ var FOCUS_CLASS = "opti-chart-builder-control";
2940
3244
  var ChartBuilder = React17.memo(function ChartBuilder2({
2941
3245
  columns,
2942
3246
  data,
@@ -2979,6 +3283,7 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
2979
3283
  className: cn("flex gap-4", className),
2980
3284
  style: { minHeight: 400 },
2981
3285
  children: [
3286
+ /* @__PURE__ */ jsx(FocusStyles, {}),
2982
3287
  /* @__PURE__ */ jsxs(
2983
3288
  "div",
2984
3289
  {
@@ -2993,10 +3298,12 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
2993
3298
  children: [
2994
3299
  /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "hsl(var(--foreground))" }, children: "Chart Builder" }),
2995
3300
  /* @__PURE__ */ jsxs("div", { children: [
2996
- /* @__PURE__ */ jsx("label", { style: labelStyle, children: "Chart Type" }),
3301
+ /* @__PURE__ */ jsx("label", { htmlFor: "cb-chart-type", style: labelStyle, children: "Chart Type" }),
2997
3302
  /* @__PURE__ */ jsx(
2998
3303
  "select",
2999
3304
  {
3305
+ id: "cb-chart-type",
3306
+ className: FOCUS_CLASS,
3000
3307
  style: selectStyle,
3001
3308
  value: config.type,
3002
3309
  onChange: (e) => setConfig((prev) => ({ ...prev, type: e.target.value })),
@@ -3005,10 +3312,12 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3005
3312
  )
3006
3313
  ] }),
3007
3314
  /* @__PURE__ */ jsxs("div", { children: [
3008
- /* @__PURE__ */ jsx("label", { style: labelStyle, children: "Category Axis" }),
3315
+ /* @__PURE__ */ jsx("label", { htmlFor: "cb-category-axis", style: labelStyle, children: "Category Axis" }),
3009
3316
  /* @__PURE__ */ jsx(
3010
3317
  "select",
3011
3318
  {
3319
+ id: "cb-category-axis",
3320
+ className: FOCUS_CLASS,
3012
3321
  style: selectStyle,
3013
3322
  value: config.categoryKey,
3014
3323
  onChange: (e) => setConfig((prev) => ({ ...prev, categoryKey: e.target.value })),
@@ -3016,8 +3325,8 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3016
3325
  }
3017
3326
  )
3018
3327
  ] }),
3019
- /* @__PURE__ */ jsxs("div", { children: [
3020
- /* @__PURE__ */ jsx("label", { style: labelStyle, children: "Series" }),
3328
+ /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "Series", children: [
3329
+ /* @__PURE__ */ jsx("div", { style: labelStyle, children: "Series" }),
3021
3330
  config.series.map((s, i) => /* @__PURE__ */ jsxs(
3022
3331
  "div",
3023
3332
  {
@@ -3027,6 +3336,8 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3027
3336
  /* @__PURE__ */ jsx(
3028
3337
  "select",
3029
3338
  {
3339
+ className: FOCUS_CLASS,
3340
+ "aria-label": `Series ${i + 1} data column`,
3030
3341
  style: { ...selectStyle, flex: 1 },
3031
3342
  value: s.dataKey,
3032
3343
  onChange: (e) => updateSeries(i, "dataKey", e.target.value),
@@ -3036,6 +3347,9 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3036
3347
  /* @__PURE__ */ jsx(
3037
3348
  "button",
3038
3349
  {
3350
+ type: "button",
3351
+ "aria-label": `Remove series ${i + 1}`,
3352
+ className: FOCUS_CLASS,
3039
3353
  onClick: () => removeSeries(i),
3040
3354
  style: {
3041
3355
  padding: "2px 6px",
@@ -3057,6 +3371,8 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3057
3371
  /* @__PURE__ */ jsx(
3058
3372
  "button",
3059
3373
  {
3374
+ type: "button",
3375
+ className: FOCUS_CLASS,
3060
3376
  onClick: addSeries,
3061
3377
  style: {
3062
3378
  width: "100%",
@@ -3074,10 +3390,12 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3074
3390
  )
3075
3391
  ] }),
3076
3392
  /* @__PURE__ */ jsxs("div", { children: [
3077
- /* @__PURE__ */ jsx("label", { style: labelStyle, children: "Legend" }),
3393
+ /* @__PURE__ */ jsx("label", { htmlFor: "cb-legend", style: labelStyle, children: "Legend" }),
3078
3394
  /* @__PURE__ */ jsx(
3079
3395
  "select",
3080
3396
  {
3397
+ id: "cb-legend",
3398
+ className: FOCUS_CLASS,
3081
3399
  style: selectStyle,
3082
3400
  value: config.legend,
3083
3401
  onChange: (e) => setConfig((prev) => ({ ...prev, legend: e.target.value })),
@@ -3091,6 +3409,8 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3091
3409
  {
3092
3410
  type: "checkbox",
3093
3411
  id: "grid-toggle",
3412
+ "aria-label": "Show Grid",
3413
+ className: FOCUS_CLASS,
3094
3414
  checked: config.grid,
3095
3415
  onChange: (e) => setConfig((prev) => ({ ...prev, grid: e.target.checked }))
3096
3416
  }
@@ -3103,6 +3423,8 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3103
3423
  {
3104
3424
  type: "checkbox",
3105
3425
  id: "animate-toggle",
3426
+ "aria-label": "Animate",
3427
+ className: FOCUS_CLASS,
3106
3428
  checked: config.animate,
3107
3429
  onChange: (e) => setConfig((prev) => ({ ...prev, animate: e.target.checked }))
3108
3430
  }
@@ -3112,6 +3434,8 @@ var ChartBuilder = React17.memo(function ChartBuilder2({
3112
3434
  onSave && /* @__PURE__ */ jsx(
3113
3435
  "button",
3114
3436
  {
3437
+ type: "button",
3438
+ className: FOCUS_CLASS,
3115
3439
  onClick: () => onSave(config),
3116
3440
  style: {
3117
3441
  marginTop: 8,