@galaxy-io/dls 1.3.0 → 1.3.1
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/charts/BarChart.d.ts +3 -3
- package/dist/charts/BarChart.js +68 -104
- package/dist/charts/ChartCategoryTargets.d.ts +27 -0
- package/dist/charts/ChartCategoryTargets.js +75 -0
- package/dist/charts/ChartFrame.d.ts +34 -6
- package/dist/charts/ChartFrame.js +72 -15
- package/dist/charts/ChartPrimitives.d.ts +35 -0
- package/dist/charts/ChartPrimitives.js +75 -38
- package/dist/charts/ChartTooltip.d.ts +3 -3
- package/dist/charts/ChartTooltip.js +2 -2
- package/dist/charts/LineChart.js +47 -62
- package/dist/charts/PieChart.js +48 -26
- package/dist/charts/barChartGeometry.d.ts +66 -33
- package/dist/charts/barChartGeometry.js +7 -15
- package/dist/charts/barChartLegend.d.ts +25 -0
- package/dist/charts/barChartLegend.js +23 -0
- package/dist/charts/chartFormat.d.ts +9 -1
- package/dist/charts/chartFormat.js +13 -1
- package/dist/charts/chartScales.d.ts +58 -22
- package/dist/charts/chartScales.js +92 -38
- package/dist/charts/constants.d.ts +22 -6
- package/dist/charts/constants.js +22 -1
- package/dist/charts/lineChartGeometry.d.ts +53 -15
- package/dist/charts/lineChartGeometry.js +6 -15
- package/dist/charts/pieChartGeometry.d.ts +20 -8
- package/dist/charts/pieChartGeometry.js +3 -13
- package/dist/charts/types.d.ts +51 -145
- package/dist/charts/types.js +22 -3
- package/dist/charts/useCartesianChartLayout.d.ts +70 -0
- package/dist/charts/useCartesianChartLayout.js +56 -0
- package/dist/charts/useChartDimensions.d.ts +10 -3
- package/dist/charts/useChartDimensions.js +14 -4
- package/dist/charts/useChartInteraction.d.ts +13 -8
- package/dist/charts/useChartInteraction.js +7 -11
- package/dist/charts/useSeriesColorResolver.d.ts +37 -0
- package/dist/charts/useSeriesColorResolver.js +27 -0
- package/dist/styles.css +13 -11
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BarChartNormalization } from "./types";
|
|
2
2
|
import type { BarChartGroupDatum, BaseCartesianChartProps, ChartSeriesStyles, ChartSize } from "./types";
|
|
3
|
-
export type BarChartProps<TMetric extends string,
|
|
3
|
+
export type BarChartProps<TMetric extends string, TComponentKey extends string = string> = BaseCartesianChartProps & ChartSize & {
|
|
4
4
|
/**
|
|
5
5
|
* Presentation for every metric, keyed by metric.
|
|
6
6
|
*
|
|
@@ -10,7 +10,7 @@ export type BarChartProps<TMetric extends string, TPivot extends string = string
|
|
|
10
10
|
*/
|
|
11
11
|
series: ChartSeriesStyles<TMetric>;
|
|
12
12
|
/** Categories along the x-axis, left to right. */
|
|
13
|
-
groups: BarChartGroupDatum<NoInfer<TMetric>, NoInfer<
|
|
13
|
+
groups: BarChartGroupDatum<NoInfer<TMetric>, NoInfer<TComponentKey>>[];
|
|
14
14
|
/** @default BarChartNormalization.NONE */
|
|
15
15
|
normalization?: BarChartNormalization;
|
|
16
16
|
/**
|
|
@@ -27,5 +27,5 @@ export type BarChartProps<TMetric extends string, TPivot extends string = string
|
|
|
27
27
|
* `bars` in a group render side by side, several `components` in a bar stack,
|
|
28
28
|
* and a chart doing both at once needs no extra configuration.
|
|
29
29
|
*/
|
|
30
|
-
declare const BarChart: <TMetric extends string,
|
|
30
|
+
declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, noTooltip, tooltipRenderer, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, onPinnedChange, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
|
|
31
31
|
export default BarChart;
|
package/dist/charts/BarChart.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { addAxisTitleSpace, getValueTicks } from "./chartScales.js";
|
|
1
|
+
import { BarChartNormalization, ChartKind, ChartTargetShape } from "./types.js";
|
|
2
|
+
import { useSeriesColorResolver } from "./useSeriesColorResolver.js";
|
|
4
3
|
import { useChartInteraction } from "./useChartInteraction.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { ChartBarPath, ChartHitTarget } from "./ChartPrimitives.js";
|
|
4
|
+
import { useCartesianChartLayout } from "./useCartesianChartLayout.js";
|
|
5
|
+
import { getBarLegendItems } from "./barChartLegend.js";
|
|
6
|
+
import { buildBarChartGeometry, getBarValueDomain, roundedRectPath } from "./barChartGeometry.js";
|
|
7
|
+
import { ChartBarPath } from "./ChartPrimitives.js";
|
|
10
8
|
import ChartFrame from "./ChartFrame.js";
|
|
11
9
|
import { ChartCategoryAxis, ChartValueAxis } from "./ChartAxis.js";
|
|
12
10
|
import { useCallback, useMemo, useRef } from "react";
|
|
@@ -20,44 +18,48 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
20
18
|
* and a chart doing both at once needs no extra configuration.
|
|
21
19
|
*/
|
|
22
20
|
var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization = BarChartNormalization.NONE, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, noTooltip = false, tooltipRenderer, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription, onPinnedChange }) => {
|
|
23
|
-
const { theme } = useGalaxyTheme();
|
|
24
21
|
const containerRef = useRef(null);
|
|
25
|
-
const
|
|
26
|
-
containerRef,
|
|
27
|
-
width,
|
|
28
|
-
height,
|
|
29
|
-
fillWidth,
|
|
30
|
-
fillHeight,
|
|
31
|
-
aspectRatio,
|
|
32
|
-
margin: addAxisTitleSpace(margin, {
|
|
33
|
-
hasValueTitle: Boolean(valueAxisTitle),
|
|
34
|
-
hasCategoryTitle: Boolean(categoryAxisTitle)
|
|
35
|
-
})
|
|
36
|
-
});
|
|
37
|
-
const { plotWidth, plotHeight } = dimensions;
|
|
38
|
-
const { formatValue, formatLabel } = useChartFormatters({
|
|
39
|
-
valueFormatter,
|
|
40
|
-
valueUnit,
|
|
41
|
-
labelFormatter
|
|
42
|
-
});
|
|
22
|
+
const colors = useSeriesColorResolver(series);
|
|
43
23
|
/**
|
|
44
|
-
* Axis
|
|
24
|
+
* Axis-only formatting for a percent-normalized chart.
|
|
45
25
|
*
|
|
46
|
-
* Normalization rescales
|
|
47
|
-
* describes their real units — would label
|
|
26
|
+
* Normalization rescales the *axis* into 0–1, so the caller's formatter —
|
|
27
|
+
* which describes their real units — would label it `0.2`, `0.4`. Unless they
|
|
48
28
|
* asked for something specific, show shares as percentages.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately not used for the tooltip. `stackComponents` normalizes each
|
|
31
|
+
* segment's extent but leaves its `value` raw, so a tooltip row still carries
|
|
32
|
+
* the real number: the axis reports the share, the tooltip reports what the
|
|
33
|
+
* share is made of. Running a row through this would multiply a raw 200 into
|
|
34
|
+
* `20000%`.
|
|
49
35
|
*/
|
|
50
|
-
const
|
|
36
|
+
const deriveAxisFormatter = useCallback((formatValue) => {
|
|
51
37
|
if (normalization !== BarChartNormalization.PERCENT) return formatValue;
|
|
52
38
|
if (valueFormatter || valueUnit) return formatValue;
|
|
53
39
|
return (value) => `${Math.round(value * 100)}%`;
|
|
54
40
|
}, [
|
|
55
41
|
normalization,
|
|
56
42
|
valueFormatter,
|
|
57
|
-
valueUnit
|
|
58
|
-
formatValue
|
|
43
|
+
valueUnit
|
|
59
44
|
]);
|
|
60
|
-
const
|
|
45
|
+
const { dimensions, domain, valueTicks, formatValue, formatLabel, formatAxisValue } = useCartesianChartLayout({
|
|
46
|
+
containerRef,
|
|
47
|
+
rawDomain: useMemo(() => getBarValueDomain(groups, normalization), [groups, normalization]),
|
|
48
|
+
valueDomain,
|
|
49
|
+
width,
|
|
50
|
+
height,
|
|
51
|
+
fillWidth,
|
|
52
|
+
fillHeight,
|
|
53
|
+
aspectRatio,
|
|
54
|
+
margin,
|
|
55
|
+
valueUnit,
|
|
56
|
+
valueFormatter,
|
|
57
|
+
labelFormatter,
|
|
58
|
+
deriveAxisFormatter,
|
|
59
|
+
valueAxisTitle,
|
|
60
|
+
categoryAxisTitle
|
|
61
|
+
});
|
|
62
|
+
const { plotWidth, plotHeight } = dimensions;
|
|
61
63
|
/**
|
|
62
64
|
* True when any bar carries more than one component.
|
|
63
65
|
*
|
|
@@ -68,22 +70,14 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
68
70
|
* paint every segment identically.
|
|
69
71
|
*/
|
|
70
72
|
const isStacked = useMemo(() => groups.some((group) => group.bars.some((bar) => bar.components.length > 1)), [groups]);
|
|
71
|
-
const resolveColor = useCallback(({ metric, componentIndex, isStacked: barIsStacked, explicit }) =>
|
|
72
|
-
if (explicit) return resolveChartPaletteColor(theme, explicit);
|
|
73
|
-
if (barIsStacked) return resolveSeriesColor(theme, componentIndex);
|
|
74
|
-
return resolveSeriesColor(theme, metricKeys.indexOf(metric), series[metric]?.color);
|
|
75
|
-
}, [
|
|
76
|
-
theme,
|
|
77
|
-
metricKeys,
|
|
78
|
-
series
|
|
79
|
-
]);
|
|
73
|
+
const resolveColor = useCallback(({ metric, componentIndex, isStacked: barIsStacked, explicit }) => barIsStacked ? colors.resolveByIndex(componentIndex, explicit) : colors.resolve(metric, explicit), [colors]);
|
|
80
74
|
const geometry = useMemo(() => buildBarChartGeometry({
|
|
81
75
|
groups,
|
|
82
76
|
plotWidth,
|
|
83
77
|
plotHeight,
|
|
84
78
|
normalization,
|
|
85
79
|
isStacked,
|
|
86
|
-
|
|
80
|
+
domain,
|
|
87
81
|
resolveColor
|
|
88
82
|
}), [
|
|
89
83
|
groups,
|
|
@@ -91,65 +85,36 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
91
85
|
plotHeight,
|
|
92
86
|
normalization,
|
|
93
87
|
isStacked,
|
|
94
|
-
|
|
88
|
+
domain,
|
|
95
89
|
resolveColor
|
|
96
90
|
]);
|
|
97
|
-
const valueTicks = useMemo(() => getValueTicks(geometry.valueScale, valueDomain), [geometry.valueScale, valueDomain]);
|
|
98
91
|
const isEmpty = groups.length === 0;
|
|
99
|
-
const legendItems = useMemo(() => {
|
|
100
|
-
if (!isStacked) return metricKeys.map((metric) => ({
|
|
101
|
-
key: metric,
|
|
102
|
-
label: series[metric].label,
|
|
103
|
-
color: resolveColor({
|
|
104
|
-
metric,
|
|
105
|
-
barIndex: 0,
|
|
106
|
-
componentIndex: 0,
|
|
107
|
-
isStacked: false,
|
|
108
|
-
explicit: series[metric].color
|
|
109
|
-
})
|
|
110
|
-
}));
|
|
111
|
-
const seen = /* @__PURE__ */ new Map();
|
|
112
|
-
for (const group of groups) for (const bar of group.bars) bar.components.forEach((component, componentIndex) => {
|
|
113
|
-
if (!seen.has(component.key)) seen.set(component.key, {
|
|
114
|
-
label: component.label,
|
|
115
|
-
index: componentIndex,
|
|
116
|
-
explicit: component.color
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
return [...seen.entries()].map(([key, { label, index, explicit }]) => ({
|
|
120
|
-
key,
|
|
121
|
-
label,
|
|
122
|
-
color: resolveColor({
|
|
123
|
-
metric: metricKeys[0],
|
|
124
|
-
barIndex: 0,
|
|
125
|
-
componentIndex: index,
|
|
126
|
-
isStacked: true,
|
|
127
|
-
explicit
|
|
128
|
-
})
|
|
129
|
-
}));
|
|
130
|
-
}, [
|
|
92
|
+
const legendItems = useMemo(() => getBarLegendItems({
|
|
131
93
|
isStacked,
|
|
132
|
-
metricKeys,
|
|
133
94
|
series,
|
|
134
95
|
groups,
|
|
135
|
-
|
|
96
|
+
colors
|
|
97
|
+
}), [
|
|
98
|
+
isStacked,
|
|
99
|
+
series,
|
|
100
|
+
groups,
|
|
101
|
+
colors
|
|
136
102
|
]);
|
|
137
103
|
const legendKeys = useMemo(() => legendItems.map((item) => item.key), [legendItems]);
|
|
138
104
|
const interaction = useChartInteraction({
|
|
139
105
|
categoryCount: geometry.bands.length,
|
|
140
106
|
seriesKeys: legendKeys,
|
|
141
107
|
disabled: noTooltip || isLoading || isEmpty,
|
|
108
|
+
isLoading,
|
|
142
109
|
onPinnedChange
|
|
143
110
|
});
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const group = groups[interaction.activeIndex];
|
|
111
|
+
const buildCategoryTooltip = useCallback((index) => {
|
|
112
|
+
const band = geometry.bands[index];
|
|
113
|
+
const group = groups[index];
|
|
148
114
|
if (!band || !group) return null;
|
|
149
|
-
const rows = geometry.rects.filter((rect) => rect.groupIndex ===
|
|
115
|
+
const rows = geometry.rects.filter((rect) => rect.groupIndex === index).map((rect) => ({
|
|
150
116
|
key: rect.id,
|
|
151
117
|
label: rect.label,
|
|
152
|
-
group: series[rect.metric]?.label,
|
|
153
118
|
value: rect.value,
|
|
154
119
|
formattedValue: formatValue(rect.value),
|
|
155
120
|
color: rect.color
|
|
@@ -161,23 +126,30 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
161
126
|
anchorY: band.anchorY
|
|
162
127
|
};
|
|
163
128
|
}, [
|
|
164
|
-
noTooltip,
|
|
165
|
-
interaction.activeIndex,
|
|
166
129
|
geometry,
|
|
167
130
|
groups,
|
|
168
|
-
series,
|
|
169
131
|
formatValue,
|
|
170
132
|
formatLabel
|
|
171
133
|
]);
|
|
172
134
|
const categoryPositions = useMemo(() => geometry.bands.map((band) => band.x), [geometry.bands]);
|
|
173
135
|
const categoryStep = geometry.bands[0]?.width ?? 0;
|
|
174
|
-
|
|
136
|
+
const categoryTargets = useMemo(() => geometry.bands.map((band) => ({
|
|
137
|
+
key: String(band.groupIndex),
|
|
138
|
+
shape: ChartTargetShape.RECT,
|
|
139
|
+
x: band.x,
|
|
140
|
+
y: band.y,
|
|
141
|
+
width: band.width,
|
|
142
|
+
height: band.height
|
|
143
|
+
})), [geometry.bands]);
|
|
144
|
+
return /* @__PURE__ */ jsx(ChartFrame, {
|
|
175
145
|
containerRef,
|
|
176
146
|
dimensions,
|
|
177
147
|
fillWidth,
|
|
178
148
|
fillHeight,
|
|
179
149
|
interaction,
|
|
180
|
-
|
|
150
|
+
kind: ChartKind.BAR,
|
|
151
|
+
categoryCount: groups.length,
|
|
152
|
+
ariaLabel,
|
|
181
153
|
ariaDescription,
|
|
182
154
|
axes: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ChartValueAxis, {
|
|
183
155
|
ticks: valueTicks,
|
|
@@ -199,31 +171,23 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
199
171
|
title: categoryAxisTitle
|
|
200
172
|
})] }),
|
|
201
173
|
noTooltip,
|
|
202
|
-
|
|
174
|
+
buildCategoryTooltip,
|
|
203
175
|
tooltipRenderer,
|
|
176
|
+
categoryTargets,
|
|
204
177
|
anchorHalfWidth: categoryStep / 2,
|
|
205
178
|
isEmpty,
|
|
206
179
|
isLoading,
|
|
207
180
|
contentWhenEmpty,
|
|
208
181
|
showLegend,
|
|
209
182
|
legendItems,
|
|
210
|
-
children:
|
|
183
|
+
children: geometry.rects.map((rect) => /* @__PURE__ */ jsx(ChartBarPath, {
|
|
211
184
|
d: roundedRectPath(rect.x, rect.y, rect.width, rect.height, rect.radius, rect.corners),
|
|
212
185
|
fill: rect.color,
|
|
213
|
-
"data-state":
|
|
186
|
+
"data-state": interaction.getMarkState({
|
|
214
187
|
categoryIndex: rect.groupIndex,
|
|
215
188
|
seriesKey: isStacked ? rect.componentKey : rect.metric
|
|
216
189
|
})
|
|
217
|
-
}, rect.id))
|
|
218
|
-
x: band.x,
|
|
219
|
-
y: band.y,
|
|
220
|
-
width: band.width,
|
|
221
|
-
height: band.height,
|
|
222
|
-
"data-interactive": "true",
|
|
223
|
-
onMouseEnter: () => interaction.onCategoryEnter(band.groupIndex),
|
|
224
|
-
onMouseLeave: interaction.onCategoryLeave,
|
|
225
|
-
onClick: () => interaction.onCategoryClick(band.groupIndex)
|
|
226
|
-
}, band.groupIndex))]
|
|
190
|
+
}, rect.id))
|
|
227
191
|
});
|
|
228
192
|
};
|
|
229
193
|
//#endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ChartCategoryTarget } from "./types";
|
|
2
|
+
import type { ChartInteraction } from "./useChartInteraction";
|
|
3
|
+
/**
|
|
4
|
+
* The interactive layer: one focusable target per category, above the marks.
|
|
5
|
+
*
|
|
6
|
+
* Shared by all three charts so pointer and keyboard behave identically. Before
|
|
7
|
+
* this existed each chart spelled hit testing its own way — two rendered targets
|
|
8
|
+
* conditionally, the pie attached handlers permanently and computed
|
|
9
|
+
* `data-interactive` instead — and none of them was reachable by keyboard at
|
|
10
|
+
* all.
|
|
11
|
+
*
|
|
12
|
+
* **Focus rides the existing hover channel.** `onFocus` calls
|
|
13
|
+
* `onCategoryEnter` and `onBlur` calls `onCategoryLeave`, so the tooltip, the
|
|
14
|
+
* cursor line, the legend dimming and every `getMarkState` light up for a
|
|
15
|
+
* keyboard reader exactly as they do for a pointer — without
|
|
16
|
+
* `useChartInteraction` growing a third state axis to keep in sync.
|
|
17
|
+
*/
|
|
18
|
+
interface ChartCategoryTargetsProps {
|
|
19
|
+
targets: ChartCategoryTarget[];
|
|
20
|
+
interaction: ChartInteraction;
|
|
21
|
+
/** Accessible name for one category. Derived from the tooltip, so they agree. */
|
|
22
|
+
getAriaLabel: (index: number) => string;
|
|
23
|
+
/** Arrow keys wrap at the ends. True for radial charts — a circle has no ends. */
|
|
24
|
+
wrap?: boolean;
|
|
25
|
+
}
|
|
26
|
+
declare const ChartCategoryTargets: ({ targets, interaction, getAriaLabel, wrap, }: ChartCategoryTargetsProps) => import("react").JSX.Element;
|
|
27
|
+
export default ChartCategoryTargets;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ChartTargetShape } from "./types.js";
|
|
2
|
+
import { clamp } from "./chartScales.js";
|
|
3
|
+
import { ChartCategoryTargetLayer, ChartHitTarget, ChartHitTargetPath } from "./ChartPrimitives.js";
|
|
4
|
+
import { match } from "ts-pattern";
|
|
5
|
+
import { useCallback, useRef, useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/charts/ChartCategoryTargets.tsx
|
|
8
|
+
var ChartCategoryTargets = ({ targets, interaction, getAriaLabel, wrap = false }) => {
|
|
9
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
10
|
+
const nodeRefs = useRef([]);
|
|
11
|
+
/**
|
|
12
|
+
* Roving tabindex, preferring whatever is pinned.
|
|
13
|
+
*
|
|
14
|
+
* Tabbing away and back returns to the category the reader was holding rather
|
|
15
|
+
* than to the start, which is the difference between the pin being a place
|
|
16
|
+
* you are and a thing that merely happened.
|
|
17
|
+
*/
|
|
18
|
+
const rovingIndex = clamp(interaction.pinnedIndex ?? focusIndex, 0, targets.length - 1);
|
|
19
|
+
const moveTo = useCallback((next) => {
|
|
20
|
+
const last = targets.length - 1;
|
|
21
|
+
const bounded = wrap ? (next % targets.length + targets.length) % targets.length : clamp(next, 0, last);
|
|
22
|
+
setFocusIndex(bounded);
|
|
23
|
+
nodeRefs.current[bounded]?.focus();
|
|
24
|
+
}, [targets.length, wrap]);
|
|
25
|
+
const onKeyDown = useCallback((event, index) => {
|
|
26
|
+
if (match(event.key).with("ArrowRight", "ArrowDown", () => moveTo(index + 1)).with("ArrowLeft", "ArrowUp", () => moveTo(index - 1)).with("Home", () => moveTo(0)).with("End", () => moveTo(targets.length - 1)).with("Enter", " ", () => interaction.onCategoryClick(index)).with("Escape", () => interaction.onBackdropClick()).otherwise(() => void 0) !== void 0 || [
|
|
27
|
+
"ArrowRight",
|
|
28
|
+
"ArrowDown",
|
|
29
|
+
"ArrowLeft",
|
|
30
|
+
"ArrowUp",
|
|
31
|
+
"Home",
|
|
32
|
+
"End",
|
|
33
|
+
"Enter",
|
|
34
|
+
" ",
|
|
35
|
+
"Escape"
|
|
36
|
+
].includes(event.key)) event.preventDefault();
|
|
37
|
+
}, [
|
|
38
|
+
moveTo,
|
|
39
|
+
interaction,
|
|
40
|
+
targets.length
|
|
41
|
+
]);
|
|
42
|
+
return /* @__PURE__ */ jsx(ChartCategoryTargetLayer, { children: targets.map((target, index) => {
|
|
43
|
+
const shared = {
|
|
44
|
+
tabIndex: index === rovingIndex ? 0 : -1,
|
|
45
|
+
role: "button",
|
|
46
|
+
"aria-label": getAriaLabel(index),
|
|
47
|
+
"aria-pressed": interaction.pinnedIndex === index,
|
|
48
|
+
"data-interactive": "true",
|
|
49
|
+
onFocus: () => interaction.onCategoryEnter(index),
|
|
50
|
+
onBlur: () => interaction.onCategoryLeave(),
|
|
51
|
+
onMouseEnter: () => interaction.onCategoryEnter(index),
|
|
52
|
+
onMouseLeave: () => interaction.onCategoryLeave(),
|
|
53
|
+
onClick: () => interaction.onCategoryClick(index),
|
|
54
|
+
onKeyDown: (event) => onKeyDown(event, index)
|
|
55
|
+
};
|
|
56
|
+
return match(target).with({ shape: ChartTargetShape.RECT }, (rect) => /* @__PURE__ */ jsx(ChartHitTarget, {
|
|
57
|
+
ref: (node) => {
|
|
58
|
+
nodeRefs.current[index] = node;
|
|
59
|
+
},
|
|
60
|
+
x: rect.x,
|
|
61
|
+
y: rect.y,
|
|
62
|
+
width: rect.width,
|
|
63
|
+
height: rect.height,
|
|
64
|
+
...shared
|
|
65
|
+
}, rect.key)).with({ shape: ChartTargetShape.PATH }, (path) => /* @__PURE__ */ jsx(ChartHitTargetPath, {
|
|
66
|
+
ref: (node) => {
|
|
67
|
+
nodeRefs.current[index] = node;
|
|
68
|
+
},
|
|
69
|
+
d: path.d,
|
|
70
|
+
...shared
|
|
71
|
+
}, path.key)).exhaustive();
|
|
72
|
+
}) });
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ChartCategoryTargets as default };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import
|
|
2
|
+
import { ChartKind } from "./types";
|
|
3
|
+
import type { ChartCategoryTarget, ChartLegendItem, ChartPlotTooltipModel, ChartTooltipRenderer } from "./types";
|
|
3
4
|
import type { ChartDimensions } from "./useChartDimensions";
|
|
4
5
|
import type { ChartInteraction } from "./useChartInteraction";
|
|
5
6
|
import type { ChartTooltipVerticalAlign } from "./useChartTooltipPosition";
|
|
@@ -40,8 +41,13 @@ export interface ChartFrameProps {
|
|
|
40
41
|
fillWidth?: boolean;
|
|
41
42
|
fillHeight?: boolean;
|
|
42
43
|
interaction: ChartInteraction;
|
|
43
|
-
/**
|
|
44
|
-
|
|
44
|
+
/** Which chart this is. Only used to phrase the default accessible name. */
|
|
45
|
+
kind: ChartKind;
|
|
46
|
+
/** Categories on the axis, or slices for a pie. Names the plot, so it counts
|
|
47
|
+
* what is *rendered* rather than what was supplied. */
|
|
48
|
+
categoryCount: number;
|
|
49
|
+
/** Overrides the name derived from {@link kind} and the counts. */
|
|
50
|
+
ariaLabel?: string;
|
|
45
51
|
/** Long description, associated with the SVG via `aria-describedby`. */
|
|
46
52
|
ariaDescription?: string;
|
|
47
53
|
/**
|
|
@@ -61,9 +67,31 @@ export interface ChartFrameProps {
|
|
|
61
67
|
/** HTML painted over the plot and under the tooltip — the donut centre. */
|
|
62
68
|
plotOverlay?: ReactNode;
|
|
63
69
|
noTooltip?: boolean;
|
|
64
|
-
/**
|
|
65
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Builds one category's tooltip, in plot coordinates.
|
|
72
|
+
*
|
|
73
|
+
* Taken as a function rather than a finished model because the frame needs it
|
|
74
|
+
* twice: once for the category under the pointer, and once per category to
|
|
75
|
+
* name it for a screen reader. Deriving both from the same builder is what
|
|
76
|
+
* makes the accessible name incapable of disagreeing with the tooltip.
|
|
77
|
+
*
|
|
78
|
+
* Must be stable; memoize it at the call site.
|
|
79
|
+
*/
|
|
80
|
+
buildCategoryTooltip: (index: number) => ChartPlotTooltipModel | null;
|
|
66
81
|
tooltipRenderer?: ChartTooltipRenderer;
|
|
82
|
+
/**
|
|
83
|
+
* One target per category, in plot coordinates and in category order.
|
|
84
|
+
*
|
|
85
|
+
* Empty disables interaction entirely, which is what a chart with nothing to
|
|
86
|
+
* plot passes.
|
|
87
|
+
*/
|
|
88
|
+
categoryTargets?: ChartCategoryTarget[];
|
|
89
|
+
/** Applied to the target layer. Radial charts pass their centre translate. */
|
|
90
|
+
categoryTargetsTransform?: string;
|
|
91
|
+
/** Arrow keys wrap at the ends. True for radial charts. */
|
|
92
|
+
wrapCategoryNavigation?: boolean;
|
|
93
|
+
/** Overrides the accessible name of a category, which defaults to its tooltip. */
|
|
94
|
+
describeCategory?: (index: number) => string;
|
|
67
95
|
/** Half a category's width, so the tooltip sits beside the marks. */
|
|
68
96
|
anchorHalfWidth?: number;
|
|
69
97
|
/** Radial charts pass their centre, in plot coordinates. */
|
|
@@ -78,5 +106,5 @@ export interface ChartFrameProps {
|
|
|
78
106
|
/** Defaults to the left margin, aligning the legend with the plot, not the gutter. */
|
|
79
107
|
legendInset?: number;
|
|
80
108
|
}
|
|
81
|
-
declare const ChartFrame: ({ containerRef, dimensions, fillWidth, fillHeight, interaction, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip,
|
|
109
|
+
declare const ChartFrame: ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip, buildCategoryTooltip, tooltipRenderer, categoryTargets, categoryTargetsTransform, wrapCategoryNavigation, describeCategory, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading, contentWhenEmpty, showLegend, legendItems, legendInset, }: ChartFrameProps) => import("react").JSX.Element;
|
|
82
110
|
export default ChartFrame;
|
|
@@ -1,15 +1,34 @@
|
|
|
1
|
+
import { ChartKind } from "./types.js";
|
|
2
|
+
import { describeTooltipModel } from "./chartFormat.js";
|
|
1
3
|
import { ChartBackdrop, ChartPlotBox, ChartPlotGroup, ChartRoot, ChartSvg } from "./ChartPrimitives.js";
|
|
4
|
+
import ChartCategoryTargets from "./ChartCategoryTargets.js";
|
|
2
5
|
import ChartEmptyState from "./ChartEmptyState.js";
|
|
3
6
|
import ChartLegend from "./ChartLegend.js";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
7
|
+
import ChartTooltip from "./ChartTooltip.js";
|
|
8
|
+
import { match } from "ts-pattern";
|
|
9
|
+
import { useCallback, useId, useMemo } from "react";
|
|
6
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
11
|
//#region src/charts/ChartFrame.tsx
|
|
8
|
-
var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip = false,
|
|
12
|
+
var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip = false, buildCategoryTooltip, tooltipRenderer, categoryTargets, categoryTargetsTransform, wrapCategoryNavigation, describeCategory, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading = false, contentWhenEmpty, showLegend = false, legendItems, legendInset }) => {
|
|
9
13
|
const { width, height, plotWidth, plotHeight, margin, isMeasured, reservedHeight, reservedAspectRatio } = dimensions;
|
|
10
14
|
const generatedId = useId();
|
|
11
15
|
const descriptionId = ariaDescription ? `${generatedId}-desc` : void 0;
|
|
12
16
|
/**
|
|
17
|
+
* Default accessible name, phrased here so the three charts cannot drift.
|
|
18
|
+
*
|
|
19
|
+
* Both counts describe what is *on screen*: `legendItems` rather than the
|
|
20
|
+
* `series` record, because a stacked bar's legend names its components, not
|
|
21
|
+
* its metrics — counting the record would announce "1 series" for a chart
|
|
22
|
+
* showing three. The two charts previously counted different things for the
|
|
23
|
+
* same situation, which is the drift this removes.
|
|
24
|
+
*/
|
|
25
|
+
const resolvedAriaLabel = ariaLabel ?? match(kind).with(ChartKind.PIE, () => `Pie chart with ${categoryCount} slices`).with(ChartKind.BAR, () => `Bar chart with ${categoryCount} categories and ${legendItems.length} series`).with(ChartKind.LINE, () => `Line chart with ${categoryCount} categories and ${legendItems.length} series`).exhaustive();
|
|
26
|
+
const tooltipModel = useMemo(() => noTooltip || interaction.activeIndex === null ? null : buildCategoryTooltip(interaction.activeIndex), [
|
|
27
|
+
noTooltip,
|
|
28
|
+
buildCategoryTooltip,
|
|
29
|
+
interaction.activeIndex
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
13
32
|
* Plot space → container space, applied once, here.
|
|
14
33
|
*
|
|
15
34
|
* Memoized for correctness, not arithmetic cost. `ChartTooltip` retains the
|
|
@@ -28,6 +47,29 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
|
|
|
28
47
|
margin.left,
|
|
29
48
|
margin.top
|
|
30
49
|
]);
|
|
50
|
+
/**
|
|
51
|
+
* Name each category from its own tooltip, unless the chart says otherwise.
|
|
52
|
+
*
|
|
53
|
+
* Same builder as the visible tooltip, so the two cannot describe the chart
|
|
54
|
+
* differently.
|
|
55
|
+
*/
|
|
56
|
+
const nameCategory = useCallback((index) => {
|
|
57
|
+
if (describeCategory) return describeCategory(index);
|
|
58
|
+
const model = buildCategoryTooltip(index);
|
|
59
|
+
return model ? describeTooltipModel(model) : "";
|
|
60
|
+
}, [describeCategory, buildCategoryTooltip]);
|
|
61
|
+
/**
|
|
62
|
+
* Whether the marks are reachable at all.
|
|
63
|
+
*
|
|
64
|
+
* Drives the SVG's role, which is the real cost of keyboard access:
|
|
65
|
+
* `role="img"` prunes the whole subtree from the accessibility tree, so
|
|
66
|
+
* nothing inside it can ever be focused *and* announced. A chart with live
|
|
67
|
+
* targets therefore has to be a `group` — and everything that is not a target
|
|
68
|
+
* has to be explicitly hidden, since under `group` it would otherwise become
|
|
69
|
+
* traversable. When there is nothing to reach, `img` stays: it announces one
|
|
70
|
+
* summary rather than an empty group.
|
|
71
|
+
*/
|
|
72
|
+
const hasFocusableTargets = isMeasured && !noTooltip && !isLoading && !isEmpty && (categoryTargets?.length ?? 0) > 0;
|
|
31
73
|
return /* @__PURE__ */ jsxs(ChartRoot, {
|
|
32
74
|
$fillWidth: fillWidth,
|
|
33
75
|
$fillHeight: fillHeight,
|
|
@@ -40,27 +82,42 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
|
|
|
40
82
|
isMeasured && /* @__PURE__ */ jsxs(ChartSvg, {
|
|
41
83
|
width,
|
|
42
84
|
height,
|
|
43
|
-
role: "img",
|
|
44
|
-
"aria-label":
|
|
85
|
+
role: hasFocusableTargets ? "group" : "img",
|
|
86
|
+
"aria-label": resolvedAriaLabel,
|
|
45
87
|
"aria-describedby": descriptionId,
|
|
46
88
|
children: [ariaDescription && /* @__PURE__ */ jsx("desc", {
|
|
47
89
|
id: descriptionId,
|
|
48
90
|
children: ariaDescription
|
|
49
91
|
}), /* @__PURE__ */ jsxs("g", {
|
|
50
92
|
transform: `translate(${margin.left}, ${margin.top})`,
|
|
51
|
-
children: [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
children: [
|
|
94
|
+
axes,
|
|
95
|
+
/* @__PURE__ */ jsx("g", {
|
|
96
|
+
"aria-hidden": "true",
|
|
97
|
+
children: /* @__PURE__ */ jsxs(ChartPlotGroup, {
|
|
98
|
+
"data-active": interaction.isEngaged ? "true" : "false",
|
|
99
|
+
children: [/* @__PURE__ */ jsx(ChartBackdrop, {
|
|
100
|
+
width: plotWidth,
|
|
101
|
+
height: plotHeight,
|
|
102
|
+
"data-interactive": interaction.pinnedIndex !== null ? "true" : "false",
|
|
103
|
+
onClick: interaction.onBackdropClick
|
|
104
|
+
}), children]
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
hasFocusableTargets && categoryTargets && /* @__PURE__ */ jsx("g", {
|
|
108
|
+
transform: categoryTargetsTransform,
|
|
109
|
+
children: /* @__PURE__ */ jsx(ChartCategoryTargets, {
|
|
110
|
+
targets: categoryTargets,
|
|
111
|
+
interaction,
|
|
112
|
+
getAriaLabel: nameCategory,
|
|
113
|
+
wrap: wrapCategoryNavigation
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
]
|
|
60
117
|
})]
|
|
61
118
|
}),
|
|
62
119
|
isMeasured && plotOverlay,
|
|
63
|
-
isMeasured && !noTooltip && /* @__PURE__ */ jsx(
|
|
120
|
+
isMeasured && !noTooltip && /* @__PURE__ */ jsx(ChartTooltip, {
|
|
64
121
|
model: positionedTooltip,
|
|
65
122
|
containerWidth: width,
|
|
66
123
|
containerHeight: height,
|
|
@@ -45,6 +45,32 @@ export declare const ChartLinePath: import("@linaria/react").StyledComponent<imp
|
|
|
45
45
|
export declare const ChartAreaPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
|
|
46
46
|
export declare const ChartPointCircle: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGCircleElement> & Record<never, unknown>>;
|
|
47
47
|
export declare const ChartArcPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
|
|
48
|
+
/**
|
|
49
|
+
* Wrapper for the category target layer.
|
|
50
|
+
*
|
|
51
|
+
* Exists to carry the focus-ring colour: the targets themselves are plain
|
|
52
|
+
* `styled.x` because they receive refs (see the module note above), so they
|
|
53
|
+
* cannot read the theme directly. Setting `color` here lets them use
|
|
54
|
+
* `currentColor` and still take the token rather than inheriting whatever the
|
|
55
|
+
* consumer's page happens to set.
|
|
56
|
+
*/
|
|
57
|
+
export declare const ChartCategoryTargetLayer: import("react").ComponentType<Pick<import("react").SVGProps<SVGGElement> & {
|
|
58
|
+
theme: import("../theme").Theme;
|
|
59
|
+
} & {
|
|
60
|
+
as?: React.ElementType;
|
|
61
|
+
}, "as" | keyof import("react").SVGProps<SVGGElement>> & {
|
|
62
|
+
theme?: import("@callstack/react-theme-provider").$DeepPartial<import("../theme").Theme> | undefined;
|
|
63
|
+
}> & import("@callstack/react-theme-provider/typings/hoist-non-react-statics").NonReactStatics<import("react").ComponentType<import("react").SVGProps<SVGGElement> & {
|
|
64
|
+
theme: import("../theme").Theme;
|
|
65
|
+
} & {
|
|
66
|
+
as?: React.ElementType;
|
|
67
|
+
}> & {
|
|
68
|
+
__wyw_meta: unknown;
|
|
69
|
+
} & import("react").FunctionComponent<import("react").SVGProps<SVGGElement> & {
|
|
70
|
+
theme: import("../theme").Theme;
|
|
71
|
+
} & {
|
|
72
|
+
as?: React.ElementType;
|
|
73
|
+
}>, {}>;
|
|
48
74
|
/**
|
|
49
75
|
* Per-category hit target, rendered above the marks.
|
|
50
76
|
*
|
|
@@ -54,6 +80,15 @@ export declare const ChartArcPath: import("@linaria/react").StyledComponent<impo
|
|
|
54
80
|
* still instead of chasing each segment.
|
|
55
81
|
*/
|
|
56
82
|
export declare const ChartHitTarget: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGRectElement> & Record<never, unknown>>;
|
|
83
|
+
/**
|
|
84
|
+
* The radial equivalent, reusing the arc's own `d`.
|
|
85
|
+
*
|
|
86
|
+
* A pie has no category axis to band, so its targets are the arc shapes
|
|
87
|
+
* themselves — drawn again, transparent, above the marks. That keeps hit
|
|
88
|
+
* testing and focus identical across all three charts instead of the pie
|
|
89
|
+
* hit-testing its visible arcs directly.
|
|
90
|
+
*/
|
|
91
|
+
export declare const ChartHitTargetPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
|
|
57
92
|
/** Full-plot backdrop; clicking it clears a pinned category. */
|
|
58
93
|
export declare const ChartBackdrop: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGRectElement> & Record<never, unknown>>;
|
|
59
94
|
/**
|