@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 +353 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +355 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/cartesian/area-chart.tsx +11 -0
- package/src/cartesian/bar-chart.tsx +109 -12
- package/src/cartesian/composed-chart.tsx +11 -0
- package/src/cartesian/line-chart.tsx +12 -2
- package/src/cartesian/scatter-chart.tsx +22 -0
- package/src/cartesian/waterfall-chart.tsx +11 -0
- package/src/dashboard/chart-builder.tsx +47 -5
- package/src/dashboard/chart-renderer.tsx +1 -1
- package/src/index.ts +5 -1
- package/src/radial/donut-chart.tsx +6 -0
- package/src/radial/pie-chart.tsx +6 -0
- package/src/radial/radar-chart.tsx +11 -0
- package/src/radial/radial-bar-chart.tsx +6 -0
- package/src/shared/chart-container.tsx +133 -6
- package/src/shared/formatters.ts +20 -0
- package/src/specialized/funnel-chart.tsx +6 -0
- package/src/specialized/gantt-chart.tsx +25 -0
- package/src/specialized/heatmap-chart.tsx +40 -5
- package/src/specialized/sankey-chart.tsx +17 -0
- package/src/specialized/treemap-chart.tsx +31 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optilogic/charts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Chart components for Optilogic - LineChart and BarChart built on Recharts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@optilogic/core": "^
|
|
27
|
+
"@optilogic/core": "^2.0.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"build": "tsup",
|
|
61
61
|
"dev": "tsup --watch",
|
|
62
62
|
"typecheck": "tsc --noEmit",
|
|
63
|
-
"lint": "eslint src",
|
|
63
|
+
"lint": "eslint src --max-warnings 0",
|
|
64
64
|
"clean": "rm -rf dist .turbo"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -71,6 +71,14 @@ const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
|
|
|
71
71
|
const tooltipProps = resolveTooltipProps(tooltip);
|
|
72
72
|
const isAnimated = live ? false : animate;
|
|
73
73
|
|
|
74
|
+
const tableColumns = React.useMemo(
|
|
75
|
+
() => [
|
|
76
|
+
{ key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
|
|
77
|
+
...series.map((s) => ({ key: s.dataKey, label: s.name })),
|
|
78
|
+
],
|
|
79
|
+
[xAxis.dataKey, xAxis.label, series],
|
|
80
|
+
);
|
|
81
|
+
|
|
74
82
|
return (
|
|
75
83
|
<ChartContainer
|
|
76
84
|
ref={ref}
|
|
@@ -78,6 +86,9 @@ const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
|
|
|
78
86
|
height={height}
|
|
79
87
|
loading={loading}
|
|
80
88
|
empty={!data?.length}
|
|
89
|
+
ariaLabel="Area chart"
|
|
90
|
+
tableData={data}
|
|
91
|
+
tableColumns={tableColumns}
|
|
81
92
|
>
|
|
82
93
|
<RechartsAreaChart data={data} margin={margin}>
|
|
83
94
|
<defs>
|
|
@@ -2,16 +2,25 @@ import * as React from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
BarChart as RechartsBarChart,
|
|
4
4
|
Bar,
|
|
5
|
+
Cell,
|
|
5
6
|
XAxis,
|
|
6
7
|
YAxis,
|
|
7
8
|
CartesianGrid,
|
|
8
9
|
Tooltip,
|
|
9
10
|
Legend,
|
|
10
11
|
} from "recharts";
|
|
12
|
+
import {
|
|
13
|
+
resolveCellFormat,
|
|
14
|
+
resolveFormatColor,
|
|
15
|
+
computeScaleDomain,
|
|
16
|
+
type ConditionalFormat,
|
|
17
|
+
type ScaleDomain,
|
|
18
|
+
} from "@optilogic/core";
|
|
11
19
|
import { getChartColor } from "../shared/colors";
|
|
12
20
|
import { resolveTooltipProps } from "../shared/chart-tooltip";
|
|
13
21
|
import { ChartLegendContent, resolveLegendConfig } from "../shared/chart-legend";
|
|
14
22
|
import { ChartContainer } from "../shared/chart-container";
|
|
23
|
+
import { toCellValue } from "../shared/formatters";
|
|
15
24
|
import type {
|
|
16
25
|
BaseChartProps,
|
|
17
26
|
ChartXAxis,
|
|
@@ -27,6 +36,14 @@ export interface BarChartSeries {
|
|
|
27
36
|
color?: string;
|
|
28
37
|
stackId?: string;
|
|
29
38
|
radius?: number | [number, number, number, number];
|
|
39
|
+
/**
|
|
40
|
+
* Per-datum conditional formatting — a value-based color scale or ordered
|
|
41
|
+
* rules. The resolved `background` (or `color`) becomes each bar's fill, so
|
|
42
|
+
* bars follow the active theme. `className`-only formats do not apply to bars.
|
|
43
|
+
* Color is supplementary; the value stays available via the tooltip and the
|
|
44
|
+
* chart's accessible data table.
|
|
45
|
+
*/
|
|
46
|
+
conditionalFormat?: ConditionalFormat<Record<string, unknown>>;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
export interface BarChartProps extends BaseChartProps {
|
|
@@ -42,6 +59,12 @@ export interface BarChartProps extends BaseChartProps {
|
|
|
42
59
|
barGap?: number;
|
|
43
60
|
barCategoryGap?: number | string;
|
|
44
61
|
loading?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* "Visualization link" — called when a data point is activated (a bar is
|
|
64
|
+
* clicked, or its keyboard/screen-reader link is triggered). Router-agnostic:
|
|
65
|
+
* navigate to a dashboard or open an external URL from your handler.
|
|
66
|
+
*/
|
|
67
|
+
onDataPointClick?: (datum: Record<string, unknown>, index: number) => void;
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|
@@ -64,6 +87,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|
|
64
87
|
height = "100%",
|
|
65
88
|
margin = { top: 8, right: 12, left: 0, bottom: 4 },
|
|
66
89
|
loading,
|
|
90
|
+
onDataPointClick,
|
|
67
91
|
},
|
|
68
92
|
ref,
|
|
69
93
|
) => {
|
|
@@ -78,6 +102,45 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|
|
78
102
|
const isAnimated = live ? false : animate;
|
|
79
103
|
const isHorizontal = layout === "horizontal";
|
|
80
104
|
|
|
105
|
+
const tableColumns = React.useMemo(
|
|
106
|
+
() => [
|
|
107
|
+
{ key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
|
|
108
|
+
...series.map((s) => ({ key: s.dataKey, label: s.name })),
|
|
109
|
+
],
|
|
110
|
+
[xAxis.dataKey, xAxis.label, series],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Precompute numeric domains for auto-ranged color-scale series.
|
|
114
|
+
const seriesScaleDomains = React.useMemo(() => {
|
|
115
|
+
const domains: Record<string, ScaleDomain | undefined> = {};
|
|
116
|
+
for (const s of series) {
|
|
117
|
+
const cf = s.conditionalFormat;
|
|
118
|
+
if (cf?.type === "colorScale" && !cf.domain) {
|
|
119
|
+
domains[s.dataKey] = computeScaleDomain(
|
|
120
|
+
data.map((d) => toCellValue(d[s.dataKey])),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return domains;
|
|
125
|
+
}, [series, data]);
|
|
126
|
+
|
|
127
|
+
// Resolve a bar's fill from its series' conditional format (else the palette color).
|
|
128
|
+
const resolveBarFill = (
|
|
129
|
+
s: BarChartSeries,
|
|
130
|
+
datum: Record<string, unknown>,
|
|
131
|
+
fallback: string,
|
|
132
|
+
): string => {
|
|
133
|
+
if (!s.conditionalFormat) return fallback;
|
|
134
|
+
const fmt = resolveCellFormat(
|
|
135
|
+
s.conditionalFormat,
|
|
136
|
+
toCellValue(datum[s.dataKey]),
|
|
137
|
+
datum,
|
|
138
|
+
seriesScaleDomains[s.dataKey],
|
|
139
|
+
);
|
|
140
|
+
const color = fmt?.background ?? fmt?.color;
|
|
141
|
+
return color ? resolveFormatColor(color) : fallback;
|
|
142
|
+
};
|
|
143
|
+
|
|
81
144
|
return (
|
|
82
145
|
<ChartContainer
|
|
83
146
|
ref={ref}
|
|
@@ -85,6 +148,17 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|
|
85
148
|
height={height}
|
|
86
149
|
loading={loading}
|
|
87
150
|
empty={!data?.length}
|
|
151
|
+
ariaLabel="Bar chart"
|
|
152
|
+
tableData={data}
|
|
153
|
+
tableColumns={tableColumns}
|
|
154
|
+
onRowActivate={
|
|
155
|
+
onDataPointClick
|
|
156
|
+
? (_row, index) => {
|
|
157
|
+
const datum = data[index];
|
|
158
|
+
if (datum) onDataPointClick(datum, index);
|
|
159
|
+
}
|
|
160
|
+
: undefined
|
|
161
|
+
}
|
|
88
162
|
>
|
|
89
163
|
<RechartsBarChart
|
|
90
164
|
data={data}
|
|
@@ -195,18 +269,41 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|
|
195
269
|
content={<ChartLegendContent />}
|
|
196
270
|
/>
|
|
197
271
|
)}
|
|
198
|
-
{series.map((s, index) =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
272
|
+
{series.map((s, index) => {
|
|
273
|
+
const baseFill = getChartColor(index, s.color);
|
|
274
|
+
const interactive = !!onDataPointClick;
|
|
275
|
+
const perDatum = !!s.conditionalFormat || interactive;
|
|
276
|
+
return (
|
|
277
|
+
<Bar
|
|
278
|
+
key={s.dataKey}
|
|
279
|
+
dataKey={s.dataKey}
|
|
280
|
+
name={s.name}
|
|
281
|
+
fill={baseFill}
|
|
282
|
+
stackId={s.stackId}
|
|
283
|
+
radius={s.radius ?? 0}
|
|
284
|
+
isAnimationActive={isAnimated}
|
|
285
|
+
animationDuration={isAnimated ? 300 : 0}
|
|
286
|
+
onClick={
|
|
287
|
+
interactive
|
|
288
|
+
? (_data: unknown, i: number) => {
|
|
289
|
+
const datum = data[i];
|
|
290
|
+
if (datum) onDataPointClick?.(datum, i);
|
|
291
|
+
}
|
|
292
|
+
: undefined
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
{perDatum
|
|
296
|
+
? data.map((d, i) => (
|
|
297
|
+
<Cell
|
|
298
|
+
key={`${s.dataKey}-${i}`}
|
|
299
|
+
fill={resolveBarFill(s, d, baseFill)}
|
|
300
|
+
cursor={interactive ? "pointer" : undefined}
|
|
301
|
+
/>
|
|
302
|
+
))
|
|
303
|
+
: null}
|
|
304
|
+
</Bar>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
210
307
|
</RechartsBarChart>
|
|
211
308
|
</ChartContainer>
|
|
212
309
|
);
|
|
@@ -79,6 +79,14 @@ const ComposedChart = React.forwardRef<HTMLDivElement, ComposedChartProps>(
|
|
|
79
79
|
const tooltipProps = resolveTooltipProps(tooltip);
|
|
80
80
|
const isAnimated = live ? false : animate;
|
|
81
81
|
|
|
82
|
+
const tableColumns = React.useMemo(
|
|
83
|
+
() => [
|
|
84
|
+
{ key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
|
|
85
|
+
...series.map((s) => ({ key: s.dataKey, label: s.name })),
|
|
86
|
+
],
|
|
87
|
+
[xAxis.dataKey, xAxis.label, series],
|
|
88
|
+
);
|
|
89
|
+
|
|
82
90
|
return (
|
|
83
91
|
<ChartContainer
|
|
84
92
|
ref={ref}
|
|
@@ -86,6 +94,9 @@ const ComposedChart = React.forwardRef<HTMLDivElement, ComposedChartProps>(
|
|
|
86
94
|
height={height}
|
|
87
95
|
loading={loading}
|
|
88
96
|
empty={!data?.length}
|
|
97
|
+
ariaLabel="Composed chart"
|
|
98
|
+
tableData={data}
|
|
99
|
+
tableColumns={tableColumns}
|
|
89
100
|
>
|
|
90
101
|
<RechartsComposedChart data={data} margin={margin}>
|
|
91
102
|
{gridConfig && (
|
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
Tooltip,
|
|
9
9
|
Legend,
|
|
10
10
|
} from "recharts";
|
|
11
|
-
import { cn } from "@optilogic/core";
|
|
12
11
|
import { getChartColor } from "../shared/colors";
|
|
13
|
-
import { resolveTooltipProps
|
|
12
|
+
import { resolveTooltipProps } from "../shared/chart-tooltip";
|
|
14
13
|
import { ChartLegendContent, resolveLegendConfig } from "../shared/chart-legend";
|
|
15
14
|
import { ChartContainer } from "../shared/chart-container";
|
|
16
15
|
import type {
|
|
@@ -72,6 +71,14 @@ const LineChart = React.forwardRef<HTMLDivElement, LineChartProps>(
|
|
|
72
71
|
const tooltipProps = resolveTooltipProps(tooltip);
|
|
73
72
|
const isAnimated = live ? false : animate;
|
|
74
73
|
|
|
74
|
+
const tableColumns = React.useMemo(
|
|
75
|
+
() => [
|
|
76
|
+
{ key: xAxis.dataKey, label: xAxis.label ?? xAxis.dataKey },
|
|
77
|
+
...series.map((s) => ({ key: s.dataKey, label: s.name })),
|
|
78
|
+
],
|
|
79
|
+
[xAxis.dataKey, xAxis.label, series],
|
|
80
|
+
);
|
|
81
|
+
|
|
75
82
|
return (
|
|
76
83
|
<ChartContainer
|
|
77
84
|
ref={ref}
|
|
@@ -79,6 +86,9 @@ const LineChart = React.forwardRef<HTMLDivElement, LineChartProps>(
|
|
|
79
86
|
height={height}
|
|
80
87
|
loading={loading}
|
|
81
88
|
empty={!data?.length}
|
|
89
|
+
ariaLabel="Line chart"
|
|
90
|
+
tableData={data}
|
|
91
|
+
tableColumns={tableColumns}
|
|
82
92
|
>
|
|
83
93
|
<RechartsLineChart data={data} margin={margin}>
|
|
84
94
|
{gridConfig && (
|
|
@@ -71,6 +71,25 @@ const ScatterChart = React.forwardRef<HTMLDivElement, ScatterChartProps>(
|
|
|
71
71
|
const isAnimated = live ? false : animate;
|
|
72
72
|
const hasZ = series.some((s) => s.zDataKey);
|
|
73
73
|
|
|
74
|
+
const zDataKey = series.find((s) => s.zDataKey)?.zDataKey;
|
|
75
|
+
const tableColumns = React.useMemo(() => {
|
|
76
|
+
const cols = [
|
|
77
|
+
{ key: "__series", label: "Series" },
|
|
78
|
+
{ key: xAxis.dataKey, label: xAxis.name ?? xAxis.label ?? xAxis.dataKey },
|
|
79
|
+
{ key: "y", label: yAxis?.name ?? yAxis?.label ?? "y" },
|
|
80
|
+
];
|
|
81
|
+
if (zDataKey) cols.push({ key: zDataKey, label: zDataKey });
|
|
82
|
+
return cols;
|
|
83
|
+
}, [xAxis.dataKey, xAxis.name, xAxis.label, yAxis?.name, yAxis?.label, zDataKey]);
|
|
84
|
+
|
|
85
|
+
const tableData = React.useMemo(
|
|
86
|
+
() =>
|
|
87
|
+
series.flatMap((s) =>
|
|
88
|
+
(s.data ?? []).map((point) => ({ __series: s.name, ...point })),
|
|
89
|
+
),
|
|
90
|
+
[series],
|
|
91
|
+
);
|
|
92
|
+
|
|
74
93
|
return (
|
|
75
94
|
<ChartContainer
|
|
76
95
|
ref={ref}
|
|
@@ -78,6 +97,9 @@ const ScatterChart = React.forwardRef<HTMLDivElement, ScatterChartProps>(
|
|
|
78
97
|
height={height}
|
|
79
98
|
loading={loading}
|
|
80
99
|
empty={!series.some((s) => s.data?.length)}
|
|
100
|
+
ariaLabel="Scatter chart"
|
|
101
|
+
tableData={tableData}
|
|
102
|
+
tableColumns={tableColumns}
|
|
81
103
|
>
|
|
82
104
|
<RechartsScatterChart margin={margin}>
|
|
83
105
|
{gridConfig && (
|
|
@@ -93,6 +93,14 @@ const WaterfallChart = React.forwardRef<HTMLDivElement, WaterfallChartProps>(
|
|
|
93
93
|
const negColor = negativeColor ?? getSemanticColor("negative");
|
|
94
94
|
const totColor = totalColor ?? "hsl(var(--chart-3))";
|
|
95
95
|
|
|
96
|
+
const tableColumns = React.useMemo(
|
|
97
|
+
() => [
|
|
98
|
+
{ key: "name", label: xAxis?.label ?? "Name" },
|
|
99
|
+
{ key: "value", label: yAxis?.label ?? "Value" },
|
|
100
|
+
],
|
|
101
|
+
[xAxis?.label, yAxis?.label],
|
|
102
|
+
);
|
|
103
|
+
|
|
96
104
|
return (
|
|
97
105
|
<ChartContainer
|
|
98
106
|
ref={ref}
|
|
@@ -100,6 +108,9 @@ const WaterfallChart = React.forwardRef<HTMLDivElement, WaterfallChartProps>(
|
|
|
100
108
|
height={height}
|
|
101
109
|
loading={loading}
|
|
102
110
|
empty={!data?.length}
|
|
111
|
+
ariaLabel="Waterfall chart"
|
|
112
|
+
tableData={data}
|
|
113
|
+
tableColumns={tableColumns}
|
|
103
114
|
>
|
|
104
115
|
<RechartsBarChart data={rows} margin={margin}>
|
|
105
116
|
<CartesianGrid
|
|
@@ -73,6 +73,22 @@ const labelStyle: React.CSSProperties = {
|
|
|
73
73
|
display: "block",
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
/** Visible keyboard focus indicator for the inline-styled controls (SC 2.4.7). */
|
|
77
|
+
const FOCUS_STYLE_ID = "opti-chart-builder-focus";
|
|
78
|
+
const focusStyles = `
|
|
79
|
+
.opti-chart-builder-control:focus-visible {
|
|
80
|
+
outline: 2px solid hsl(var(--ring));
|
|
81
|
+
outline-offset: 1px;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
function FocusStyles() {
|
|
87
|
+
return <style id={FOCUS_STYLE_ID}>{focusStyles}</style>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const FOCUS_CLASS = "opti-chart-builder-control";
|
|
91
|
+
|
|
76
92
|
const ChartBuilder = React.memo(function ChartBuilder({
|
|
77
93
|
columns,
|
|
78
94
|
data,
|
|
@@ -121,6 +137,7 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
121
137
|
className={cn("flex gap-4", className)}
|
|
122
138
|
style={{ minHeight: 400 }}
|
|
123
139
|
>
|
|
140
|
+
<FocusStyles />
|
|
124
141
|
{/* Config panel */}
|
|
125
142
|
<div
|
|
126
143
|
className="flex flex-col gap-3 shrink-0 overflow-y-auto"
|
|
@@ -138,8 +155,12 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
138
155
|
|
|
139
156
|
{/* Chart type */}
|
|
140
157
|
<div>
|
|
141
|
-
<label style={labelStyle}>
|
|
158
|
+
<label htmlFor="cb-chart-type" style={labelStyle}>
|
|
159
|
+
Chart Type
|
|
160
|
+
</label>
|
|
142
161
|
<select
|
|
162
|
+
id="cb-chart-type"
|
|
163
|
+
className={FOCUS_CLASS}
|
|
143
164
|
style={selectStyle}
|
|
144
165
|
value={config.type}
|
|
145
166
|
onChange={(e) =>
|
|
@@ -154,8 +175,12 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
154
175
|
|
|
155
176
|
{/* Category axis */}
|
|
156
177
|
<div>
|
|
157
|
-
<label style={labelStyle}>
|
|
178
|
+
<label htmlFor="cb-category-axis" style={labelStyle}>
|
|
179
|
+
Category Axis
|
|
180
|
+
</label>
|
|
158
181
|
<select
|
|
182
|
+
id="cb-category-axis"
|
|
183
|
+
className={FOCUS_CLASS}
|
|
159
184
|
style={selectStyle}
|
|
160
185
|
value={config.categoryKey}
|
|
161
186
|
onChange={(e) =>
|
|
@@ -169,8 +194,8 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
169
194
|
</div>
|
|
170
195
|
|
|
171
196
|
{/* Series */}
|
|
172
|
-
<div>
|
|
173
|
-
<
|
|
197
|
+
<div role="group" aria-label="Series">
|
|
198
|
+
<div style={labelStyle}>Series</div>
|
|
174
199
|
{config.series.map((s, i) => (
|
|
175
200
|
<div
|
|
176
201
|
key={i}
|
|
@@ -178,6 +203,8 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
178
203
|
style={{ marginBottom: 4 }}
|
|
179
204
|
>
|
|
180
205
|
<select
|
|
206
|
+
className={FOCUS_CLASS}
|
|
207
|
+
aria-label={`Series ${i + 1} data column`}
|
|
181
208
|
style={{ ...selectStyle, flex: 1 }}
|
|
182
209
|
value={s.dataKey}
|
|
183
210
|
onChange={(e) => updateSeries(i, "dataKey", e.target.value)}
|
|
@@ -189,6 +216,9 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
189
216
|
))}
|
|
190
217
|
</select>
|
|
191
218
|
<button
|
|
219
|
+
type="button"
|
|
220
|
+
aria-label={`Remove series ${i + 1}`}
|
|
221
|
+
className={FOCUS_CLASS}
|
|
192
222
|
onClick={() => removeSeries(i)}
|
|
193
223
|
style={{
|
|
194
224
|
padding: "2px 6px",
|
|
@@ -206,6 +236,8 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
206
236
|
</div>
|
|
207
237
|
))}
|
|
208
238
|
<button
|
|
239
|
+
type="button"
|
|
240
|
+
className={FOCUS_CLASS}
|
|
209
241
|
onClick={addSeries}
|
|
210
242
|
style={{
|
|
211
243
|
width: "100%",
|
|
@@ -225,8 +257,12 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
225
257
|
|
|
226
258
|
{/* Legend */}
|
|
227
259
|
<div>
|
|
228
|
-
<label style={labelStyle}>
|
|
260
|
+
<label htmlFor="cb-legend" style={labelStyle}>
|
|
261
|
+
Legend
|
|
262
|
+
</label>
|
|
229
263
|
<select
|
|
264
|
+
id="cb-legend"
|
|
265
|
+
className={FOCUS_CLASS}
|
|
230
266
|
style={selectStyle}
|
|
231
267
|
value={config.legend}
|
|
232
268
|
onChange={(e) =>
|
|
@@ -244,6 +280,8 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
244
280
|
<input
|
|
245
281
|
type="checkbox"
|
|
246
282
|
id="grid-toggle"
|
|
283
|
+
aria-label="Show Grid"
|
|
284
|
+
className={FOCUS_CLASS}
|
|
247
285
|
checked={config.grid}
|
|
248
286
|
onChange={(e) =>
|
|
249
287
|
setConfig((prev) => ({ ...prev, grid: e.target.checked }))
|
|
@@ -259,6 +297,8 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
259
297
|
<input
|
|
260
298
|
type="checkbox"
|
|
261
299
|
id="animate-toggle"
|
|
300
|
+
aria-label="Animate"
|
|
301
|
+
className={FOCUS_CLASS}
|
|
262
302
|
checked={config.animate}
|
|
263
303
|
onChange={(e) =>
|
|
264
304
|
setConfig((prev) => ({ ...prev, animate: e.target.checked }))
|
|
@@ -271,6 +311,8 @@ const ChartBuilder = React.memo(function ChartBuilder({
|
|
|
271
311
|
|
|
272
312
|
{onSave && (
|
|
273
313
|
<button
|
|
314
|
+
type="button"
|
|
315
|
+
className={FOCUS_CLASS}
|
|
274
316
|
onClick={() => onSave(config)}
|
|
275
317
|
style={{
|
|
276
318
|
marginTop: 8,
|
|
@@ -4,7 +4,7 @@ import { BarChart } from "../cartesian/bar-chart";
|
|
|
4
4
|
import { AreaChart } from "../cartesian/area-chart";
|
|
5
5
|
import { ScatterChart } from "../cartesian/scatter-chart";
|
|
6
6
|
import { ComposedChart } from "../cartesian/composed-chart";
|
|
7
|
-
import { WaterfallChart
|
|
7
|
+
import { WaterfallChart } from "../cartesian/waterfall-chart";
|
|
8
8
|
import { PieChart } from "../radial/pie-chart";
|
|
9
9
|
import { DonutChart } from "../radial/donut-chart";
|
|
10
10
|
import { RadarChart } from "../radial/radar-chart";
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
export { CHART_COLORS, getChartColor, getSemanticColor, type SemanticColorName } from "./shared/colors";
|
|
3
3
|
export { ChartTooltipContent, resolveTooltipProps, type ChartTooltipContentProps } from "./shared/chart-tooltip";
|
|
4
4
|
export { ChartLegendContent, resolveLegendConfig } from "./shared/chart-legend";
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
ChartContainer,
|
|
7
|
+
type ChartContainerProps,
|
|
8
|
+
type ChartTableColumn,
|
|
9
|
+
} from "./shared/chart-container";
|
|
6
10
|
export { useLiveData, type UseLiveDataConfig, type UseLiveDataReturn } from "./shared/use-live-data";
|
|
7
11
|
export {
|
|
8
12
|
formatCompact,
|
|
@@ -62,6 +62,12 @@ const DonutChart = React.forwardRef<HTMLDivElement, DonutChartProps>(
|
|
|
62
62
|
height={height}
|
|
63
63
|
loading={loading}
|
|
64
64
|
empty={!data?.length}
|
|
65
|
+
ariaLabel={centerLabel ?? "Donut chart"}
|
|
66
|
+
tableData={data}
|
|
67
|
+
tableColumns={[
|
|
68
|
+
{ key: "name", label: "Name" },
|
|
69
|
+
{ key: "value", label: "Value" },
|
|
70
|
+
]}
|
|
65
71
|
>
|
|
66
72
|
<RechartsPieChart margin={margin}>
|
|
67
73
|
<Pie
|
package/src/radial/pie-chart.tsx
CHANGED
|
@@ -108,6 +108,12 @@ const PieChart = React.forwardRef<HTMLDivElement, PieChartProps>(
|
|
|
108
108
|
height={height}
|
|
109
109
|
loading={loading}
|
|
110
110
|
empty={!data?.length}
|
|
111
|
+
ariaLabel="Pie chart"
|
|
112
|
+
tableData={data}
|
|
113
|
+
tableColumns={[
|
|
114
|
+
{ key: "name", label: "Name" },
|
|
115
|
+
{ key: "value", label: "Value" },
|
|
116
|
+
]}
|
|
111
117
|
>
|
|
112
118
|
<RechartsPieChart margin={margin}>
|
|
113
119
|
<Pie
|
|
@@ -57,6 +57,14 @@ const RadarChart = React.forwardRef<HTMLDivElement, RadarChartProps>(
|
|
|
57
57
|
const tooltipProps = resolveTooltipProps(tooltip);
|
|
58
58
|
const isAnimated = live ? false : animate;
|
|
59
59
|
|
|
60
|
+
const tableColumns = React.useMemo(
|
|
61
|
+
() => [
|
|
62
|
+
{ key: categoryKey, label: categoryKey },
|
|
63
|
+
...series.map((s) => ({ key: s.dataKey, label: s.name })),
|
|
64
|
+
],
|
|
65
|
+
[categoryKey, series],
|
|
66
|
+
);
|
|
67
|
+
|
|
60
68
|
return (
|
|
61
69
|
<ChartContainer
|
|
62
70
|
ref={ref}
|
|
@@ -64,6 +72,9 @@ const RadarChart = React.forwardRef<HTMLDivElement, RadarChartProps>(
|
|
|
64
72
|
height={height}
|
|
65
73
|
loading={loading}
|
|
66
74
|
empty={!data?.length}
|
|
75
|
+
ariaLabel="Radar chart"
|
|
76
|
+
tableData={data}
|
|
77
|
+
tableColumns={tableColumns}
|
|
67
78
|
>
|
|
68
79
|
<RechartsRadarChart data={data} margin={margin} cx="50%" cy="50%">
|
|
69
80
|
<PolarGrid stroke="hsl(var(--divider))" />
|
|
@@ -70,6 +70,12 @@ const RadialBarChart = React.forwardRef<HTMLDivElement, RadialBarChartProps>(
|
|
|
70
70
|
height={height}
|
|
71
71
|
loading={loading}
|
|
72
72
|
empty={!data?.length}
|
|
73
|
+
ariaLabel="Radial bar chart"
|
|
74
|
+
tableData={data}
|
|
75
|
+
tableColumns={[
|
|
76
|
+
{ key: "name", label: "Name" },
|
|
77
|
+
{ key: "value", label: "Value" },
|
|
78
|
+
]}
|
|
73
79
|
>
|
|
74
80
|
<RechartsRadialBarChart
|
|
75
81
|
data={coloredData}
|