@galaxy-io/dls 1.3.4 → 1.3.6

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.
@@ -1,13 +1,13 @@
1
1
  import FlexWrapper, { AlignItems, FlexDirection, JustifyContent } from "../containers/FlexWrapper.js";
2
- import { ChartKind, ChartTargetShape } from "./types.js";
3
- import { PIE_CHART_MARGIN, PIE_MIN_SLICE_SHARE } from "./constants.js";
2
+ import { ChartKind, ChartLegendPlacement, ChartTargetShape } from "./types.js";
3
+ import { PIE_CHART_MARGIN, PIE_MIN_SLICE_SHARE, SKELETON_PIE_VALUES } from "./constants.js";
4
4
  import { useSeriesColorResolver } from "./useSeriesColorResolver.js";
5
5
  import { ChartTooltipVerticalAlign } from "./useChartTooltipPosition.js";
6
6
  import { useChartInteraction } from "./useChartInteraction.js";
7
7
  import { useChartFormatters } from "./useChartFormatters.js";
8
8
  import { useChartDimensions } from "./useChartDimensions.js";
9
9
  import { buildPieChartGeometry } from "./pieChartGeometry.js";
10
- import { ChartArcPath, ChartCenterBox } from "./ChartPrimitives.js";
10
+ import { ChartArcPath, ChartCenterBox, ChartSkeletonGroup } from "./ChartPrimitives.js";
11
11
  import ChartFrame from "./ChartFrame.js";
12
12
  import { useCallback, useMemo, useRef } from "react";
13
13
  import { jsx } from "react/jsx-runtime";
