@galaxy-io/dls 1.5.10 → 1.5.12
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 +26 -2
- package/dist/charts/BarChart.js +69 -14
- package/dist/charts/ChartCategoryTargets.d.ts +11 -2
- package/dist/charts/ChartCategoryTargets.js +3 -2
- package/dist/charts/ChartFrame.d.ts +20 -3
- package/dist/charts/ChartFrame.js +12 -5
- package/dist/charts/ChartMarkTargets.d.ts +30 -0
- package/dist/charts/ChartMarkTargets.js +24 -0
- package/dist/charts/ChartPrimitives.d.ts +21 -6
- package/dist/charts/ChartPrimitives.js +76 -53
- package/dist/charts/LineChart.d.ts +21 -2
- package/dist/charts/LineChart.js +34 -2
- package/dist/charts/PieChart.d.ts +23 -2
- package/dist/charts/PieChart.js +16 -1
- package/dist/charts/chartTheme.js +1 -1
- package/dist/charts/types.d.ts +83 -1
- package/dist/charts/types.js +4 -0
- package/dist/charts/useChartInteraction.d.ts +47 -8
- package/dist/charts/useChartInteraction.js +65 -2
- package/dist/styles.css +4 -4
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BarChartNormalization } from "./types";
|
|
2
|
-
import type { BarChartGroupDatum, BaseCartesianChartProps, ChartSeriesStyles, ChartSize } from "./types";
|
|
2
|
+
import type { BarChartGroupDatum, BaseCartesianChartProps, ChartSelectionEvent, ChartSelectionInput, ChartSeriesStyles, ChartSize } from "./types";
|
|
3
3
|
export type BarChartProps<TMetric extends string, TComponentKey extends string = string> = BaseCartesianChartProps & ChartSize & {
|
|
4
4
|
/**
|
|
5
5
|
* Presentation for every metric, keyed by metric.
|
|
@@ -16,6 +16,30 @@ export type BarChartProps<TMetric extends string, TComponentKey extends string =
|
|
|
16
16
|
/** Tint the active category's full band, marking the hover position even
|
|
17
17
|
* where there is no data. @default true */
|
|
18
18
|
showCursor?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Fires when a bar segment or a category is clicked. Supplying it is what
|
|
21
|
+
* makes the plot clickable.
|
|
22
|
+
*
|
|
23
|
+
* A click resolves to the finest thing under the pointer: a segment reports
|
|
24
|
+
* its own key and value, while the gutters, the plot above a short bar and
|
|
25
|
+
* the hairline seams between segments fall back to the category alone.
|
|
26
|
+
*
|
|
27
|
+
* `seriesKey` follows the same rule the legend and the mute axis do —
|
|
28
|
+
* components when the chart is stacked, metrics when it is not — so all
|
|
29
|
+
* three always name the same dimension.
|
|
30
|
+
*/
|
|
31
|
+
onSelect?: (event: ChartSelectionEvent<NoInfer<TMetric> | NoInfer<TComponentKey>>) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Which marks read as selected. Matching bars hold full strength, the rest
|
|
34
|
+
* recede, and each selected category takes a persistent band — the hover
|
|
35
|
+
* tint one step firmer, held rather than previewed.
|
|
36
|
+
*
|
|
37
|
+
* Presentational and never written by the chart: the state belongs to
|
|
38
|
+
* whatever the selection is filtering, so the two cannot drift apart. Feed
|
|
39
|
+
* an {@link onSelect} event straight back, or describe a broader set —
|
|
40
|
+
* `{ seriesKey: "failed" }` selects every failure on the axis.
|
|
41
|
+
*/
|
|
42
|
+
selection?: ChartSelectionInput<NoInfer<TMetric> | NoInfer<TComponentKey>>;
|
|
19
43
|
};
|
|
20
44
|
/**
|
|
21
45
|
* Bar chart with a three-level data model.
|
|
@@ -24,5 +48,5 @@ export type BarChartProps<TMetric extends string, TComponentKey extends string =
|
|
|
24
48
|
* `bars` in a group render side by side, several `components` in a bar stack,
|
|
25
49
|
* and a chart doing both at once needs no extra configuration.
|
|
26
50
|
*/
|
|
27
|
-
declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, showCursor, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
|
|
51
|
+
declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, showCursor, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
|
|
28
52
|
export default BarChart;
|
package/dist/charts/BarChart.js
CHANGED
|
@@ -17,7 +17,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
* `bars` in a group render side by side, several `components` in a bar stack,
|
|
18
18
|
* and a chart doing both at once needs no extra configuration.
|
|
19
19
|
*/
|
|
20
|
-
var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization = BarChartNormalization.NONE, showCursor = true, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
|
|
20
|
+
var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization = BarChartNormalization.NONE, showCursor = true, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
|
|
21
21
|
const containerRef = useRef(null);
|
|
22
22
|
const colors = useSeriesColorResolver(series);
|
|
23
23
|
/**
|
|
@@ -107,6 +107,7 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
107
107
|
seriesKeys: legendKeys,
|
|
108
108
|
categoryKeys,
|
|
109
109
|
categoryMuteState: ChartMarkState.DIMMED,
|
|
110
|
+
selection,
|
|
110
111
|
isDisabled: noTooltip || isLoading || isEmpty,
|
|
111
112
|
isLoading
|
|
112
113
|
});
|
|
@@ -144,6 +145,47 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
144
145
|
width: band.width,
|
|
145
146
|
height: band.height
|
|
146
147
|
})), [geometry.bands]);
|
|
148
|
+
/**
|
|
149
|
+
* One clickable target per drawn rectangle, over the bands.
|
|
150
|
+
*
|
|
151
|
+
* The rects already carry everything a click needs, so the target is exactly
|
|
152
|
+
* the mark — no inflation to make a thin segment easier to hit. Growing one
|
|
153
|
+
* would overlap the neighbour whose edge the geometry snapped to the same
|
|
154
|
+
* pixel, and a segment too small to hit still lands on the band beneath and
|
|
155
|
+
* selects its category, which is the honest answer for a sliver.
|
|
156
|
+
*/
|
|
157
|
+
const markTargets = useMemo(() => geometry.rects.map((rect) => ({
|
|
158
|
+
key: rect.id,
|
|
159
|
+
categoryIndex: rect.groupIndex,
|
|
160
|
+
shape: ChartTargetShape.RECT,
|
|
161
|
+
x: rect.x,
|
|
162
|
+
y: rect.y,
|
|
163
|
+
width: rect.width,
|
|
164
|
+
height: rect.height
|
|
165
|
+
})), [geometry.rects]);
|
|
166
|
+
const handleCategorySelect = useCallback((index) => {
|
|
167
|
+
const group = groups[index];
|
|
168
|
+
if (!group || !onSelect) return;
|
|
169
|
+
onSelect({
|
|
170
|
+
categoryIndex: index,
|
|
171
|
+
categoryKey: group.label
|
|
172
|
+
});
|
|
173
|
+
}, [groups, onSelect]);
|
|
174
|
+
const handleMarkSelect = useCallback((index) => {
|
|
175
|
+
const rect = geometry.rects[index];
|
|
176
|
+
if (!rect || !onSelect) return;
|
|
177
|
+
onSelect({
|
|
178
|
+
categoryIndex: rect.groupIndex,
|
|
179
|
+
categoryKey: groups[rect.groupIndex]?.label ?? rect.metric,
|
|
180
|
+
seriesKey: isStacked ? rect.componentKey : rect.metric,
|
|
181
|
+
value: rect.value
|
|
182
|
+
});
|
|
183
|
+
}, [
|
|
184
|
+
geometry.rects,
|
|
185
|
+
groups,
|
|
186
|
+
isStacked,
|
|
187
|
+
onSelect
|
|
188
|
+
]);
|
|
147
189
|
return /* @__PURE__ */ jsxs(ChartFrame, {
|
|
148
190
|
containerRef,
|
|
149
191
|
dimensions,
|
|
@@ -178,6 +220,9 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
178
220
|
tooltipRenderer,
|
|
179
221
|
tooltipMaxItems,
|
|
180
222
|
categoryTargets,
|
|
223
|
+
markTargets,
|
|
224
|
+
onCategorySelect: onSelect && handleCategorySelect,
|
|
225
|
+
onMarkSelect: onSelect && handleMarkSelect,
|
|
181
226
|
anchorHalfWidth: categoryStep / 2,
|
|
182
227
|
isEmpty,
|
|
183
228
|
isLoading,
|
|
@@ -185,19 +230,29 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
|
|
|
185
230
|
showLegend,
|
|
186
231
|
legendItems,
|
|
187
232
|
legendMaxItems,
|
|
188
|
-
children: [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
233
|
+
children: [
|
|
234
|
+
geometry.bands.filter((band) => interaction.selectedCategoryIndices.has(band.groupIndex)).map((band) => /* @__PURE__ */ jsx(ChartCursorBand, {
|
|
235
|
+
x: band.x,
|
|
236
|
+
y: band.y,
|
|
237
|
+
width: band.width,
|
|
238
|
+
height: band.height,
|
|
239
|
+
"data-selected": "true"
|
|
240
|
+
}, `selected:${band.groupIndex}`)),
|
|
241
|
+
showCursor && activeBand && /* @__PURE__ */ jsx(ChartCursorBand, {
|
|
242
|
+
x: activeBand.x,
|
|
243
|
+
y: activeBand.y,
|
|
244
|
+
width: activeBand.width,
|
|
245
|
+
height: activeBand.height
|
|
246
|
+
}),
|
|
247
|
+
geometry.rects.map((rect) => /* @__PURE__ */ jsx(ChartBarPath, {
|
|
248
|
+
d: roundedRectPath(rect.x, rect.y, rect.width, rect.height, rect.radius, rect.corners),
|
|
249
|
+
fill: rect.color,
|
|
250
|
+
"data-state": interaction.getMarkState({
|
|
251
|
+
categoryIndex: rect.groupIndex,
|
|
252
|
+
seriesKey: isStacked ? rect.componentKey : rect.metric
|
|
253
|
+
})
|
|
254
|
+
}, rect.id))
|
|
255
|
+
]
|
|
201
256
|
});
|
|
202
257
|
};
|
|
203
258
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChartCategoryTarget } from "./types";
|
|
2
2
|
import type { ChartInteraction } from "./useChartInteraction";
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The base hit layer: one transparent target per category, above the marks.
|
|
5
5
|
*
|
|
6
6
|
* Shared by all three charts so hit testing is spelled once. Targets carry no
|
|
7
7
|
* roles and no tab stops — the plot is `role="img"` and its accessible story is
|
|
@@ -10,10 +10,19 @@ import type { ChartInteraction } from "./useChartInteraction";
|
|
|
10
10
|
* Every category is hoverable, including one with nothing to show: the cursor
|
|
11
11
|
* still marks where the pointer is over an empty stretch of axis, and the
|
|
12
12
|
* tooltip opens with a "No data" body so the hover always gets an answer.
|
|
13
|
+
*
|
|
14
|
+
* **Only `onMouseEnter` lives here; leaving is the target group's job.** Per-target
|
|
15
|
+
* leave handlers fire on every crossing *within* the plot, so the state fell to
|
|
16
|
+
* null and back on each one — a wasted render between neighbouring bands, and a
|
|
17
|
+
* visible flicker once the mark layer above added a second boundary inside a
|
|
18
|
+
* single category. The group's leave fires exactly once, when the pointer
|
|
19
|
+
* actually leaves the plot.
|
|
13
20
|
*/
|
|
14
21
|
interface ChartCategoryTargetsProps {
|
|
15
22
|
targets: ChartCategoryTarget[];
|
|
16
23
|
interaction: ChartInteraction;
|
|
24
|
+
/** Supplying this makes the category clickable, cursor included. */
|
|
25
|
+
onSelect?: (categoryIndex: number) => void;
|
|
17
26
|
}
|
|
18
|
-
declare const ChartCategoryTargets: ({ targets, interaction }: ChartCategoryTargetsProps) => import("react").JSX.Element;
|
|
27
|
+
declare const ChartCategoryTargets: ({ targets, interaction, onSelect }: ChartCategoryTargetsProps) => import("react").JSX.Element;
|
|
19
28
|
export default ChartCategoryTargets;
|
|
@@ -3,10 +3,11 @@ import { ChartHitTarget, ChartHitTargetPath } from "./ChartPrimitives.js";
|
|
|
3
3
|
import { match } from "ts-pattern";
|
|
4
4
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/charts/ChartCategoryTargets.tsx
|
|
6
|
-
var ChartCategoryTargets = ({ targets, interaction }) => /* @__PURE__ */ jsx(Fragment, { children: targets.map((target, index) => {
|
|
6
|
+
var ChartCategoryTargets = ({ targets, interaction, onSelect }) => /* @__PURE__ */ jsx(Fragment, { children: targets.map((target, index) => {
|
|
7
7
|
const shared = {
|
|
8
8
|
onMouseEnter: () => interaction.onCategoryEnter(index),
|
|
9
|
-
|
|
9
|
+
onClick: onSelect === void 0 ? void 0 : () => onSelect(index),
|
|
10
|
+
"data-clickable": onSelect === void 0 ? void 0 : "true"
|
|
10
11
|
};
|
|
11
12
|
return match(target).with({ shape: ChartTargetShape.RECT }, (rect) => /* @__PURE__ */ jsx(ChartHitTarget, {
|
|
12
13
|
x: rect.x,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { ChartKind, ChartLegendPlacement } from "./types";
|
|
3
|
-
import type { ChartCategoryTarget, ChartLegendItem, ChartPlotTooltipModel, ChartTooltipRenderer } from "./types";
|
|
3
|
+
import type { ChartCategoryTarget, ChartLegendItem, ChartMarkTarget, ChartPlotTooltipModel, ChartTooltipRenderer } from "./types";
|
|
4
4
|
import type { ChartDimensions } from "./useChartDimensions";
|
|
5
5
|
import type { ChartInteraction } from "./useChartInteraction";
|
|
6
6
|
import type { ChartTooltipVerticalAlign } from "./useChartTooltipPosition";
|
|
@@ -87,8 +87,25 @@ export interface ChartFrameProps {
|
|
|
87
87
|
* plot passes.
|
|
88
88
|
*/
|
|
89
89
|
categoryTargets?: ChartCategoryTarget[];
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* One target per rendered mark, painted over {@link categoryTargets} so a
|
|
92
|
+
* click can name a segment rather than only its column. Omitted by the charts
|
|
93
|
+
* whose marks are not separately clickable — see `ChartMarkTargets`.
|
|
94
|
+
*/
|
|
95
|
+
markTargets?: ChartMarkTarget[];
|
|
96
|
+
/** Applied to both target layers. Radial charts pass their centre translate. */
|
|
91
97
|
categoryTargetsTransform?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Click on a category, by index. Supplying it is what makes the plot
|
|
100
|
+
* clickable at all — the cursor and the mark layer's handler both follow it.
|
|
101
|
+
*
|
|
102
|
+
* Index-based, because building the event needs the chart's own geometry and
|
|
103
|
+
* vocabulary. The frame knows neither, exactly as with
|
|
104
|
+
* {@link buildCategoryTooltip}.
|
|
105
|
+
*/
|
|
106
|
+
onCategorySelect?: (categoryIndex: number) => void;
|
|
107
|
+
/** Click on one mark, by its index in {@link markTargets}. */
|
|
108
|
+
onMarkSelect?: (markIndex: number) => void;
|
|
92
109
|
/** Half a category's width, so the tooltip sits beside the marks. */
|
|
93
110
|
anchorHalfWidth?: number;
|
|
94
111
|
/** Radial charts pass their centre, in plot coordinates. */
|
|
@@ -111,5 +128,5 @@ export interface ChartFrameProps {
|
|
|
111
128
|
*/
|
|
112
129
|
legendInset?: number;
|
|
113
130
|
}
|
|
114
|
-
declare const ChartFrame: ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip, buildCategoryTooltip, tooltipRenderer, tooltipMaxItems, categoryTargets, categoryTargetsTransform, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading, contentWhenEmpty, showLegend, legendItems, legendMaxItems, legendPlacement, legendInset, }: ChartFrameProps) => import("react").JSX.Element;
|
|
131
|
+
declare const ChartFrame: ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip, buildCategoryTooltip, tooltipRenderer, tooltipMaxItems, categoryTargets, markTargets, categoryTargetsTransform, onCategorySelect, onMarkSelect, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading, contentWhenEmpty, showLegend, legendItems, legendMaxItems, legendPlacement, legendInset, }: ChartFrameProps) => import("react").JSX.Element;
|
|
115
132
|
export default ChartFrame;
|
|
@@ -4,12 +4,13 @@ import { ChartPlotBox, ChartRoot, ChartSvg } from "./ChartPrimitives.js";
|
|
|
4
4
|
import ChartCategoryTargets from "./ChartCategoryTargets.js";
|
|
5
5
|
import ChartEmptyState from "./ChartEmptyState.js";
|
|
6
6
|
import ChartLegend from "./ChartLegend.js";
|
|
7
|
+
import ChartMarkTargets from "./ChartMarkTargets.js";
|
|
7
8
|
import ChartTooltip from "./ChartTooltip.js";
|
|
8
9
|
import { match } from "ts-pattern";
|
|
9
10
|
import { useId, useMemo } from "react";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
12
|
//#region src/charts/ChartFrame.tsx
|
|
12
|
-
var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip = false, buildCategoryTooltip, tooltipRenderer, tooltipMaxItems, categoryTargets, categoryTargetsTransform, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading = false, contentWhenEmpty, showLegend = false, legendItems, legendMaxItems, legendPlacement = ChartLegendPlacement.BOTTOM, legendInset }) => {
|
|
13
|
+
var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip = false, buildCategoryTooltip, tooltipRenderer, tooltipMaxItems, categoryTargets, markTargets, categoryTargetsTransform, onCategorySelect, onMarkSelect, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading = false, contentWhenEmpty, showLegend = false, legendItems, legendMaxItems, legendPlacement = ChartLegendPlacement.BOTTOM, legendInset }) => {
|
|
13
14
|
const { width, height, margin, isMeasured, reservedHeight, reservedAspectRatio } = dimensions;
|
|
14
15
|
const generatedId = useId();
|
|
15
16
|
const descriptionId = ariaDescription ? `${generatedId}-desc` : void 0;
|
|
@@ -83,12 +84,18 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
|
|
|
83
84
|
children: [
|
|
84
85
|
axes,
|
|
85
86
|
/* @__PURE__ */ jsx("g", { children }),
|
|
86
|
-
hasTargets && categoryTargets && /* @__PURE__ */
|
|
87
|
+
hasTargets && categoryTargets && /* @__PURE__ */ jsxs("g", {
|
|
87
88
|
transform: categoryTargetsTransform,
|
|
88
|
-
|
|
89
|
+
onMouseLeave: interaction.onCategoryLeave,
|
|
90
|
+
children: [/* @__PURE__ */ jsx(ChartCategoryTargets, {
|
|
89
91
|
targets: categoryTargets,
|
|
90
|
-
interaction
|
|
91
|
-
|
|
92
|
+
interaction,
|
|
93
|
+
onSelect: onCategorySelect
|
|
94
|
+
}), onMarkSelect && markTargets && markTargets.length > 0 && /* @__PURE__ */ jsx(ChartMarkTargets, {
|
|
95
|
+
targets: markTargets,
|
|
96
|
+
interaction,
|
|
97
|
+
onSelect: onMarkSelect
|
|
98
|
+
})]
|
|
92
99
|
})
|
|
93
100
|
]
|
|
94
101
|
})]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ChartMarkTarget } from "./types";
|
|
2
|
+
import type { ChartInteraction } from "./useChartInteraction";
|
|
3
|
+
/**
|
|
4
|
+
* The mark hit layer: one transparent target per rendered mark, above the
|
|
5
|
+
* category targets.
|
|
6
|
+
*
|
|
7
|
+
* Exists so a click can name a single segment — the red block of the 00:00 bar
|
|
8
|
+
* — where the category layer beneath can only name the column. Rendered by the
|
|
9
|
+
* charts that have marks worth distinguishing: BarChart does, LineChart has no
|
|
10
|
+
* segments to hit, and PieChart's category targets already *are* its arcs.
|
|
11
|
+
*
|
|
12
|
+
* **Hover is not refined here.** Each target reports its own category on enter,
|
|
13
|
+
* exactly as the band underneath would, so the tooltip reads the same index
|
|
14
|
+
* whichever layer the pointer is over and holds still while crossing a stack.
|
|
15
|
+
* The banded hover is deliberate — it makes a two-pixel segment as easy to
|
|
16
|
+
* hover as a two-hundred-pixel one — and this layer only adds a finer *click*.
|
|
17
|
+
*
|
|
18
|
+
* Anything the marks do not cover falls through to the category target: the
|
|
19
|
+
* gutters, the plot above a short bar, and the hairline gaps between stack
|
|
20
|
+
* segments. That is the documented fallback, and it is why a segment too small
|
|
21
|
+
* to hit still selects something sensible.
|
|
22
|
+
*/
|
|
23
|
+
interface ChartMarkTargetsProps {
|
|
24
|
+
targets: ChartMarkTarget[];
|
|
25
|
+
interaction: ChartInteraction;
|
|
26
|
+
/** Supplying this makes the marks clickable, cursor included. */
|
|
27
|
+
onSelect?: (markIndex: number) => void;
|
|
28
|
+
}
|
|
29
|
+
declare const ChartMarkTargets: ({ targets, interaction, onSelect }: ChartMarkTargetsProps) => import("react").JSX.Element;
|
|
30
|
+
export default ChartMarkTargets;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChartTargetShape } from "./types.js";
|
|
2
|
+
import { ChartHitTarget, ChartHitTargetPath } from "./ChartPrimitives.js";
|
|
3
|
+
import { match } from "ts-pattern";
|
|
4
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/charts/ChartMarkTargets.tsx
|
|
6
|
+
var ChartMarkTargets = ({ targets, interaction, onSelect }) => /* @__PURE__ */ jsx(Fragment, { children: targets.map((target, index) => {
|
|
7
|
+
const shared = {
|
|
8
|
+
onMouseEnter: () => interaction.onCategoryEnter(target.categoryIndex),
|
|
9
|
+
onClick: onSelect === void 0 ? void 0 : () => onSelect(index),
|
|
10
|
+
"data-clickable": onSelect === void 0 ? void 0 : "true"
|
|
11
|
+
};
|
|
12
|
+
return match(target).with({ shape: ChartTargetShape.RECT }, (rect) => /* @__PURE__ */ jsx(ChartHitTarget, {
|
|
13
|
+
x: rect.x,
|
|
14
|
+
y: rect.y,
|
|
15
|
+
width: rect.width,
|
|
16
|
+
height: rect.height,
|
|
17
|
+
...shared
|
|
18
|
+
}, rect.key)).with({ shape: ChartTargetShape.PATH }, (path) => /* @__PURE__ */ jsx(ChartHitTargetPath, {
|
|
19
|
+
d: path.d,
|
|
20
|
+
...shared
|
|
21
|
+
}, path.key)).exhaustive();
|
|
22
|
+
}) });
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ChartMarkTargets as default };
|
|
@@ -51,15 +51,19 @@ export declare const ChartAreaPath: import("@linaria/react").StyledComponent<imp
|
|
|
51
51
|
export declare const ChartPointCircle: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGCircleElement> & Record<never, unknown>>;
|
|
52
52
|
export declare const ChartArcPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* Hit target, rendered above the marks. Used for both layers — one per category
|
|
55
|
+
* beneath, one per mark above.
|
|
55
56
|
*
|
|
56
57
|
* Hit testing the full column rather than the bars themselves means a two-pixel
|
|
57
58
|
* bar is as easy to hover as a two-hundred-pixel one, and crossing between
|
|
58
|
-
* segments inside a category
|
|
59
|
+
* segments inside a category changes nothing the tooltip reads — so it holds
|
|
59
60
|
* still instead of chasing each segment.
|
|
60
61
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
* The pointer cursor is conditional because the promise is: it appears only
|
|
63
|
+
* where a click actually reports something, and a chart with no `onSelect`
|
|
64
|
+
* keeps the default cursor rather than advertising an interaction it does not
|
|
65
|
+
* have. No focus treatment either — the plot is `role="img"` and its marks are
|
|
66
|
+
* not in the accessibility tree, so there is nothing here to focus yet.
|
|
63
67
|
*/
|
|
64
68
|
export declare const ChartHitTarget: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGRectElement> & Record<never, unknown>>;
|
|
65
69
|
/**
|
|
@@ -93,7 +97,14 @@ export declare const ChartGridLine: import("react").ComponentType<Pick<import("r
|
|
|
93
97
|
} & {
|
|
94
98
|
as?: React.ElementType;
|
|
95
99
|
}>, {}>;
|
|
96
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Vertical rule marking the hovered category on a line chart.
|
|
102
|
+
*
|
|
103
|
+
* `data-selected` steps it up the same neutral ramp rather than introducing a
|
|
104
|
+
* hue: a selection is the *same* mark held rather than previewed, and a colour
|
|
105
|
+
* change would read as a different kind of thing. The state is an attribute
|
|
106
|
+
* rather than a `$` prop for the reason at the top of this file.
|
|
107
|
+
*/
|
|
97
108
|
export declare const ChartCursorLine: import("react").ComponentType<Pick<import("react").SVGLineElementAttributes<SVGLineElement> & {
|
|
98
109
|
theme: import("../theme").Theme;
|
|
99
110
|
} & {
|
|
@@ -115,7 +126,11 @@ export declare const ChartCursorLine: import("react").ComponentType<Pick<import(
|
|
|
115
126
|
* Full-height tint over the hovered category's band on a bar chart — the bar
|
|
116
127
|
* counterpart to {@link ChartCursorLine}. A rule reads oddly threaded through
|
|
117
128
|
* wide filled marks; a quiet band behind them marks the same position without
|
|
118
|
-
* touching them.
|
|
129
|
+
* touching them.
|
|
130
|
+
*
|
|
131
|
+
* 4% of the foreground, so it survives both themes. Deliberately fainter than
|
|
132
|
+
* the line chart's rule: a band is a large area where a rule is a hairline, and
|
|
133
|
+
* the same value that reads as quiet on one shouts on the other.
|
|
119
134
|
*/
|
|
120
135
|
export declare const ChartCursorBand: import("react").ComponentType<Pick<import("react").SVGProps<SVGRectElement> & {
|
|
121
136
|
theme: import("../theme").Theme;
|
|
@@ -128,15 +128,19 @@ var ChartArcPath = /*#__PURE__*/ styled("path")({
|
|
|
128
128
|
propsAsIs: true
|
|
129
129
|
});
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
131
|
+
* Hit target, rendered above the marks. Used for both layers — one per category
|
|
132
|
+
* beneath, one per mark above.
|
|
132
133
|
*
|
|
133
134
|
* Hit testing the full column rather than the bars themselves means a two-pixel
|
|
134
135
|
* bar is as easy to hover as a two-hundred-pixel one, and crossing between
|
|
135
|
-
* segments inside a category
|
|
136
|
+
* segments inside a category changes nothing the tooltip reads — so it holds
|
|
136
137
|
* still instead of chasing each segment.
|
|
137
138
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
139
|
+
* The pointer cursor is conditional because the promise is: it appears only
|
|
140
|
+
* where a click actually reports something, and a chart with no `onSelect`
|
|
141
|
+
* keeps the default cursor rather than advertising an interaction it does not
|
|
142
|
+
* have. No focus treatment either — the plot is `role="img"` and its marks are
|
|
143
|
+
* not in the accessibility tree, so there is nothing here to focus yet.
|
|
140
144
|
*/
|
|
141
145
|
var ChartHitTarget = /*#__PURE__*/ styled("rect")({
|
|
142
146
|
name: "ChartHitTarget",
|
|
@@ -168,26 +172,45 @@ var ChartGridLine = withTheme(/*#__PURE__*/ styled("line")({
|
|
|
168
172
|
propsAsIs: true,
|
|
169
173
|
vars: { "c17guewt-0": [_exp15()] }
|
|
170
174
|
}));
|
|
171
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Vertical rule marking the hovered category on a line chart.
|
|
177
|
+
*
|
|
178
|
+
* `data-selected` steps it up the same neutral ramp rather than introducing a
|
|
179
|
+
* hue: a selection is the *same* mark held rather than previewed, and a colour
|
|
180
|
+
* change would read as a different kind of thing. The state is an attribute
|
|
181
|
+
* rather than a `$` prop for the reason at the top of this file.
|
|
182
|
+
*/
|
|
172
183
|
var _exp16 = () => ({ theme }) => theme.color.border.secondary;
|
|
184
|
+
var _exp17 = () => ({ theme }) => theme.color.opacity.bw32Alt;
|
|
173
185
|
var ChartCursorLine = withTheme(/*#__PURE__*/ styled("line")({
|
|
174
186
|
name: "ChartCursorLine",
|
|
175
187
|
class: "cblgiyf",
|
|
176
188
|
propsAsIs: true,
|
|
177
|
-
vars: {
|
|
189
|
+
vars: {
|
|
190
|
+
"cblgiyf-0": [_exp16()],
|
|
191
|
+
"cblgiyf-1": [_exp17()]
|
|
192
|
+
}
|
|
178
193
|
}));
|
|
179
194
|
/**
|
|
180
195
|
* Full-height tint over the hovered category's band on a bar chart — the bar
|
|
181
196
|
* counterpart to {@link ChartCursorLine}. A rule reads oddly threaded through
|
|
182
197
|
* wide filled marks; a quiet band behind them marks the same position without
|
|
183
|
-
* touching them.
|
|
198
|
+
* touching them.
|
|
199
|
+
*
|
|
200
|
+
* 4% of the foreground, so it survives both themes. Deliberately fainter than
|
|
201
|
+
* the line chart's rule: a band is a large area where a rule is a hairline, and
|
|
202
|
+
* the same value that reads as quiet on one shouts on the other.
|
|
184
203
|
*/
|
|
185
|
-
var
|
|
204
|
+
var _exp18 = () => ({ theme }) => theme.color.opacity.b4Alt;
|
|
205
|
+
var _exp19 = () => ({ theme }) => theme.color.opacity.bw8Alt;
|
|
186
206
|
var ChartCursorBand = withTheme(/*#__PURE__*/ styled("rect")({
|
|
187
207
|
name: "ChartCursorBand",
|
|
188
208
|
class: "c1lr03cc",
|
|
189
209
|
propsAsIs: true,
|
|
190
|
-
vars: {
|
|
210
|
+
vars: {
|
|
211
|
+
"c1lr03cc-0": [_exp18()],
|
|
212
|
+
"c1lr03cc-1": [_exp19()]
|
|
213
|
+
}
|
|
191
214
|
}));
|
|
192
215
|
var ChartAxisLabelBox = /*#__PURE__*/ styled("foreignObject")({
|
|
193
216
|
name: "ChartAxisLabelBox",
|
|
@@ -201,21 +224,21 @@ var ChartAxisLabelBox = /*#__PURE__*/ styled("foreignObject")({
|
|
|
201
224
|
* than teleporting, and it stays mounted while hidden so the fade can actually
|
|
202
225
|
* run — unmounting on hide is why a fade-out never appears.
|
|
203
226
|
*/
|
|
204
|
-
var
|
|
205
|
-
var
|
|
206
|
-
var
|
|
207
|
-
var
|
|
208
|
-
var
|
|
227
|
+
var _exp20 = () => ({ $x }) => `${$x}px`;
|
|
228
|
+
var _exp21 = () => ({ $y }) => `${$y}px`;
|
|
229
|
+
var _exp24 = () => ({ theme }) => theme.color.border.primary;
|
|
230
|
+
var _exp25 = () => ({ theme }) => theme.color.background.tertiary;
|
|
231
|
+
var _exp26 = () => ({ $isVisible }) => $isVisible ? 1 : 0;
|
|
209
232
|
var ChartTooltipPositioner = withTheme(/*#__PURE__*/ styled("div")({
|
|
210
233
|
name: "ChartTooltipPositioner",
|
|
211
234
|
class: "c1ukq11d",
|
|
212
235
|
propsAsIs: false,
|
|
213
236
|
vars: {
|
|
214
|
-
"c1ukq11d-0": [
|
|
215
|
-
"c1ukq11d-1": [
|
|
216
|
-
"c1ukq11d-2": [
|
|
217
|
-
"c1ukq11d-3": [
|
|
218
|
-
"c1ukq11d-4": [
|
|
237
|
+
"c1ukq11d-0": [_exp20()],
|
|
238
|
+
"c1ukq11d-1": [_exp21()],
|
|
239
|
+
"c1ukq11d-2": [_exp24()],
|
|
240
|
+
"c1ukq11d-3": [_exp25()],
|
|
241
|
+
"c1ukq11d-4": [_exp26()]
|
|
219
242
|
}
|
|
220
243
|
}));
|
|
221
244
|
/**
|
|
@@ -252,58 +275,58 @@ var ChartTooltipValueCell = /*#__PURE__*/ styled("div")({
|
|
|
252
275
|
* of the DLS. `pointer-events: none` keeps it from stealing hover from the arc
|
|
253
276
|
* behind it.
|
|
254
277
|
*/
|
|
255
|
-
var
|
|
256
|
-
var
|
|
257
|
-
var
|
|
278
|
+
var _exp30 = () => ({ $centerX }) => `${$centerX}px`;
|
|
279
|
+
var _exp31 = () => ({ $centerY }) => `${$centerY}px`;
|
|
280
|
+
var _exp33 = () => ({ $diameter }) => `${$diameter}px`;
|
|
258
281
|
var ChartCenterBox = /*#__PURE__*/ styled("div")({
|
|
259
282
|
name: "ChartCenterBox",
|
|
260
283
|
class: "cz6jod1",
|
|
261
284
|
propsAsIs: false,
|
|
262
285
|
vars: {
|
|
263
|
-
"cz6jod1-0": [
|
|
264
|
-
"cz6jod1-1": [
|
|
265
|
-
"cz6jod1-2": [
|
|
286
|
+
"cz6jod1-0": [_exp30()],
|
|
287
|
+
"cz6jod1-1": [_exp31()],
|
|
288
|
+
"cz6jod1-2": [_exp33()]
|
|
266
289
|
}
|
|
267
290
|
});
|
|
268
291
|
/**
|
|
269
292
|
* Sits slightly above centre, over where the bars would be, so the axes stay
|
|
270
293
|
* legible underneath.
|
|
271
294
|
*/
|
|
272
|
-
var
|
|
273
|
-
var
|
|
295
|
+
var _exp34 = () => ({ theme }) => theme.color.border.primary;
|
|
296
|
+
var _exp35 = () => ({ theme }) => theme.color.background.primary;
|
|
274
297
|
var ChartEmptyOverlay = withTheme(/*#__PURE__*/ styled("div")({
|
|
275
298
|
name: "ChartEmptyOverlay",
|
|
276
299
|
class: "c1de5mqk",
|
|
277
300
|
propsAsIs: false,
|
|
278
301
|
vars: {
|
|
279
|
-
"c1de5mqk-0": [
|
|
280
|
-
"c1de5mqk-1": [
|
|
302
|
+
"c1de5mqk-0": [_exp34()],
|
|
303
|
+
"c1de5mqk-1": [_exp35()]
|
|
281
304
|
}
|
|
282
305
|
}));
|
|
283
306
|
/**
|
|
284
307
|
* Aligns the legend under the plot rather than the value-axis gutter.
|
|
285
308
|
*/
|
|
286
|
-
var
|
|
287
|
-
var
|
|
288
|
-
var
|
|
289
|
-
var
|
|
290
|
-
var
|
|
291
|
-
var
|
|
292
|
-
var
|
|
293
|
-
var
|
|
309
|
+
var _exp36 = () => ({ $vertical }) => $vertical ? "column" : "row";
|
|
310
|
+
var _exp37 = () => ({ $vertical }) => $vertical ? "flex-start" : "center";
|
|
311
|
+
var _exp38 = () => ({ $vertical }) => $vertical ? "center" : "flex-start";
|
|
312
|
+
var _exp39 = () => ({ $vertical }) => $vertical ? "auto" : "0";
|
|
313
|
+
var _exp40 = () => ({ $vertical }) => $vertical ? "0" : "100%";
|
|
314
|
+
var _exp41 = () => ({ $vertical, $extent }) => $vertical && $extent ? `${$extent}px` : "auto";
|
|
315
|
+
var _exp42 = () => ({ $vertical }) => $vertical ? "0" : "10px";
|
|
316
|
+
var _exp43 = () => ({ $inset }) => `${$inset}px`;
|
|
294
317
|
var ChartLegendRow = /*#__PURE__*/ styled("div")({
|
|
295
318
|
name: "ChartLegendRow",
|
|
296
319
|
class: "cjirv6a",
|
|
297
320
|
propsAsIs: false,
|
|
298
321
|
vars: {
|
|
299
|
-
"cjirv6a-0": [
|
|
300
|
-
"cjirv6a-1": [
|
|
301
|
-
"cjirv6a-2": [
|
|
302
|
-
"cjirv6a-3": [
|
|
303
|
-
"cjirv6a-4": [
|
|
304
|
-
"cjirv6a-5": [
|
|
305
|
-
"cjirv6a-6": [
|
|
306
|
-
"cjirv6a-7": [
|
|
322
|
+
"cjirv6a-0": [_exp36()],
|
|
323
|
+
"cjirv6a-1": [_exp37()],
|
|
324
|
+
"cjirv6a-2": [_exp38()],
|
|
325
|
+
"cjirv6a-3": [_exp39()],
|
|
326
|
+
"cjirv6a-4": [_exp40()],
|
|
327
|
+
"cjirv6a-5": [_exp41()],
|
|
328
|
+
"cjirv6a-6": [_exp42()],
|
|
329
|
+
"cjirv6a-7": [_exp43()]
|
|
307
330
|
}
|
|
308
331
|
});
|
|
309
332
|
/**
|
|
@@ -333,15 +356,15 @@ var ChartLegendMeasureClip = /*#__PURE__*/ styled("div")({
|
|
|
333
356
|
* Absolute and hidden: it must not affect the row's layout, and measuring live
|
|
334
357
|
* entries instead would conflate "what fits" with "what is currently shown".
|
|
335
358
|
*/
|
|
336
|
-
var
|
|
337
|
-
var
|
|
359
|
+
var _exp44 = () => ({ $vertical }) => $vertical ? "column" : "row";
|
|
360
|
+
var _exp45 = () => ({ $vertical }) => $vertical ? "flex-start" : "center";
|
|
338
361
|
var ChartLegendMeasure = /*#__PURE__*/ styled("div")({
|
|
339
362
|
name: "ChartLegendMeasure",
|
|
340
363
|
class: "cgd77rv",
|
|
341
364
|
propsAsIs: false,
|
|
342
365
|
vars: {
|
|
343
|
-
"cgd77rv-0": [
|
|
344
|
-
"cgd77rv-1": [
|
|
366
|
+
"cgd77rv-0": [_exp44()],
|
|
367
|
+
"cgd77rv-1": [_exp45()]
|
|
345
368
|
}
|
|
346
369
|
});
|
|
347
370
|
/**
|
|
@@ -374,21 +397,21 @@ var ChartLegendOverflowList = /*#__PURE__*/ styled("div")({
|
|
|
374
397
|
* series to the next. Trailing only, so the first entry stays flush with the
|
|
375
398
|
* plot's left edge (or the column's top).
|
|
376
399
|
*/
|
|
377
|
-
var
|
|
400
|
+
var _exp48 = () => ({ $vertical }) => $vertical ? `0 0 8px 0` : `0 16px 0 0`;
|
|
378
401
|
var ChartLegendItemButton = /*#__PURE__*/ styled("button")({
|
|
379
402
|
name: "ChartLegendItemButton",
|
|
380
403
|
class: "ccpczg",
|
|
381
404
|
propsAsIs: false,
|
|
382
|
-
vars: { "ccpczg-0": [
|
|
405
|
+
vars: { "ccpczg-0": [_exp48()] }
|
|
383
406
|
});
|
|
384
407
|
/** The same row, when the legend is inert — no button semantics to announce.
|
|
385
408
|
* Carries the same trailing padding; see {@link ChartLegendItemButton}. */
|
|
386
|
-
var
|
|
409
|
+
var _exp50 = () => ({ $vertical }) => $vertical ? `0 0 8px 0` : `0 16px 0 0`;
|
|
387
410
|
var ChartLegendItemStatic = /*#__PURE__*/ styled("div")({
|
|
388
411
|
name: "ChartLegendItemStatic",
|
|
389
412
|
class: "c2re25h",
|
|
390
413
|
propsAsIs: false,
|
|
391
|
-
vars: { "c2re25h-0": [
|
|
414
|
+
vars: { "c2re25h-0": [_exp50()] }
|
|
392
415
|
});
|
|
393
416
|
//#endregion
|
|
394
417
|
export { ChartArcPath, ChartAreaPath, ChartAxisLabelBox, ChartBarPath, ChartCenterBox, ChartCursorBand, ChartCursorLine, ChartEmptyOverlay, ChartGridLine, ChartHitTarget, ChartHitTargetPath, ChartLegendItemButton, ChartLegendItemStatic, ChartLegendMeasure, ChartLegendMeasureClip, ChartLegendOverflowList, ChartLegendRow, ChartLinePath, ChartPlotBox, ChartPointCircle, ChartRoot, ChartSvg, ChartTooltipGrid, ChartTooltipLabelCell, ChartTooltipPositioner, ChartTooltipValueCell };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LineChartCurve } from "./types";
|
|
2
|
-
import type { BaseCartesianChartProps, ChartSeriesStyles, ChartSize, LineChartLineDatum } from "./types";
|
|
2
|
+
import type { BaseCartesianChartProps, ChartSelectionEvent, ChartSelectionInput, ChartSeriesStyles, ChartSize, LineChartLineDatum } from "./types";
|
|
3
3
|
export type LineChartProps<TMetric extends string> = BaseCartesianChartProps & ChartSize & {
|
|
4
4
|
/**
|
|
5
5
|
* Presentation for every metric, keyed by metric.
|
|
@@ -25,6 +25,25 @@ export type LineChartProps<TMetric extends string> = BaseCartesianChartProps & C
|
|
|
25
25
|
strokeWidth?: number;
|
|
26
26
|
/** Draw a vertical rule at the active category. @default true */
|
|
27
27
|
showCursor?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Fires when a category is clicked. Supplying it is what makes the plot
|
|
30
|
+
* clickable.
|
|
31
|
+
*
|
|
32
|
+
* Always the category, never a single line: hit testing is per column here,
|
|
33
|
+
* and a line has no segment to land on — the reading at a category belongs
|
|
34
|
+
* to every line at once, which is exactly what the tooltip reports. Use
|
|
35
|
+
* `seriesKey` in {@link selection} to narrow to one line.
|
|
36
|
+
*/
|
|
37
|
+
onSelect?: (event: ChartSelectionEvent<NoInfer<TMetric>>) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Which marks read as selected. Matching lines hold full strength, the rest
|
|
40
|
+
* recede, and each selected category keeps a persistent cursor with its
|
|
41
|
+
* dots — the hover rule one step firmer, held rather than previewed.
|
|
42
|
+
*
|
|
43
|
+
* Presentational and never written by the chart: the state belongs to
|
|
44
|
+
* whatever the selection is filtering, so the two cannot drift apart.
|
|
45
|
+
*/
|
|
46
|
+
selection?: ChartSelectionInput<NoInfer<TMetric>>;
|
|
28
47
|
};
|
|
29
48
|
/**
|
|
30
49
|
* Line chart over a categorical x-axis.
|
|
@@ -34,5 +53,5 @@ export type LineChartProps<TMetric extends string> = BaseCartesianChartProps & C
|
|
|
34
53
|
* what a reader wants, and it avoids the proximity search a
|
|
35
54
|
* nearest-point-to-cursor model needs.
|
|
36
55
|
*/
|
|
37
|
-
declare const LineChart: <TMetric extends string>({ series, lines, categories, width, height, fillWidth, fillHeight, aspectRatio, margin, curve, strokeWidth, showCursor, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: LineChartProps<TMetric>) => import("react").JSX.Element;
|
|
56
|
+
declare const LineChart: <TMetric extends string>({ series, lines, categories, width, height, fillWidth, fillHeight, aspectRatio, margin, curve, strokeWidth, showCursor, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: LineChartProps<TMetric>) => import("react").JSX.Element;
|
|
38
57
|
export default LineChart;
|
package/dist/charts/LineChart.js
CHANGED
|
@@ -18,7 +18,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
* what a reader wants, and it avoids the proximity search a
|
|
19
19
|
* nearest-point-to-cursor model needs.
|
|
20
20
|
*/
|
|
21
|
-
var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeight, aspectRatio, margin, curve = LineChartCurve.SMOOTH, strokeWidth, showCursor = true, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
|
|
21
|
+
var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeight, aspectRatio, margin, curve = LineChartCurve.SMOOTH, strokeWidth, showCursor = true, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
|
|
22
22
|
const containerRef = useRef(null);
|
|
23
23
|
const colors = useSeriesColorResolver(series);
|
|
24
24
|
const { dimensions, domain, valueTicks, formatValue, formatLabel } = useCartesianChartLayout({
|
|
@@ -69,6 +69,7 @@ var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeig
|
|
|
69
69
|
categoryCount: geometry.columns.length,
|
|
70
70
|
seriesKeys: legendKeys,
|
|
71
71
|
categoryKeys: geometry.categories,
|
|
72
|
+
selection,
|
|
72
73
|
isDisabled: noTooltip || isLoading || isEmpty,
|
|
73
74
|
isLoading
|
|
74
75
|
});
|
|
@@ -104,6 +105,22 @@ var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeig
|
|
|
104
105
|
width: column.bandWidth,
|
|
105
106
|
height: plotHeight
|
|
106
107
|
})), [geometry.columns, plotHeight]);
|
|
108
|
+
const handleCategorySelect = useCallback((index) => {
|
|
109
|
+
const category = geometry.categories[index];
|
|
110
|
+
if (category === void 0 || !onSelect) return;
|
|
111
|
+
onSelect({
|
|
112
|
+
categoryIndex: index,
|
|
113
|
+
categoryKey: category
|
|
114
|
+
});
|
|
115
|
+
}, [geometry.categories, onSelect]);
|
|
116
|
+
/**
|
|
117
|
+
* Columns the selection names, resolved once.
|
|
118
|
+
*
|
|
119
|
+
* These get the cursor and the dots permanently — the same treatment hovering
|
|
120
|
+
* gives, one step firmer on the same neutral ramp, which is the whole
|
|
121
|
+
* difference the held stroke has to communicate.
|
|
122
|
+
*/
|
|
123
|
+
const selectedColumns = useMemo(() => geometry.columns.filter((column) => interaction.selectedCategoryIndices.has(column.categoryIndex)), [geometry.columns, interaction.selectedCategoryIndices]);
|
|
107
124
|
return /* @__PURE__ */ jsxs(ChartFrame, {
|
|
108
125
|
containerRef,
|
|
109
126
|
dimensions,
|
|
@@ -138,6 +155,7 @@ var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeig
|
|
|
138
155
|
tooltipRenderer,
|
|
139
156
|
tooltipMaxItems,
|
|
140
157
|
categoryTargets,
|
|
158
|
+
onCategorySelect: onSelect && handleCategorySelect,
|
|
141
159
|
anchorHalfWidth: geometry.step / 2,
|
|
142
160
|
isEmpty,
|
|
143
161
|
isLoading,
|
|
@@ -152,6 +170,13 @@ var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeig
|
|
|
152
170
|
fillOpacity: LINE_AREA_OPACITY,
|
|
153
171
|
"data-state": interaction.getMarkState({ seriesKey: linePath.metric })
|
|
154
172
|
}, `${linePath.id}:area`) : null),
|
|
173
|
+
selectedColumns.map((column) => /* @__PURE__ */ jsx(ChartCursorLine, {
|
|
174
|
+
x1: column.x,
|
|
175
|
+
x2: column.x,
|
|
176
|
+
y1: 0,
|
|
177
|
+
y2: plotHeight,
|
|
178
|
+
"data-selected": "true"
|
|
179
|
+
}, `selected:${column.categoryIndex}`)),
|
|
155
180
|
showCursor && activeColumn && /* @__PURE__ */ jsx(ChartCursorLine, {
|
|
156
181
|
x1: activeColumn.x,
|
|
157
182
|
x2: activeColumn.x,
|
|
@@ -178,7 +203,14 @@ var LineChart = ({ series, lines, categories, width, height, fillWidth, fillHeig
|
|
|
178
203
|
r: LINE_POINT_RADIUS,
|
|
179
204
|
fill: point.color,
|
|
180
205
|
"data-state": interaction.getMarkState({ seriesKey: point.metric })
|
|
181
|
-
}, point.id))
|
|
206
|
+
}, point.id)),
|
|
207
|
+
selectedColumns.filter((column) => column.categoryIndex !== activeColumn?.categoryIndex).flatMap((column) => column.points.map((point) => /* @__PURE__ */ jsx(ChartPointCircle, {
|
|
208
|
+
cx: point.x,
|
|
209
|
+
cy: point.y,
|
|
210
|
+
r: LINE_POINT_RADIUS,
|
|
211
|
+
fill: point.color,
|
|
212
|
+
"data-state": interaction.getMarkState({ seriesKey: point.metric })
|
|
213
|
+
}, `selected:${point.id}`)))
|
|
182
214
|
]
|
|
183
215
|
});
|
|
184
216
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import
|
|
2
|
+
import { AGGREGATED_SLICE_KEY } from "./constants";
|
|
3
|
+
import type { BaseChartProps, ChartSelectionEvent, ChartSelectionInput, ChartSeriesStyles, ChartSize, PieChartSliceDatum } from "./types";
|
|
3
4
|
export type PieChartProps<TSlice extends string> = BaseChartProps & ChartSize & {
|
|
4
5
|
/**
|
|
5
6
|
* Presentation for every slice, keyed by slice.
|
|
@@ -32,6 +33,26 @@ export type PieChartProps<TSlice extends string> = BaseChartProps & ChartSize &
|
|
|
32
33
|
* Ignored when `innerRadiusRatio` is 0.
|
|
33
34
|
*/
|
|
34
35
|
centerContent?: ReactNode;
|
|
36
|
+
/**
|
|
37
|
+
* Fires when a slice is clicked. Supplying it is what makes the plot
|
|
38
|
+
* clickable.
|
|
39
|
+
*
|
|
40
|
+
* `categoryKey` and `seriesKey` are always the same value here: a pie's
|
|
41
|
+
* category axis *is* its series axis, and the arcs are already its hit
|
|
42
|
+
* targets, so there is no coarser fallback to take. Clicking the folded
|
|
43
|
+
* bucket reports {@link AGGREGATED_SLICE_KEY}, which the key union says
|
|
44
|
+
* outright rather than widening to `string`.
|
|
45
|
+
*/
|
|
46
|
+
onSelect?: (event: ChartSelectionEvent<NoInfer<TSlice> | typeof AGGREGATED_SLICE_KEY>) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Which arcs read as selected. Matching arcs hold full strength and the
|
|
49
|
+
* rest recede — the lone lit slice is unambiguous, so there is no band or
|
|
50
|
+
* outline on top of it.
|
|
51
|
+
*
|
|
52
|
+
* Presentational and never written by the chart: the state belongs to
|
|
53
|
+
* whatever the selection is filtering, so the two cannot drift apart.
|
|
54
|
+
*/
|
|
55
|
+
selection?: ChartSelectionInput<NoInfer<TSlice> | typeof AGGREGATED_SLICE_KEY>;
|
|
35
56
|
};
|
|
36
57
|
/**
|
|
37
58
|
* Pie and donut chart.
|
|
@@ -40,5 +61,5 @@ export type PieChartProps<TSlice extends string> = BaseChartProps & ChartSize &
|
|
|
40
61
|
* each arc is its own target. Small slices are folded into an "Other" bucket by
|
|
41
62
|
* default, which keeps every remaining arc large enough to actually hit.
|
|
42
63
|
*/
|
|
43
|
-
declare const PieChart: <TSlice extends string>({ series, slices, width, height, fillWidth, fillHeight, aspectRatio, margin, innerRadiusRatio, padAngle, cornerRadius, minSliceShare, aggregatedLabel, centerContent, valueUnit, valueFormatter, labelFormatter, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: PieChartProps<TSlice>) => import("react").JSX.Element;
|
|
64
|
+
declare const PieChart: <TSlice extends string>({ series, slices, width, height, fillWidth, fillHeight, aspectRatio, margin, innerRadiusRatio, padAngle, cornerRadius, minSliceShare, aggregatedLabel, centerContent, onSelect, selection, valueUnit, valueFormatter, labelFormatter, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: PieChartProps<TSlice>) => import("react").JSX.Element;
|
|
44
65
|
export default PieChart;
|
package/dist/charts/PieChart.js
CHANGED
|
@@ -19,7 +19,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
19
19
|
* each arc is its own target. Small slices are folded into an "Other" bucket by
|
|
20
20
|
* default, which keeps every remaining arc large enough to actually hit.
|
|
21
21
|
*/
|
|
22
|
-
var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRatio, margin, innerRadiusRatio, padAngle, cornerRadius, minSliceShare = PIE_MIN_SLICE_SHARE, aggregatedLabel, centerContent, valueUnit, valueFormatter, labelFormatter, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
|
|
22
|
+
var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRatio, margin, innerRadiusRatio, padAngle, cornerRadius, minSliceShare = PIE_MIN_SLICE_SHARE, aggregatedLabel, centerContent, onSelect, selection, valueUnit, valueFormatter, labelFormatter, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
|
|
23
23
|
const containerRef = useRef(null);
|
|
24
24
|
const colors = useSeriesColorResolver(series);
|
|
25
25
|
const dimensions = useChartDimensions({
|
|
@@ -76,6 +76,9 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
|
|
|
76
76
|
const interaction = useChartInteraction({
|
|
77
77
|
categoryCount: geometry.arcs.length,
|
|
78
78
|
seriesKeys: legendKeys,
|
|
79
|
+
categoryKeys: legendKeys,
|
|
80
|
+
enableGroupSync: false,
|
|
81
|
+
selection,
|
|
79
82
|
isDisabled: noTooltip || isLoading || isEmpty,
|
|
80
83
|
isLoading
|
|
81
84
|
});
|
|
@@ -110,6 +113,17 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
|
|
|
110
113
|
shape: ChartTargetShape.PATH,
|
|
111
114
|
d: chartArc.path
|
|
112
115
|
})), [geometry.arcs]);
|
|
116
|
+
const handleCategorySelect = useCallback((index) => {
|
|
117
|
+
const chartArc = geometry.arcs[index];
|
|
118
|
+
if (!chartArc || !onSelect) return;
|
|
119
|
+
const key = chartArc.id;
|
|
120
|
+
onSelect({
|
|
121
|
+
categoryIndex: index,
|
|
122
|
+
categoryKey: chartArc.id,
|
|
123
|
+
seriesKey: key,
|
|
124
|
+
value: chartArc.value
|
|
125
|
+
});
|
|
126
|
+
}, [geometry.arcs, onSelect]);
|
|
113
127
|
const showCenter = centerContent && (innerRadiusRatio ?? .62) > 0;
|
|
114
128
|
return /* @__PURE__ */ jsx(ChartFrame, {
|
|
115
129
|
containerRef,
|
|
@@ -126,6 +140,7 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
|
|
|
126
140
|
tooltipRenderer,
|
|
127
141
|
tooltipMaxItems,
|
|
128
142
|
categoryTargets,
|
|
143
|
+
onCategorySelect: onSelect && handleCategorySelect,
|
|
129
144
|
categoryTargetsTransform: `translate(${geometry.centerX}, ${geometry.centerY})`,
|
|
130
145
|
placeAwayFromX: geometry.centerX,
|
|
131
146
|
verticalAlign: ChartTooltipVerticalAlign.CENTERED,
|
|
@@ -13,7 +13,7 @@ import { match } from "ts-pattern";
|
|
|
13
13
|
*/
|
|
14
14
|
/** Resolve a palette slot to a concrete color for the active theme. */
|
|
15
15
|
function resolveChartPaletteColor(theme, palette) {
|
|
16
|
-
return match(palette).with(ChartPalette.PURPLE, () => theme.color.icon.purple).with(ChartPalette.PINK, () => theme.color.icon.pink).with(ChartPalette.BLUE, () => theme.color.icon.blue).with(ChartPalette.TEAL, () => theme.color.icon.teal).with(ChartPalette.LIME, () => theme.color.icon.lime).with(ChartPalette.ORANGE, () => theme.color.icon.orange).with(ChartPalette.YELLOW, () => theme.color.icon.yellow).with(ChartPalette.GREEN, () => theme.color.icon.success).with(ChartPalette.RED, () => theme.color.icon.error).exhaustive();
|
|
16
|
+
return match(palette).with(ChartPalette.PURPLE, () => theme.color.icon.purple).with(ChartPalette.PINK, () => theme.color.icon.pink).with(ChartPalette.BLUE, () => theme.color.icon.blue).with(ChartPalette.TEAL, () => theme.color.icon.teal).with(ChartPalette.LIME, () => theme.color.icon.lime).with(ChartPalette.ORANGE, () => theme.color.icon.orange).with(ChartPalette.YELLOW, () => theme.color.icon.yellow).with(ChartPalette.GREEN, () => theme.color.icon.success).with(ChartPalette.RED, () => theme.color.icon.error).with(ChartPalette.PRIMARY, () => theme.color.icon.primary).with(ChartPalette.SECONDARY, () => theme.color.icon.secondary).with(ChartPalette.TERTIARY, () => theme.color.icon.tertiary).with(ChartPalette.WARNING, () => theme.color.icon.warning).exhaustive();
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Resolve the color for a series, honoring an explicit slot and otherwise
|
package/dist/charts/types.d.ts
CHANGED
|
@@ -46,7 +46,11 @@ export declare enum ChartPalette {
|
|
|
46
46
|
ORANGE = "ORANGE",
|
|
47
47
|
YELLOW = "YELLOW",
|
|
48
48
|
GREEN = "GREEN",
|
|
49
|
-
RED = "RED"
|
|
49
|
+
RED = "RED",
|
|
50
|
+
PRIMARY = "PRIMARY",
|
|
51
|
+
SECONDARY = "SECONDARY",
|
|
52
|
+
TERTIARY = "TERTIARY",
|
|
53
|
+
WARNING = "WARNING"
|
|
50
54
|
}
|
|
51
55
|
/**
|
|
52
56
|
* Built-in value formats. Supplies an `Intl`-based default for axis ticks,
|
|
@@ -358,6 +362,84 @@ export type ChartCategoryTarget = {
|
|
|
358
362
|
shape: ChartTargetShape.PATH;
|
|
359
363
|
d: string;
|
|
360
364
|
};
|
|
365
|
+
/**
|
|
366
|
+
* One rendered mark's own target, in plot coordinates.
|
|
367
|
+
*
|
|
368
|
+
* Drawn above the category targets and bound to *clicks* only. Hover stays
|
|
369
|
+
* per category — crossing between two segments of one stack re-enters the
|
|
370
|
+
* category the pointer already had, so the tooltip holds still. That banding is
|
|
371
|
+
* why a two-pixel segment is as easy to hover as a two-hundred-pixel one, and a
|
|
372
|
+
* click layer laid over it must not undo it.
|
|
373
|
+
*
|
|
374
|
+
* Unlike {@link ChartCategoryTarget}, array position says nothing about the
|
|
375
|
+
* category here, so each target names its own.
|
|
376
|
+
*/
|
|
377
|
+
export type ChartMarkTarget = {
|
|
378
|
+
key: string;
|
|
379
|
+
/** The category this mark sits in — its hover, and its selection event. */
|
|
380
|
+
categoryIndex: number;
|
|
381
|
+
shape: ChartTargetShape.RECT;
|
|
382
|
+
x: number;
|
|
383
|
+
y: number;
|
|
384
|
+
width: number;
|
|
385
|
+
height: number;
|
|
386
|
+
} | {
|
|
387
|
+
key: string;
|
|
388
|
+
categoryIndex: number;
|
|
389
|
+
shape: ChartTargetShape.PATH;
|
|
390
|
+
d: string;
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Which marks a chart's `selection` covers.
|
|
394
|
+
*
|
|
395
|
+
* A *descriptor* rather than a coordinate: a mark matches when every field
|
|
396
|
+
* present matches it, and a field the mark has no notion of never excludes it.
|
|
397
|
+
* One shape therefore spells every granularity without a mode flag —
|
|
398
|
+
*
|
|
399
|
+
* ```ts
|
|
400
|
+
* { categoryKey: "00:00" } // the whole 00:00 column
|
|
401
|
+
* { seriesKey: "failed" } // every failure, all columns
|
|
402
|
+
* { categoryKey: "00:00", seriesKey: "failed" } // one segment
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* — and that last rule is what stops a category selection from muting a
|
|
406
|
+
* LineChart's strokes, which span every category and belong to none.
|
|
407
|
+
*/
|
|
408
|
+
export interface ChartSelection<TSeriesKey extends string = string> {
|
|
409
|
+
/**
|
|
410
|
+
* Raw category label, never `labelFormatter` output — the same vocabulary
|
|
411
|
+
* `ChartGroupProvider` matches on, for the same reason: two charts formatting
|
|
412
|
+
* one label differently must still agree on what it names.
|
|
413
|
+
*/
|
|
414
|
+
categoryKey?: string;
|
|
415
|
+
/**
|
|
416
|
+
* Whatever this chart's legend names: components in a stacked bar, metrics in
|
|
417
|
+
* an unstacked one or a line chart, slices in a pie. The same key the mute
|
|
418
|
+
* axis uses, so a selection and the legend cannot describe different things.
|
|
419
|
+
*/
|
|
420
|
+
seriesKey?: TSeriesKey;
|
|
421
|
+
}
|
|
422
|
+
/** One descriptor, several, or none. */
|
|
423
|
+
export type ChartSelectionInput<TSeriesKey extends string = string> = ChartSelection<TSeriesKey> | ChartSelection<TSeriesKey>[] | null;
|
|
424
|
+
/**
|
|
425
|
+
* What a click reports.
|
|
426
|
+
*
|
|
427
|
+
* Deliberately thin — no rows, no colors, no formatted strings. The caller
|
|
428
|
+
* already holds the data it passed in and can look the rest up by key, and a
|
|
429
|
+
* presentation-shaped payload is the kind that becomes load-bearing and then
|
|
430
|
+
* cannot be changed. The fields are exactly those a {@link ChartSelection} is
|
|
431
|
+
* built from, so feeding an event straight back as one is the common case.
|
|
432
|
+
*/
|
|
433
|
+
export interface ChartSelectionEvent<TSeriesKey extends string = string> {
|
|
434
|
+
categoryIndex: number;
|
|
435
|
+
/** Raw label, matching {@link ChartSelection.categoryKey}. */
|
|
436
|
+
categoryKey: string;
|
|
437
|
+
/** Absent when the click landed on empty plot rather than on a mark. */
|
|
438
|
+
seriesKey?: TSeriesKey;
|
|
439
|
+
/** The mark's own value, before any normalization. Absent whenever
|
|
440
|
+
* {@link seriesKey} is. */
|
|
441
|
+
value?: number;
|
|
442
|
+
}
|
|
361
443
|
/** One legend entry. */
|
|
362
444
|
export interface ChartLegendItem {
|
|
363
445
|
key: string;
|
package/dist/charts/types.js
CHANGED
|
@@ -47,6 +47,10 @@ var ChartPalette = /* @__PURE__ */ function(ChartPalette) {
|
|
|
47
47
|
ChartPalette["YELLOW"] = "YELLOW";
|
|
48
48
|
ChartPalette["GREEN"] = "GREEN";
|
|
49
49
|
ChartPalette["RED"] = "RED";
|
|
50
|
+
ChartPalette["PRIMARY"] = "PRIMARY";
|
|
51
|
+
ChartPalette["SECONDARY"] = "SECONDARY";
|
|
52
|
+
ChartPalette["TERTIARY"] = "TERTIARY";
|
|
53
|
+
ChartPalette["WARNING"] = "WARNING";
|
|
50
54
|
return ChartPalette;
|
|
51
55
|
}({});
|
|
52
56
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChartMarkState } from "./types";
|
|
2
|
+
import type { ChartSelectionInput } from "./types";
|
|
2
3
|
/**
|
|
3
4
|
* Hover interaction shared by every chart.
|
|
4
5
|
*
|
|
@@ -18,9 +19,14 @@ import { ChartMarkState } from "./types";
|
|
|
18
19
|
* isolating "Delivered" in a chart that happens to share the name would
|
|
19
20
|
* correlate nothing.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Plus one axis that is not hover at all: **selection**, supplied by the caller
|
|
23
|
+
* and never changed here. A selection is a commitment rather than a preview —
|
|
24
|
+
* it outranks both hover filters, and it releases only when the caller says so.
|
|
25
|
+
*
|
|
26
|
+
* Clicking deliberately does *not* pin the hover. That is what it used to do,
|
|
27
|
+
* with no effect outside the chart, and it read as the plot getting stuck. A
|
|
28
|
+
* click now only reports what was hit; whether anything lights up afterwards is
|
|
29
|
+
* the caller's `selection` to decide.
|
|
24
30
|
*/
|
|
25
31
|
interface UseChartInteractionOptions {
|
|
26
32
|
/** Number of hoverable categories. A shrinking count clears stale indices. */
|
|
@@ -28,12 +34,36 @@ interface UseChartInteractionOptions {
|
|
|
28
34
|
/** Legend keys currently rendered. A key that disappears clears stale hover. */
|
|
29
35
|
seriesKeys?: string[];
|
|
30
36
|
/**
|
|
31
|
-
* Raw category labels, index-aligned with {@link categoryCount}.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
37
|
+
* Raw category labels, index-aligned with {@link categoryCount}. Raw labels,
|
|
38
|
+
* never `labelFormatter` output — two charts formatting the same label
|
|
39
|
+
* differently must still match, and so must a caller's `selection`.
|
|
40
|
+
*
|
|
41
|
+
* Identity only. Opting into `ChartGroupProvider` sync is
|
|
42
|
+
* {@link enableGroupSync}, which used to be implied by supplying these — the
|
|
43
|
+
* two were split once selection needed the labels in a chart (PieChart) that
|
|
44
|
+
* must stay out of the sync group.
|
|
35
45
|
*/
|
|
36
46
|
categoryKeys?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Follow a `ChartGroupProvider` sibling's hovered category. PieChart passes
|
|
49
|
+
* `false`: it has no category axis, so there is nothing to correlate.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
enableGroupSync?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Marks the caller has selected. Matching marks hold full strength and the
|
|
55
|
+
* rest recede — the same "soften everything else" grammar the hover axes use,
|
|
56
|
+
* which is why selection needs no visual vocabulary of its own.
|
|
57
|
+
*
|
|
58
|
+
* Presentational and strictly read-only: this hook never writes it. The state
|
|
59
|
+
* lives with whatever the selection is filtering, so the chart and that thing
|
|
60
|
+
* cannot drift apart, and the DLS never has to pick toggle or multi-select
|
|
61
|
+
* semantics.
|
|
62
|
+
*
|
|
63
|
+
* Normalized here rather than by each chart, so "one, several, or none" is
|
|
64
|
+
* spelled once.
|
|
65
|
+
*/
|
|
66
|
+
selection?: ChartSelectionInput;
|
|
37
67
|
/**
|
|
38
68
|
* How marks outside the active category recede. Defaults to
|
|
39
69
|
* {@link ChartMarkState.MUTED}; BarChart passes
|
|
@@ -75,6 +105,15 @@ export interface ChartInteraction {
|
|
|
75
105
|
*/
|
|
76
106
|
showTooltip: boolean;
|
|
77
107
|
hoveredSeriesKey: string | null;
|
|
108
|
+
/**
|
|
109
|
+
* Categories a {@link UseChartInteractionOptions.selection} names outright.
|
|
110
|
+
*
|
|
111
|
+
* Drives the persistent cursor a cartesian chart draws at a selected
|
|
112
|
+
* category. A descriptor carrying only a `seriesKey` names no category and
|
|
113
|
+
* contributes nothing here — "every failure" should mute the other series,
|
|
114
|
+
* not band every column on the axis.
|
|
115
|
+
*/
|
|
116
|
+
selectedCategoryIndices: ReadonlySet<number>;
|
|
78
117
|
/**
|
|
79
118
|
* Emphasis for one mark, composed from both axes.
|
|
80
119
|
*
|
|
@@ -92,5 +131,5 @@ export interface ChartInteraction {
|
|
|
92
131
|
onSeriesEnter: (seriesKey: string) => void;
|
|
93
132
|
onSeriesLeave: () => void;
|
|
94
133
|
}
|
|
95
|
-
export declare function useChartInteraction({ categoryCount, seriesKeys, categoryKeys, categoryMuteState, isDisabled, isLoading, }: UseChartInteractionOptions): ChartInteraction;
|
|
134
|
+
export declare function useChartInteraction({ categoryCount, seriesKeys, categoryKeys, enableGroupSync, selection, categoryMuteState, isDisabled, isLoading, }: UseChartInteractionOptions): ChartInteraction;
|
|
96
135
|
export {};
|
|
@@ -2,12 +2,15 @@ import { ChartMarkState } from "./types.js";
|
|
|
2
2
|
import { ChartGroupContext } from "./ChartGroupProvider.js";
|
|
3
3
|
import { useCallback, useContext, useEffect, useId, useMemo, useState } from "react";
|
|
4
4
|
//#region src/charts/useChartInteraction.ts
|
|
5
|
-
|
|
5
|
+
/** Stable identity, so an absent selection never re-memoizes anything. */
|
|
6
|
+
var NO_SELECTION = [];
|
|
7
|
+
var NO_SELECTED_INDICES = /* @__PURE__ */ new Set();
|
|
8
|
+
function useChartInteraction({ categoryCount, seriesKeys, categoryKeys, enableGroupSync = true, selection, categoryMuteState = ChartMarkState.MUTED, isDisabled = false, isLoading = false }) {
|
|
6
9
|
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
7
10
|
const [hoveredSeriesKey, setHoveredSeriesKey] = useState(null);
|
|
8
11
|
const sync = useContext(ChartGroupContext);
|
|
9
12
|
const syncId = useId();
|
|
10
|
-
const canSync = sync !== null && categoryKeys !== void 0;
|
|
13
|
+
const canSync = sync !== null && categoryKeys !== void 0 && enableGroupSync;
|
|
11
14
|
const setActive = sync?.setActive;
|
|
12
15
|
const clearActive = sync?.clearActive;
|
|
13
16
|
const safeHovered = hoveredIndex !== null && hoveredIndex < categoryCount ? hoveredIndex : null;
|
|
@@ -75,14 +78,72 @@ function useChartInteraction({ categoryCount, seriesKeys, categoryKeys, category
|
|
|
75
78
|
const onSeriesLeave = useCallback(() => {
|
|
76
79
|
setHoveredSeriesKey(null);
|
|
77
80
|
}, []);
|
|
81
|
+
const selectionList = useMemo(() => {
|
|
82
|
+
if (selection === void 0 || selection === null) return NO_SELECTION;
|
|
83
|
+
if (!Array.isArray(selection)) return [selection];
|
|
84
|
+
return selection.length === 0 ? NO_SELECTION : selection;
|
|
85
|
+
}, [selection]);
|
|
86
|
+
const hasSelection = selectionList.length > 0;
|
|
87
|
+
/**
|
|
88
|
+
* Whether one mark falls inside the selection.
|
|
89
|
+
*
|
|
90
|
+
* A descriptor is a conjunction of the fields it *carries*: an absent field
|
|
91
|
+
* constrains nothing, and a field the mark itself has no notion of cannot
|
|
92
|
+
* exclude it. The second half is what keeps `{ categoryKey: "00:00" }` from
|
|
93
|
+
* muting every LineChart stroke — a stroke spans all the categories and is
|
|
94
|
+
* asked about none — while still muting the bars outside that column.
|
|
95
|
+
*
|
|
96
|
+
* Several descriptors union: a mark inside any of them is selected.
|
|
97
|
+
*/
|
|
98
|
+
const matchesSelection = useCallback(({ categoryIndex, seriesKey }) => {
|
|
99
|
+
if (!hasSelection) return true;
|
|
100
|
+
return selectionList.some((descriptor) => {
|
|
101
|
+
if (descriptor.categoryKey !== void 0 && categoryIndex !== void 0 && categoryKeys !== void 0 && categoryKeys[categoryIndex] !== descriptor.categoryKey) return false;
|
|
102
|
+
if (descriptor.seriesKey !== void 0 && seriesKey !== void 0 && seriesKey !== descriptor.seriesKey) return false;
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
}, [
|
|
106
|
+
hasSelection,
|
|
107
|
+
selectionList,
|
|
108
|
+
categoryKeys
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* Resolved once rather than per mark: the cursor a cartesian chart draws at a
|
|
112
|
+
* selected category needs the set, and every mark would otherwise re-scan
|
|
113
|
+
* every descriptor.
|
|
114
|
+
*
|
|
115
|
+
* Deliberately not pruned when the data shrinks under it, unlike the hover
|
|
116
|
+
* indices above. A selection is the caller's state and describes what they
|
|
117
|
+
* asked for; a key that is momentarily absent from the data — mid-refetch,
|
|
118
|
+
* say — simply matches nothing until it returns.
|
|
119
|
+
*/
|
|
120
|
+
const selectedCategoryIndices = useMemo(() => {
|
|
121
|
+
if (!hasSelection || categoryKeys === void 0) return NO_SELECTED_INDICES;
|
|
122
|
+
const selectedKeys = new Set(selectionList.map((descriptor) => descriptor.categoryKey).filter((key) => key !== void 0));
|
|
123
|
+
if (selectedKeys.size === 0) return NO_SELECTED_INDICES;
|
|
124
|
+
const indices = /* @__PURE__ */ new Set();
|
|
125
|
+
categoryKeys.forEach((key, index) => {
|
|
126
|
+
if (selectedKeys.has(key)) indices.add(index);
|
|
127
|
+
});
|
|
128
|
+
return indices;
|
|
129
|
+
}, [
|
|
130
|
+
hasSelection,
|
|
131
|
+
selectionList,
|
|
132
|
+
categoryKeys
|
|
133
|
+
]);
|
|
78
134
|
const getMarkState = useCallback(({ categoryIndex, seriesKey }) => {
|
|
79
135
|
if (isLoading) return ChartMarkState.MUTED;
|
|
136
|
+
if (!matchesSelection({
|
|
137
|
+
categoryIndex,
|
|
138
|
+
seriesKey
|
|
139
|
+
})) return ChartMarkState.MUTED;
|
|
80
140
|
const categoryMuted = categoryIndex !== void 0 && effectiveIndex !== null && categoryIndex !== effectiveIndex;
|
|
81
141
|
if (seriesKey !== void 0 && safeHoveredSeries !== null && seriesKey !== safeHoveredSeries) return ChartMarkState.MUTED;
|
|
82
142
|
if (categoryMuted) return categoryMuteState;
|
|
83
143
|
return ChartMarkState.ACTIVE;
|
|
84
144
|
}, [
|
|
85
145
|
isLoading,
|
|
146
|
+
matchesSelection,
|
|
86
147
|
effectiveIndex,
|
|
87
148
|
safeHoveredSeries,
|
|
88
149
|
categoryMuteState
|
|
@@ -91,6 +152,7 @@ function useChartInteraction({ categoryCount, seriesKeys, categoryKeys, category
|
|
|
91
152
|
activeIndex: effectiveIndex,
|
|
92
153
|
showTooltip,
|
|
93
154
|
hoveredSeriesKey: safeHoveredSeries,
|
|
155
|
+
selectedCategoryIndices,
|
|
94
156
|
getMarkState,
|
|
95
157
|
onCategoryEnter,
|
|
96
158
|
onCategoryLeave,
|
|
@@ -100,6 +162,7 @@ function useChartInteraction({ categoryCount, seriesKeys, categoryKeys, category
|
|
|
100
162
|
effectiveIndex,
|
|
101
163
|
showTooltip,
|
|
102
164
|
safeHoveredSeries,
|
|
165
|
+
selectedCategoryIndices,
|
|
103
166
|
getMarkState,
|
|
104
167
|
onCategoryEnter,
|
|
105
168
|
onCategoryLeave,
|
package/dist/styles.css
CHANGED
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
.c8c506o{stroke:none;pointer-events:none;opacity:1;}.c8c506o[data-state="muted"]{opacity:0.1;}.c8c506o[data-state="dimmed"]{opacity:0.4;}
|
|
34
34
|
.ceqlpek{pointer-events:none;opacity:1;}.ceqlpek[data-state="muted"]{opacity:0.1;}.ceqlpek[data-state="dimmed"]{opacity:0.4;}
|
|
35
35
|
.c1fcbddl{opacity:1;}.c1fcbddl[data-state="muted"]{opacity:0.1;}.c1fcbddl[data-state="dimmed"]{opacity:0.4;}
|
|
36
|
-
.cpcxr59{fill:transparent;outline:none;}
|
|
37
|
-
.c1wvzvu4{fill:transparent;outline:none;}
|
|
36
|
+
.cpcxr59{fill:transparent;outline:none;}.cpcxr59[data-clickable="true"]{cursor:pointer;}
|
|
37
|
+
.c1wvzvu4{fill:transparent;outline:none;}.c1wvzvu4[data-clickable="true"]{cursor:pointer;}
|
|
38
38
|
.c17guewt{stroke:var(--c17guewt-0);shape-rendering:crispEdges;}
|
|
39
|
-
.cblgiyf{stroke:var(--cblgiyf-0);pointer-events:none;}
|
|
40
|
-
.c1lr03cc{fill:var(--c1lr03cc-0);pointer-events:none;}
|
|
39
|
+
.cblgiyf{stroke:var(--cblgiyf-0);pointer-events:none;}.cblgiyf[data-selected="true"]{stroke:var(--cblgiyf-1);}
|
|
40
|
+
.c1lr03cc{fill:var(--c1lr03cc-0);pointer-events:none;}.c1lr03cc[data-selected="true"]{fill:var(--c1lr03cc-1);}
|
|
41
41
|
.cizy1s8{overflow:visible;pointer-events:none;}
|
|
42
42
|
.c1ukq11d{position:absolute;z-index:10;pointer-events:none;left:var(--c1ukq11d-0);top:var(--c1ukq11d-1);min-width:200px;max-width:320px;padding:10px 12px;border:0.5px solid var(--c1ukq11d-2);border-radius:6px;background-color:var(--c1ukq11d-3);opacity:var(--c1ukq11d-4);-webkit-transition:opacity 100ms ease-in-out,left 100ms ease-out,top 100ms ease-out;transition:opacity 100ms ease-in-out,left 100ms ease-out,top 100ms ease-out;}@media (prefers-reduced-motion: reduce){.c1ukq11d{-webkit-transition:none;transition:none;}}
|
|
43
43
|
.cwcko2m{display:-ms-grid;display:grid;-ms-grid-columns:minmax(0, 1fr) auto;grid-template-columns:minmax(0, 1fr) auto;-webkit-column-gap:16px;column-gap:16px;row-gap:6px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;}
|