@@ -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, noTooltip = false, tooltipRenderer, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription, onPinnedChange }) => {
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, noTooltip = false, tooltipRenderer, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
23
23
  const containerRef = useRef(null);
24
24
  const colors = useSeriesColorResolver(series);
25
25
  const dimensions = useChartDimensions({
@@ -40,9 +40,23 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
40
40
  });
41
41
  const getSliceLabel = useCallback((sliceKey) => series[sliceKey]?.label ?? sliceKey, [series]);
42
42
  const getSliceColor = useCallback((sliceKey) => series[sliceKey]?.color, [series]);
43
- const resolveColor = useCallback(({ sliceKey, explicit }) => sliceKey === null ? colors.neutral : colors.resolve(sliceKey, explicit), [colors]);
43
+ /**
44
+ * The slices actually rendered: the caller's, or three ghost arcs while
45
+ * loading, pushed through the real geometry so the skeleton donut gets the
46
+ * true pad angles and corner radii. The cast is safe because nothing derived
47
+ * from skeleton data ever surfaces: targets are off, the legend shows stubs,
48
+ * and the accessible name says "loading" instead of counting slices.
49
+ */
50
+ const displaySlices = useMemo(() => isLoading ? SKELETON_PIE_VALUES.map((value, index) => ({
51
+ key: `skeleton-${index}`,
52
+ value
53
+ })) : slices, [isLoading, slices]);
54
+ const resolveColor = useCallback(({ sliceKey, explicit }) => {
55
+ if (isLoading) return colors.placeholder;
56
+ return sliceKey === null ? colors.neutral : colors.resolve(sliceKey, explicit);
57
+ }, [colors, isLoading]);
44
58
  const geometry = useMemo(() => buildPieChartGeometry({
45
- slices,
59
+ slices: displaySlices,
46
60
  plotWidth,
47
61
  plotHeight,
48
62
  innerRadiusRatio,
@@ -54,7 +68,7 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
54
68
  getSliceColor,
55
69
  getSliceLabel
56
70
  }), [
57
- slices,
71
+ displaySlices,
58
72
  plotWidth,
59
73
  plotHeight,
60
74
  innerRadiusRatio,
@@ -76,9 +90,7 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
76
90
  const interaction = useChartInteraction({
77
91
  categoryCount: geometry.arcs.length,
78
92
  seriesKeys: legendKeys,
79
- disabled: noTooltip || isLoading || isEmpty,
80
- isLoading,
81
- onPinnedChange
93
+ disabled: noTooltip || isLoading || isEmpty
82
94
  });
83
95
  const buildCategoryTooltip = useCallback((index) => {
84
96
  const chartArc = geometry.arcs[index];
@@ -101,35 +113,28 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
101
113
  formatLabel
102
114
  ]);
103
115
  /**
104
- * A slice reads better than the generic "label: row, row" sentence.
105
- *
106
- * The frame's default would render "Enterprise: 42.1% of total 1,240", which
107
- * puts the share where a reader expects the value.
108
- */
109
- const describeCategory = useCallback((index) => {
110
- const chartArc = geometry.arcs[index];
111
- if (!chartArc) return "";
112
- const share = `${Math.round(chartArc.share * 1e3) / 10}% of total`;
113
- return `${formatLabel(chartArc.label)}, ${formatValue(chartArc.value)}, ${share}`;
114
- }, [
115
- geometry.arcs,
116
- formatValue,
117
- formatLabel
118
- ]);
119
- /**
120
116
  * Targets reuse each arc's own `d`, drawn again transparent above the marks.
121
117
  *
122
- * The arcs themselves stop being interactive: hit testing, focus and the
123
- * focus ring then work identically on all three charts, instead of the pie
124
- * attaching handlers permanently and toggling `data-interactive` while the
125
- * cartesian charts rendered their targets away.
118
+ * The arcs themselves stay pure marks: hit testing then works identically on
119
+ * all three charts, instead of the pie hit-testing its visible arcs directly.
126
120
  */
127
121
  const categoryTargets = useMemo(() => geometry.arcs.map((chartArc) => ({
128
122
  key: chartArc.id,
129
123
  shape: ChartTargetShape.PATH,
130
124
  d: chartArc.path
131
125
  })), [geometry.arcs]);
132
- const showCenter = centerContent && (innerRadiusRatio ?? .62) > 0;
126
+ const showCenter = centerContent && !isLoading && (innerRadiusRatio ?? .62) > 0;
127
+ const marks = /* @__PURE__ */ jsx("g", {
128
+ transform: `translate(${geometry.centerX}, ${geometry.centerY})`,
129
+ children: geometry.arcs.map((chartArc, index) => /* @__PURE__ */ jsx(ChartArcPath, {
130
+ d: chartArc.path,
131
+ fill: chartArc.color,
132
+ "data-state": interaction.getMarkState({
133
+ categoryIndex: index,
134
+ seriesKey: chartArc.id
135
+ })
136
+ }, chartArc.id))
137
+ });
133
138
  return /* @__PURE__ */ jsx(ChartFrame, {
134
139
  containerRef,
135
140
  dimensions,
@@ -145,8 +150,6 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
145
150
  tooltipRenderer,
146
151
  categoryTargets,
147
152
  categoryTargetsTransform: `translate(${geometry.centerX}, ${geometry.centerY})`,
148
- wrapCategoryNavigation: true,
149
- describeCategory,
150
153
  placeAwayFromX: geometry.centerX,
151
154
  verticalAlign: ChartTooltipVerticalAlign.CENTERED,
152
155
  isEmpty,
@@ -154,24 +157,14 @@ var PieChart = ({ series, slices, width, height, fillWidth, fillHeight, aspectRa
154
157
  contentWhenEmpty,
155
158
  showLegend,
156
159
  legendItems,
157
- legendInset: 0,
160
+ legendPlacement: ChartLegendPlacement.RIGHT,
158
161
  plotOverlay: showCenter && !isEmpty ? /* @__PURE__ */ jsx(ChartDonutCenter, {
159
162
  centerX: dimensions.margin.left + geometry.centerX,
160
163
  centerY: dimensions.margin.top + geometry.centerY,
161
164
  diameter: geometry.innerRadius * 2,
162
165
  children: centerContent
163
166
  }) : null,
164
- children: /* @__PURE__ */ jsx("g", {
165
- transform: `translate(${geometry.centerX}, ${geometry.centerY})`,
166
- children: geometry.arcs.map((chartArc, index) => /* @__PURE__ */ jsx(ChartArcPath, {
167
- d: chartArc.path,
168
- fill: chartArc.color,
169
- "data-state": interaction.getMarkState({
170
- categoryIndex: index,
171
- seriesKey: chartArc.id
172
- })
173
- }, chartArc.id))
174
- })
167
+ children: isLoading ? /* @__PURE__ */ jsx(ChartSkeletonGroup, { children: marks }) : marks
175
168
  });
176
169
  };
177
170
  /** Positions arbitrary content inside the donut hole. */
@@ -1,5 +1,5 @@
1
1
  import { ChartValueUnit } from "./types";
2
- import type { ChartLabelFormatter, ChartPlotTooltipModel, ChartValueFormatter } from "./types";
2
+ import type { ChartLabelFormatter, ChartValueFormatter } from "./types";
3
3
  /**
4
4
  * Format a value with a built-in unit.
5
5
  *
@@ -17,11 +17,3 @@ export declare function formatChartValue(value: number, unit: ChartValueUnit): s
17
17
  export declare function resolveValueFormatter(valueFormatter?: ChartValueFormatter, unit?: ChartValueUnit): ChartValueFormatter;
18
18
  /** Pick the effective label formatter, defaulting to identity. */
19
19
  export declare function resolveLabelFormatter(labelFormatter?: ChartLabelFormatter): ChartLabelFormatter;
20
- /**
21
- * Flatten a tooltip model into one sentence, for a screen reader.
22
- *
23
- * Derived from the tooltip rather than written separately so the two cannot
24
- * drift: whatever a sighted reader sees on hover is what a keyboard reader
25
- * hears on focus, by construction.
26
- */
27
- export declare function describeTooltipModel(model: ChartPlotTooltipModel): string;
@@ -55,17 +55,5 @@ function resolveValueFormatter(valueFormatter, unit) {
55
55
  function resolveLabelFormatter(labelFormatter) {
56
56
  return labelFormatter ?? ((label) => label);
57
57
  }
58
- /**
59
- * Flatten a tooltip model into one sentence, for a screen reader.
60
- *
61
- * Derived from the tooltip rather than written separately so the two cannot
62
- * drift: whatever a sighted reader sees on hover is what a keyboard reader
63
- * hears on focus, by construction.
64
- */
65
- function describeTooltipModel(model) {
66
- if (model.rows.length === 0) return model.label;
67
- const rows = model.rows.map((row) => `${row.label} ${row.formattedValue}`).join(", ");
68
- return `${model.label}: ${rows}`;
69
- }
70
58
  //#endregion
71
- export { describeTooltipModel, formatChartValue, resolveLabelFormatter, resolveValueFormatter };
59
+ export { formatChartValue, resolveLabelFormatter, resolveValueFormatter };
@@ -185,27 +185,43 @@ export declare const CHART_RESTORE_DELAY_MS = 200;
185
185
  *
186
186
  * The grammar is inherited from the reference charts and is the opposite of the
187
187
  * obvious one: hovering does not brighten the hovered mark, it *softens*
188
- * everything else. `MUTED` applies to marks outside the hovered category;
189
- * `FADED` to marks outside a *pinned* one.
188
+ * everything else. `MUTED` applies to marks outside the hovered category or
189
+ * series.
190
190
  *
191
191
  * The step down from ACTIVE is deliberately steep. These are large filled areas
192
192
  * — bars and arcs — and a filled shape still reads as present at an opacity
193
193
  * where a hairline would have vanished, so a timid step barely registers as a
194
194
  * highlight at all.
195
- *
196
- * Keep `FADED` below `MUTED`, or pinning becomes visually indistinguishable
197
- * from hovering — the pin exists to say "I am holding this one", which only
198
- * reads if the rest recede further than they do on hover.
199
195
  */
200
196
  export declare const CHART_MARK_OPACITY_ACTIVE = 1;
201
197
  export declare const CHART_MARK_OPACITY_MUTED = 0.1;
202
- export declare const CHART_MARK_OPACITY_FADED = 0.04;
203
198
  /** Distance in px between the tooltip and its anchor. */
204
199
  export declare const CHART_TOOLTIP_OFFSET = 10;
205
200
  /** Minimum width of the tooltip, in px. */
206
201
  export declare const CHART_TOOLTIP_MIN_WIDTH = 168;
207
202
  /** Maximum width of the tooltip, in px. */
208
203
  export declare const CHART_TOOLTIP_MAX_WIDTH = 320;
204
+ /** Gap in px between the plot box and a side-placed legend. */
205
+ export declare const CHART_LEGEND_SIDE_GAP = 16;
206
+ /**
207
+ * Ghost bar heights, one inner array per group — three bars each, so the
208
+ * skeleton reads as the grouped chart it usually stands in for rather than a
209
+ * sparse row of lone bars.
210
+ */
211
+ export declare const SKELETON_BAR_GROUPS: number[][];
212
+ /**
213
+ * Ghost lines, one inner array per line, offset from each other and drawn with
214
+ * their area fills so the skeleton has the layered depth of a real multi-series
215
+ * chart instead of a single bare stroke.
216
+ */
217
+ export declare const SKELETON_LINE_SERIES: number[][];
218
+ /** Ghost donut shares — three arcs, halving. */
219
+ export declare const SKELETON_PIE_VALUES: number[];
220
+ /**
221
+ * Skeleton pulse period, matching the cadence of `TextShimmer` and
222
+ * `AvatarShimmer` so charts load in step with everything else on the page.
223
+ */
224
+ export declare const CHART_SKELETON_PULSE_MS = 2500;
209
225
  /**
210
226
  * Approximate width of one CAPTION-size character.
211
227
  *
@@ -195,27 +195,101 @@ var CHART_RESTORE_DELAY_MS = 200;
195
195
  *
196
196
  * The grammar is inherited from the reference charts and is the opposite of the
197
197
  * obvious one: hovering does not brighten the hovered mark, it *softens*
198
- * everything else. `MUTED` applies to marks outside the hovered category;
199
- * `FADED` to marks outside a *pinned* one.
198
+ * everything else. `MUTED` applies to marks outside the hovered category or
199
+ * series.
200
200
  *
201
201
  * The step down from ACTIVE is deliberately steep. These are large filled areas
202
202
  * — bars and arcs — and a filled shape still reads as present at an opacity
203
203
  * where a hairline would have vanished, so a timid step barely registers as a
204
204
  * highlight at all.
205
- *
206
- * Keep `FADED` below `MUTED`, or pinning becomes visually indistinguishable
207
- * from hovering — the pin exists to say "I am holding this one", which only
208
- * reads if the rest recede further than they do on hover.
209
205
  */
210
206
  var CHART_MARK_OPACITY_ACTIVE = 1;
211
207
  var CHART_MARK_OPACITY_MUTED = .1;
212
- var CHART_MARK_OPACITY_FADED = .04;
213
208
  /** Distance in px between the tooltip and its anchor. */
214
209
  var CHART_TOOLTIP_OFFSET = 10;
215
210
  /** Minimum width of the tooltip, in px. */
216
211
  var CHART_TOOLTIP_MIN_WIDTH = 168;
217
212
  /** Maximum width of the tooltip, in px. */
218
213
  var CHART_TOOLTIP_MAX_WIDTH = 320;
214
+ /** Gap in px between the plot box and a side-placed legend. */
215
+ var CHART_LEGEND_SIDE_GAP = 16;
216
+ /**
217
+ * Ghost bar heights, one inner array per group — three bars each, so the
218
+ * skeleton reads as the grouped chart it usually stands in for rather than a
219
+ * sparse row of lone bars.
220
+ */
221
+ var SKELETON_BAR_GROUPS = [
222
+ [
223
+ 65,
224
+ 45,
225
+ 25
226
+ ],
227
+ [
228
+ 80,
229
+ 55,
230
+ 30
231
+ ],
232
+ [
233
+ 45,
234
+ 70,
235
+ 35
236
+ ],
237
+ [
238
+ 70,
239
+ 50,
240
+ 40
241
+ ],
242
+ [
243
+ 55,
244
+ 75,
245
+ 30
246
+ ]
247
+ ];
248
+ /**
249
+ * Ghost lines, one inner array per line, offset from each other and drawn with
250
+ * their area fills so the skeleton has the layered depth of a real multi-series
251
+ * chart instead of a single bare stroke.
252
+ */
253
+ var SKELETON_LINE_SERIES = [
254
+ [
255
+ 30,
256
+ 55,
257
+ 45,
258
+ 70,
259
+ 60,
260
+ 85,
261
+ 75
262
+ ],
263
+ [
264
+ 20,
265
+ 35,
266
+ 50,
267
+ 40,
268
+ 60,
269
+ 55,
270
+ 70
271
+ ],
272
+ [
273
+ 10,
274
+ 25,
275
+ 20,
276
+ 35,
277
+ 30,
278
+ 45,
279
+ 40
280
+ ]
281
+ ];
282
+ /** Ghost donut shares — three arcs, halving. */
283
+ var SKELETON_PIE_VALUES = [
284
+ 5,
285
+ 3,
286
+ 2
287
+ ];
288
+ /**
289
+ * Skeleton pulse period, matching the cadence of `TextShimmer` and
290
+ * `AvatarShimmer` so charts load in step with everything else on the page.
291
+ */
292
+ var CHART_SKELETON_PULSE_MS = 2500;
219
293
  /**
220
294
  * Approximate width of one CAPTION-size character.
221
295
  *
@@ -264,4 +338,4 @@ var CHART_PALETTE_ORDER = [
264
338
  ChartPalette.RED
265
339
  ];
266
340
  //#endregion
267
- export { AGGREGATED_SLICE_KEY, AUTO_MARGIN_LEFT_STEP, AXIS_TITLE_BAND, AXIS_TITLE_GAP, BAR_GAP_COMFORTABLE, BAR_GAP_MEDIUM, BAR_GAP_TIGHT, BAR_RADIUS, BAR_RADIUS_NARROW, BAR_WIDTH_RATIO, CAPTION_CHAR_WIDTH_PX, CATEGORY_AXIS_LABEL_GAP, CATEGORY_AXIS_LABEL_HEIGHT, CHART_MARK_OPACITY_ACTIVE, CHART_MARK_OPACITY_FADED, CHART_MARK_OPACITY_MUTED, CHART_PALETTE_ORDER, CHART_RESTORE_DELAY_MS, CHART_TOOLTIP_MAX_WIDTH, CHART_TOOLTIP_MIN_WIDTH, CHART_TOOLTIP_OFFSET, CHART_TRANSITION_MS, DEFAULT_CHART_MARGIN, DEFAULT_LINE_STROKE_WIDTH, DEFAULT_VALUE_TICK_COUNT, GRID_LINE_BLEED, GROUP_GAP_COMFORTABLE, GROUP_GAP_MEDIUM, GROUP_GAP_TIGHT, LINE_AREA_OPACITY, LINE_POINT_RADIUS, MAX_AUTO_MARGIN_LEFT, MAX_BAR_WIDTH, MAX_GAP_SHARE, MIN_AUTO_MARGIN_LEFT, MIN_BARS_FOR_TIGHT_SPACING, MIN_BAR_LENGTH, MIN_CATEGORY_LABEL_GAP, NARROW_BAR_WIDTH, PIE_CHART_MARGIN, PIE_CORNER_RADIUS, PIE_DONUT_INNER_RATIO, PIE_MAX_AUTO_INNER_RATIO, PIE_MAX_INSET_SHARE, PIE_MIN_SLICE_SHARE, PIE_PAD_ANGLE, PIE_TOOLTIP_ANCHOR_INSET, STACK_SEGMENT_GAP, VALUE_AXIS_LABEL_HEIGHT, VALUE_AXIS_TICK_GAP, WIDTH_FOR_MEDIUM_SPACING, WIDTH_FOR_TIGHT_SPACING };
341
+ export { AGGREGATED_SLICE_KEY, AUTO_MARGIN_LEFT_STEP, AXIS_TITLE_BAND, AXIS_TITLE_GAP, BAR_GAP_COMFORTABLE, BAR_GAP_MEDIUM, BAR_GAP_TIGHT, BAR_RADIUS, BAR_RADIUS_NARROW, BAR_WIDTH_RATIO, CAPTION_CHAR_WIDTH_PX, CATEGORY_AXIS_LABEL_GAP, CATEGORY_AXIS_LABEL_HEIGHT, CHART_LEGEND_SIDE_GAP, CHART_MARK_OPACITY_ACTIVE, CHART_MARK_OPACITY_MUTED, CHART_PALETTE_ORDER, CHART_RESTORE_DELAY_MS, CHART_SKELETON_PULSE_MS, CHART_TOOLTIP_MAX_WIDTH, CHART_TOOLTIP_MIN_WIDTH, CHART_TOOLTIP_OFFSET, CHART_TRANSITION_MS, DEFAULT_CHART_MARGIN, DEFAULT_LINE_STROKE_WIDTH, DEFAULT_VALUE_TICK_COUNT, GRID_LINE_BLEED, GROUP_GAP_COMFORTABLE, GROUP_GAP_MEDIUM, GROUP_GAP_TIGHT, LINE_AREA_OPACITY, LINE_POINT_RADIUS, MAX_AUTO_MARGIN_LEFT, MAX_BAR_WIDTH, MAX_GAP_SHARE, MIN_AUTO_MARGIN_LEFT, MIN_BARS_FOR_TIGHT_SPACING, MIN_BAR_LENGTH, MIN_CATEGORY_LABEL_GAP, NARROW_BAR_WIDTH, PIE_CHART_MARGIN, PIE_CORNER_RADIUS, PIE_DONUT_INNER_RATIO, PIE_MAX_AUTO_INNER_RATIO, PIE_MAX_INSET_SHARE, PIE_MIN_SLICE_SHARE, PIE_PAD_ANGLE, PIE_TOOLTIP_ANCHOR_INSET, SKELETON_BAR_GROUPS, SKELETON_LINE_SERIES, SKELETON_PIE_VALUES, STACK_SEGMENT_GAP, VALUE_AXIS_LABEL_HEIGHT, VALUE_AXIS_TICK_GAP, WIDTH_FOR_MEDIUM_SPACING, WIDTH_FOR_TIGHT_SPACING };
@@ -120,6 +120,18 @@ export declare enum ChartKind {
120
120
  LINE = "LINE",
121
121
  PIE = "PIE"
122
122
  }
123
+ /**
124
+ * Where the legend sits relative to the plot.
125
+ *
126
+ * Its own enum rather than a branch on {@link ChartKind}, which promises to be
127
+ * nothing but a label for the accessible name.
128
+ */
129
+ export declare enum ChartLegendPlacement {
130
+ /** Beneath the plot, wrapping — the cartesian layout. */
131
+ BOTTOM = "BOTTOM",
132
+ /** Beside the plot, stacked, vertically centred on it — the pie layout. */
133
+ RIGHT = "RIGHT"
134
+ }
123
135
  /** How the category axis copes when labels outgrow their slots. */
124
136
  export declare enum ChartCategoryLabelMode {
125
137
  /** Render every label that fits; otherwise show every nth. The default. */
@@ -139,12 +151,10 @@ export declare enum ChartCategoryLabelMode {
139
151
  * DOM and trigger a React warning.
140
152
  */
141
153
  export declare enum ChartMarkState {
142
- /** Full strength — the default, and the hovered or pinned category. */
154
+ /** Full strength — the default, and the hovered category. */
143
155
  ACTIVE = "active",
144
156
  /** Softened, for categories other than the hovered one. */
145
- MUTED = "muted",
146
- /** Furthest back, for categories outside a pinned one. */
147
- FADED = "faded"
157
+ MUTED = "muted"
148
158
  }
149
159
  /** Which pair of a rectangle's corners carries the radius. */
150
160
  export declare enum BarCorners {
@@ -80,6 +80,19 @@ var ChartKind = /* @__PURE__ */ function(ChartKind) {
80
80
  ChartKind["PIE"] = "PIE";
81
81
  return ChartKind;
82
82
  }({});
83
+ /**
84
+ * Where the legend sits relative to the plot.
85
+ *
86
+ * Its own enum rather than a branch on {@link ChartKind}, which promises to be
87
+ * nothing but a label for the accessible name.
88
+ */
89
+ var ChartLegendPlacement = /* @__PURE__ */ function(ChartLegendPlacement) {
90
+ /** Beneath the plot, wrapping — the cartesian layout. */
91
+ ChartLegendPlacement["BOTTOM"] = "BOTTOM";
92
+ /** Beside the plot, stacked, vertically centred on it — the pie layout. */
93
+ ChartLegendPlacement["RIGHT"] = "RIGHT";
94
+ return ChartLegendPlacement;
95
+ }({});
83
96
  /** How the category axis copes when labels outgrow their slots. */
84
97
  var ChartCategoryLabelMode = /* @__PURE__ */ function(ChartCategoryLabelMode) {
85
98
  /** Render every label that fits; otherwise show every nth. The default. */
@@ -100,12 +113,10 @@ var ChartCategoryLabelMode = /* @__PURE__ */ function(ChartCategoryLabelMode) {
100
113
  * DOM and trigger a React warning.
101
114
  */
102
115
  var ChartMarkState = /* @__PURE__ */ function(ChartMarkState) {
103
- /** Full strength — the default, and the hovered or pinned category. */
116
+ /** Full strength — the default, and the hovered category. */
104
117
  ChartMarkState["ACTIVE"] = "active";
105
118
  /** Softened, for categories other than the hovered one. */
106
119
  ChartMarkState["MUTED"] = "muted";
107
- /** Furthest back, for categories outside a pinned one. */
108
- ChartMarkState["FADED"] = "faded";
109
120
  return ChartMarkState;
110
121
  }({});
111
122
  /** Which pair of a rectangle's corners carries the radius. */
@@ -152,4 +163,4 @@ var ChartTargetShape = /* @__PURE__ */ function(ChartTargetShape) {
152
163
  return ChartTargetShape;
153
164
  }({});
154
165
  //#endregion
155
- export { BarChartNormalization, BarCorners, ChartCategoryLabelMode, ChartKind, ChartMarkState, ChartPalette, ChartTargetShape, ChartValueUnit, LineChartCurve };
166
+ export { BarChartNormalization, BarCorners, ChartCategoryLabelMode, ChartKind, ChartLegendPlacement, ChartMarkState, ChartPalette, ChartTargetShape, ChartValueUnit, LineChartCurve };
@@ -1,40 +1,30 @@
1
1
  import { ChartMarkState } from "./types";
2
2
  /**
3
- * Hover and pin interaction shared by every chart.
3
+ * Hover interaction shared by every chart.
4
4
  *
5
- * Two independent axes:
5
+ * Two independent axes, both hover-only:
6
6
  *
7
- * - **Category** — a numeric index along the plot. Hovering previews, clicking
8
- * pins so the tooltip survives the pointer leaving.
9
- * - **Series** — a legend key. Hovering isolates that series, clicking pins the
10
- * isolation.
7
+ * - **Category** — a numeric index along the plot. Hovering previews it: the
8
+ * tooltip opens and everything outside the category recedes.
9
+ * - **Series** — a legend key. Hovering (or focusing) a legend entry isolates
10
+ * that series.
11
11
  *
12
12
  * Both live here rather than one of them in the charts, so there is a single
13
13
  * `isEngaged` boolean. That matters more than it looks: `ChartPlotGroup` uses it
14
14
  * to cancel the mark restore delay, and a second hover source that didn't feed
15
15
  * it would sit through that delay before muting anything and feel broken.
16
16
  *
17
- * Escape or a click on the plot background clears both pins.
17
+ * Deliberately no click layer. Clicking used to pin the hover so it survived
18
+ * the pointer leaving, and it read as the chart getting stuck — the emphasis
19
+ * now always follows the pointer and releases with it.
18
20
  */
19
21
  interface UseChartInteractionOptions {
20
22
  /** Number of hoverable categories. A shrinking count clears stale indices. */
21
23
  categoryCount: number;
22
- /** Legend keys currently rendered. A key that disappears clears a stale pin. */
24
+ /** Legend keys currently rendered. A key that disappears clears stale hover. */
23
25
  seriesKeys?: string[];
24
26
  /** Suppress all interaction — used while loading and when the tooltip is off. */
25
27
  disabled?: boolean;
26
- /**
27
- * Render every mark muted, whatever the two axes say.
28
- *
29
- * Distinct from {@link disabled}, which suppresses *input*. Loading suppresses
30
- * *emphasis*: there is nothing yet worth drawing attention to. Folded in here
31
- * rather than left to each mark, because the alternative is the same
32
- * `isLoading ? MUTED : getMarkState(…)` ternary hand-copied at every mark —
33
- * which is exactly how one of them came to be missing it.
34
- */
35
- isLoading?: boolean;
36
- /** Notified when the pinned category changes. */
37
- onPinnedChange?: (index: number | null) => void;
38
28
  }
39
29
  interface MarkStateArgs {
40
30
  /** Omit for marks that belong to no particular category, such as a line. */
@@ -43,11 +33,9 @@ interface MarkStateArgs {
43
33
  seriesKey?: string;
44
34
  }
45
35
  export interface ChartInteraction {
46
- pinnedIndex: number | null;
47
- /** Pinned wins over hovered — this is what the tooltip follows. */
36
+ /** The hovered category — what the tooltip follows. */
48
37
  activeIndex: number | null;
49
38
  hoveredSeriesKey: string | null;
50
- pinnedSeriesKey: string | null;
51
39
  /** True while *either* axis is engaged; cancels the mark restore delay. */
52
40
  isEngaged: boolean;
53
41
  /**
@@ -55,22 +43,17 @@ export interface ChartInteraction {
55
43
  *
56
44
  * The grammar is inherited from the reference and is the opposite of the
57
45
  * obvious one: engaging something does not brighten it, it softens everything
58
- * else. With something pinned, the unrelated marks recede further still, so
59
- * the pinned mark and the one under the pointer stay legible against them.
46
+ * else.
60
47
  *
61
48
  * A mark has to survive *both* filters to stay active — the two axes
62
- * intersect rather than override, so pinning a series and then hovering a
63
- * category leaves only their overlap lit.
49
+ * intersect rather than override, so hovering a legend entry and a category
50
+ * at once leaves only their overlap lit.
64
51
  */
65
52
  getMarkState: (args: MarkStateArgs) => ChartMarkState;
66
53
  onCategoryEnter: (categoryIndex: number) => void;
67
54
  onCategoryLeave: () => void;
68
- onCategoryClick: (categoryIndex: number) => void;
69
55
  onSeriesEnter: (seriesKey: string) => void;
70
56
  onSeriesLeave: () => void;
71
- onSeriesClick: (seriesKey: string) => void;
72
- /** Attach to the plot backdrop; clears both pins. */
73
- onBackdropClick: () => void;
74
57
  }
75
- export declare function useChartInteraction({ categoryCount, seriesKeys, disabled, isLoading, onPinnedChange, }: UseChartInteractionOptions): ChartInteraction;
58
+ export declare function useChartInteraction({ categoryCount, seriesKeys, disabled, }: UseChartInteractionOptions): ChartInteraction;
76
59
  export {};
@@ -1,43 +1,11 @@
1
1
  import { ChartMarkState } from "./types.js";
2
- import { match } from "ts-pattern";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useCallback, useMemo, useState } from "react";
4
3
  //#region src/charts/useChartInteraction.ts
5
- /** How far back a state sits, so two axes can be compared and the worse taken. */
6
- function severity(state) {
7
- return match(state).with(ChartMarkState.ACTIVE, () => 0).with(ChartMarkState.MUTED, () => 1).with(ChartMarkState.FADED, () => 2).exhaustive();
8
- }
9
- function useChartInteraction({ categoryCount, seriesKeys, disabled = false, isLoading = false, onPinnedChange }) {
4
+ function useChartInteraction({ categoryCount, seriesKeys, disabled = false }) {
10
5
  const [hoveredIndex, setHoveredIndex] = useState(null);
11
- const [pinnedIndex, setPinnedIndex] = useState(null);
12
6
  const [hoveredSeriesKey, setHoveredSeriesKey] = useState(null);
13
- const [pinnedSeriesKey, setPinnedSeriesKey] = useState(null);
14
- const onPinnedChangeRef = useRef(onPinnedChange);
15
- useEffect(() => {
16
- onPinnedChangeRef.current = onPinnedChange;
17
- });
18
7
  const safeHovered = hoveredIndex !== null && hoveredIndex < categoryCount ? hoveredIndex : null;
19
- const safePinned = pinnedIndex !== null && pinnedIndex < categoryCount ? pinnedIndex : null;
20
- const isKnownSeries = (key) => key !== null && (seriesKeys === void 0 || seriesKeys.includes(key));
21
- const safeHoveredSeries = isKnownSeries(hoveredSeriesKey) ? hoveredSeriesKey : null;
22
- const safePinnedSeries = isKnownSeries(pinnedSeriesKey) ? pinnedSeriesKey : null;
23
- const clearPins = useCallback(() => {
24
- setPinnedIndex(null);
25
- setPinnedSeriesKey(null);
26
- onPinnedChangeRef.current?.(null);
27
- }, []);
28
- const hasPin = safePinned !== null || safePinnedSeries !== null;
29
- useEffect(() => {
30
- if (disabled || !hasPin) return;
31
- const onKeyDown = (event) => {
32
- if (event.key === "Escape") clearPins();
33
- };
34
- window.addEventListener("keydown", onKeyDown);
35
- return () => window.removeEventListener("keydown", onKeyDown);
36
- }, [
37
- disabled,
38
- hasPin,
39
- clearPins
40
- ]);
8
+ const safeHoveredSeries = hoveredSeriesKey !== null && (seriesKeys === void 0 || seriesKeys.includes(hoveredSeriesKey)) ? hoveredSeriesKey : null;
41
9
  const onCategoryEnter = useCallback((categoryIndex) => {
42
10
  if (disabled) return;
43
11
  setHoveredIndex(categoryIndex);
@@ -46,80 +14,32 @@ function useChartInteraction({ categoryCount, seriesKeys, disabled = false, isLo
46
14
  if (disabled) return;
47
15
  setHoveredIndex(null);
48
16
  }, [disabled]);
49
- const onCategoryClick = useCallback((categoryIndex) => {
50
- if (disabled) return;
51
- const next = pinnedIndex === categoryIndex ? null : categoryIndex;
52
- setPinnedIndex(next);
53
- onPinnedChangeRef.current?.(next);
54
- }, [disabled, pinnedIndex]);
55
17
  const onSeriesEnter = useCallback((seriesKey) => {
56
18
  setHoveredSeriesKey(seriesKey);
57
19
  }, []);
58
20
  const onSeriesLeave = useCallback(() => {
59
21
  setHoveredSeriesKey(null);
60
22
  }, []);
61
- const onSeriesClick = useCallback((seriesKey) => {
62
- const next = pinnedSeriesKey === seriesKey ? null : seriesKey;
63
- setPinnedSeriesKey(next);
64
- if (next === null) setHoveredSeriesKey(null);
65
- }, [pinnedSeriesKey]);
66
23
  const getMarkState = useCallback(({ categoryIndex, seriesKey }) => {
67
- if (isLoading) return ChartMarkState.MUTED;
68
- const categoryState = (() => {
69
- if (categoryIndex === void 0) return ChartMarkState.ACTIVE;
70
- if (safePinned !== null) {
71
- if (categoryIndex === safePinned) return ChartMarkState.ACTIVE;
72
- if (categoryIndex === safeHovered) return ChartMarkState.MUTED;
73
- return ChartMarkState.FADED;
74
- }
75
- if (safeHovered !== null) return categoryIndex === safeHovered ? ChartMarkState.ACTIVE : ChartMarkState.MUTED;
76
- return ChartMarkState.ACTIVE;
77
- })();
78
- const seriesState = (() => {
79
- if (seriesKey === void 0) return ChartMarkState.ACTIVE;
80
- if (safePinnedSeries !== null) {
81
- if (seriesKey === safePinnedSeries) return ChartMarkState.ACTIVE;
82
- if (seriesKey === safeHoveredSeries) return ChartMarkState.MUTED;
83
- return ChartMarkState.FADED;
84
- }
85
- if (safeHoveredSeries !== null) return seriesKey === safeHoveredSeries ? ChartMarkState.ACTIVE : ChartMarkState.MUTED;
86
- return ChartMarkState.ACTIVE;
87
- })();
88
- return severity(categoryState) >= severity(seriesState) ? categoryState : seriesState;
89
- }, [
90
- isLoading,
91
- safePinned,
92
- safeHovered,
93
- safePinnedSeries,
94
- safeHoveredSeries
95
- ]);
24
+ return categoryIndex !== void 0 && safeHovered !== null && categoryIndex !== safeHovered || seriesKey !== void 0 && safeHoveredSeries !== null && seriesKey !== safeHoveredSeries ? ChartMarkState.MUTED : ChartMarkState.ACTIVE;
25
+ }, [safeHovered, safeHoveredSeries]);
96
26
  return useMemo(() => ({
97
- pinnedIndex: safePinned,
98
- activeIndex: safePinned ?? safeHovered,
27
+ activeIndex: safeHovered,
99
28
  hoveredSeriesKey: safeHoveredSeries,
100
- pinnedSeriesKey: safePinnedSeries,
101
- isEngaged: safePinned !== null || safeHovered !== null || safePinnedSeries !== null || safeHoveredSeries !== null,
29
+ isEngaged: safeHovered !== null || safeHoveredSeries !== null,
102
30
  getMarkState,
103
31
  onCategoryEnter,
104
32
  onCategoryLeave,
105
- onCategoryClick,
106
33
  onSeriesEnter,
107
- onSeriesLeave,
108
- onSeriesClick,
109
- onBackdropClick: clearPins
34
+ onSeriesLeave
110
35
  }), [
111
36
  safeHovered,
112
- safePinned,
113
37
  safeHoveredSeries,
114
- safePinnedSeries,
115
38
  getMarkState,
116
39
  onCategoryEnter,
117
40
  onCategoryLeave,
118
- onCategoryClick,
119
41
  onSeriesEnter,
120
- onSeriesLeave,
121
- onSeriesClick,
122
- clearPins
42
+ onSeriesLeave
123
43
  ]);
124
44
  }
125
45
  //#endregion