@galaxy-io/dls 1.3.4 → 1.3.5

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.
@@ -14,7 +14,6 @@ export interface AccordionProps {
14
14
  variant?: AccordionVariant;
15
15
  size?: AccordionSize;
16
16
  icon?: PhosphorIcon;
17
- isTrailingIcon?: boolean;
18
17
  header: string;
19
18
  subheader?: string;
20
19
  metric?: React.ReactNode;
@@ -26,5 +25,5 @@ export interface AccordionProps {
26
25
  /** Controlled mode: callback when toggle is clicked */
27
26
  onToggle?: () => void;
28
27
  }
29
- declare const Accordion: ({ children, variant, size, icon, isTrailingIcon, header, subheader, metric, borderRadius, padding, isOpenInitial, isOpen: controlledIsOpen, onToggle: controlledOnToggle, }: PropsWithChildren<AccordionProps>) => React.JSX.Element;
28
+ declare const Accordion: ({ children, variant, size, icon, header, subheader, metric, borderRadius, padding, isOpenInitial, isOpen: controlledIsOpen, onToggle: controlledOnToggle, }: PropsWithChildren<AccordionProps>) => React.JSX.Element;
30
29
  export default Accordion;
@@ -65,7 +65,7 @@ var AccordionContentInner = withTheme(/*#__PURE__*/ styled("div")({
65
65
  class: "a1f5gwwu",
66
66
  propsAsIs: false
67
67
  }));
68
- var Accordion = ({ children, variant = "PRIMARY", size = "MEDIUM", icon, isTrailingIcon = false, header, subheader, metric, borderRadius, padding = "12px", isOpenInitial = false, isOpen: controlledIsOpen, onToggle: controlledOnToggle }) => {
68
+ var Accordion = ({ children, variant = "PRIMARY", size = "MEDIUM", icon, header, subheader, metric, borderRadius, padding = "12px", isOpenInitial = false, isOpen: controlledIsOpen, onToggle: controlledOnToggle }) => {
69
69
  const [internalIsOpen, setInternalIsOpen] = useState(isOpenInitial);
70
70
  const isControlled = controlledIsOpen !== void 0;
71
71
  const isOpen = isControlled ? controlledIsOpen : internalIsOpen;
@@ -87,31 +87,23 @@ var Accordion = ({ children, variant = "PRIMARY", size = "MEDIUM", icon, isTrail
87
87
  children: [/* @__PURE__ */ jsxs(FlexWrapper, {
88
88
  alignItems: AlignItems.CENTER,
89
89
  gap: 8,
90
- children: [
91
- icon && !isTrailingIcon && /* @__PURE__ */ jsx(Icon, {
92
- component: icon,
93
- variant: isOpen ? IconVariant.PRIMARY : IconVariant.SECONDARY,
94
- size: 16
95
- }),
96
- /* @__PURE__ */ jsxs(FlexWrapper, {
97
- direction: FlexDirection.COLUMN,
98
- gap: 2,
99
- children: [/* @__PURE__ */ jsx(Text, {
100
- variant: isOpen ? TextVariant.PRIMARY : TextVariant.SECONDARY,
101
- weight: TextWeight.MEDIUM,
102
- children: header
103
- }), subheader && /* @__PURE__ */ jsx(Text, {
104
- size: TextSize.BODY_SM,
105
- variant: isOpen ? TextVariant.SECONDARY : TextVariant.TERTIARY,
106
- children: subheader
107
- })]
108
- }),
109
- icon && isTrailingIcon && /* @__PURE__ */ jsx(Icon, {
110
- component: icon,
111
- variant: isOpen ? IconVariant.PRIMARY : IconVariant.SECONDARY,
112
- size: 16
113
- })
114
- ]
90
+ children: [icon && /* @__PURE__ */ jsx(Icon, {
91
+ component: icon,
92
+ variant: isOpen ? IconVariant.PRIMARY : IconVariant.SECONDARY,
93
+ size: 16
94
+ }), /* @__PURE__ */ jsxs(FlexWrapper, {
95
+ direction: FlexDirection.COLUMN,
96
+ gap: 2,
97
+ children: [/* @__PURE__ */ jsx(Text, {
98
+ variant: isOpen ? TextVariant.PRIMARY : TextVariant.SECONDARY,
99
+ weight: TextWeight.MEDIUM,
100
+ children: header
101
+ }), subheader && /* @__PURE__ */ jsx(Text, {
102
+ size: TextSize.BODY_SM,
103
+ variant: isOpen ? TextVariant.SECONDARY : TextVariant.TERTIARY,
104
+ children: subheader
105
+ })]
106
+ })]
115
107
  }), /* @__PURE__ */ jsxs(FlexWrapper, {
116
108
  alignItems: AlignItems.CENTER,
117
109
  gap: 8,
@@ -13,12 +13,6 @@ export type BarChartProps<TMetric extends string, TComponentKey extends string =
13
13
  groups: BarChartGroupDatum<NoInfer<TMetric>, NoInfer<TComponentKey>>[];
14
14
  /** @default BarChartNormalization.NONE */
15
15
  normalization?: BarChartNormalization;
16
- /**
17
- * Notified when a group is pinned or unpinned by a click.
18
- *
19
- * The index is a position in `groups`.
20
- */
21
- onPinnedChange?: (groupIndex: number | null) => void;
22
16
  };
23
17
  /**
24
18
  * Bar chart with a three-level data model.
@@ -27,5 +21,5 @@ export type BarChartProps<TMetric extends string, TComponentKey extends string =
27
21
  * `bars` in a group render side by side, several `components` in a bar stack,
28
22
  * and a chart doing both at once needs no extra configuration.
29
23
  */
30
- declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, noTooltip, tooltipRenderer, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, onPinnedChange, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
24
+ declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, noTooltip, tooltipRenderer, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
31
25
  export default BarChart;
@@ -17,7 +17,7 @@ import { Fragment, 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, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, noTooltip = false, tooltipRenderer, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription, onPinnedChange }) => {
20
+ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization = BarChartNormalization.NONE, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, noTooltip = false, tooltipRenderer, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
21
21
  const containerRef = useRef(null);
22
22
  const colors = useSeriesColorResolver(series);
23
23
  /**
@@ -105,8 +105,7 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
105
105
  categoryCount: geometry.bands.length,
106
106
  seriesKeys: legendKeys,
107
107
  disabled: noTooltip || isLoading || isEmpty,
108
- isLoading,
109
- onPinnedChange
108
+ isLoading
110
109
  });
111
110
  const buildCategoryTooltip = useCallback((index) => {
112
111
  const band = geometry.bands[index];
@@ -1,27 +1,27 @@
1
1
  import type { ChartCategoryTarget } from "./types";
2
2
  import type { ChartInteraction } from "./useChartInteraction";
3
3
  /**
4
- * The interactive layer: one focusable target per category, above the marks.
4
+ * The hover layer: one transparent target per category, above the marks.
5
5
  *
6
- * Shared by all three charts so pointer and keyboard behave identically. Before
7
- * this existed each chart spelled hit testing its own way two rendered targets
8
- * conditionally, the pie attached handlers permanently and computed
9
- * `data-interactive` instead — and none of them was reachable by keyboard at
10
- * all.
6
+ * Shared by all three charts so hit testing is spelled once. Targets carry no
7
+ * roles and no tab stops the plot is `role="img"` and its accessible story is
8
+ * the frame's label and description, while pointer users get the tooltip.
11
9
  *
12
- * **Focus rides the existing hover channel.** `onFocus` calls
13
- * `onCategoryEnter` and `onBlur` calls `onCategoryLeave`, so the tooltip, the
14
- * cursor line, the legend dimming and every `getMarkState` light up for a
15
- * keyboard reader exactly as they do for a pointer — without
16
- * `useChartInteraction` growing a third state axis to keep in sync.
10
+ * A category with nothing to show gets no target at all: hovering an all-zero
11
+ * bar group or an all-null line column must not open a tooltip with a heading
12
+ * and no rows, so the empty stretch of plot is simply not hoverable.
17
13
  */
18
14
  interface ChartCategoryTargetsProps {
19
15
  targets: ChartCategoryTarget[];
20
16
  interaction: ChartInteraction;
21
- /** Accessible name for one category. Derived from the tooltip, so they agree. */
22
- getAriaLabel: (index: number) => string;
23
- /** Arrow keys wrap at the ends. True for radial charts — a circle has no ends. */
24
- wrap?: boolean;
17
+ /**
18
+ * Category indices whose tooltip would have no rows.
19
+ *
20
+ * Skipped entirely rather than rendered inert, so the pointer falls through
21
+ * to nothing. Indexing is unaffected: `targets.map` still passes the true
22
+ * category index for every target that does render.
23
+ */
24
+ inertIndices?: ReadonlySet<number>;
25
25
  }
26
- declare const ChartCategoryTargets: ({ targets, interaction, getAriaLabel, wrap, }: ChartCategoryTargetsProps) => import("react").JSX.Element;
26
+ declare const ChartCategoryTargets: ({ targets, interaction, inertIndices, }: ChartCategoryTargetsProps) => import("react").JSX.Element;
27
27
  export default ChartCategoryTargets;
@@ -1,75 +1,24 @@
1
1
  import { ChartTargetShape } from "./types.js";
2
- import { clamp } from "./chartScales.js";
3
- import { ChartCategoryTargetLayer, ChartHitTarget, ChartHitTargetPath } from "./ChartPrimitives.js";
2
+ import { ChartHitTarget, ChartHitTargetPath } from "./ChartPrimitives.js";
4
3
  import { match } from "ts-pattern";
5
- import { useCallback, useRef, useState } from "react";
6
- import { jsx } from "react/jsx-runtime";
4
+ import { Fragment, jsx } from "react/jsx-runtime";
7
5
  //#region src/charts/ChartCategoryTargets.tsx
8
- var ChartCategoryTargets = ({ targets, interaction, getAriaLabel, wrap = false }) => {
9
- const [focusIndex, setFocusIndex] = useState(0);
10
- const nodeRefs = useRef([]);
11
- /**
12
- * Roving tabindex, preferring whatever is pinned.
13
- *
14
- * Tabbing away and back returns to the category the reader was holding rather
15
- * than to the start, which is the difference between the pin being a place
16
- * you are and a thing that merely happened.
17
- */
18
- const rovingIndex = clamp(interaction.pinnedIndex ?? focusIndex, 0, targets.length - 1);
19
- const moveTo = useCallback((next) => {
20
- const last = targets.length - 1;
21
- const bounded = wrap ? (next % targets.length + targets.length) % targets.length : clamp(next, 0, last);
22
- setFocusIndex(bounded);
23
- nodeRefs.current[bounded]?.focus();
24
- }, [targets.length, wrap]);
25
- const onKeyDown = useCallback((event, index) => {
26
- if (match(event.key).with("ArrowRight", "ArrowDown", () => moveTo(index + 1)).with("ArrowLeft", "ArrowUp", () => moveTo(index - 1)).with("Home", () => moveTo(0)).with("End", () => moveTo(targets.length - 1)).with("Enter", " ", () => interaction.onCategoryClick(index)).with("Escape", () => interaction.onBackdropClick()).otherwise(() => void 0) !== void 0 || [
27
- "ArrowRight",
28
- "ArrowDown",
29
- "ArrowLeft",
30
- "ArrowUp",
31
- "Home",
32
- "End",
33
- "Enter",
34
- " ",
35
- "Escape"
36
- ].includes(event.key)) event.preventDefault();
37
- }, [
38
- moveTo,
39
- interaction,
40
- targets.length
41
- ]);
42
- return /* @__PURE__ */ jsx(ChartCategoryTargetLayer, { children: targets.map((target, index) => {
43
- const shared = {
44
- tabIndex: index === rovingIndex ? 0 : -1,
45
- role: "button",
46
- "aria-label": getAriaLabel(index),
47
- "aria-pressed": interaction.pinnedIndex === index,
48
- "data-interactive": "true",
49
- onFocus: () => interaction.onCategoryEnter(index),
50
- onBlur: () => interaction.onCategoryLeave(),
51
- onMouseEnter: () => interaction.onCategoryEnter(index),
52
- onMouseLeave: () => interaction.onCategoryLeave(),
53
- onClick: () => interaction.onCategoryClick(index),
54
- onKeyDown: (event) => onKeyDown(event, index)
55
- };
56
- return match(target).with({ shape: ChartTargetShape.RECT }, (rect) => /* @__PURE__ */ jsx(ChartHitTarget, {
57
- ref: (node) => {
58
- nodeRefs.current[index] = node;
59
- },
60
- x: rect.x,
61
- y: rect.y,
62
- width: rect.width,
63
- height: rect.height,
64
- ...shared
65
- }, rect.key)).with({ shape: ChartTargetShape.PATH }, (path) => /* @__PURE__ */ jsx(ChartHitTargetPath, {
66
- ref: (node) => {
67
- nodeRefs.current[index] = node;
68
- },
69
- d: path.d,
70
- ...shared
71
- }, path.key)).exhaustive();
72
- }) });
73
- };
6
+ var ChartCategoryTargets = ({ targets, interaction, inertIndices }) => /* @__PURE__ */ jsx(Fragment, { children: targets.map((target, index) => {
7
+ if (inertIndices?.has(index)) return null;
8
+ const shared = {
9
+ onMouseEnter: () => interaction.onCategoryEnter(index),
10
+ onMouseLeave: () => interaction.onCategoryLeave()
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
+ }) });
74
23
  //#endregion
75
24
  export { ChartCategoryTargets as default };
@@ -1,13 +1,13 @@
1
1
  import type { ReactNode } from "react";
2
- import { ChartKind } from "./types";
2
+ import { ChartKind, ChartLegendPlacement } from "./types";
3
3
  import type { ChartCategoryTarget, ChartLegendItem, 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";
7
7
  /**
8
8
  * The shell every chart renders into: measured root, reserved plot box, SVG and
9
- * its margin translate, the interactive plot group, the backdrop, the tooltip,
10
- * the empty state and the legend.
9
+ * its margin translate, the plot group, the hover targets, the tooltip, the
10
+ * empty state and the legend.
11
11
  *
12
12
  * **The frame owns the plot translate.** Marks are drawn inside a
13
13
  * `translate(margin.left, margin.top)` group, and every coordinate handed to
@@ -56,7 +56,7 @@ export interface ChartFrameProps {
56
56
  */
57
57
  axes?: ReactNode;
58
58
  /**
59
- * Marks, drawn inside the plot group above the backdrop.
59
+ * Marks, drawn inside the plot group beneath the hover targets.
60
60
  *
61
61
  * Rendered only once measured, so no caller writes that guard. Note it is
62
62
  * still *evaluated* on the unmeasured pass — as is the geometry it maps over,
@@ -72,8 +72,8 @@ export interface ChartFrameProps {
72
72
  *
73
73
  * Taken as a function rather than a finished model because the frame needs it
74
74
  * twice: once for the category under the pointer, and once per category to
75
- * name it for a screen reader. Deriving both from the same builder is what
76
- * makes the accessible name incapable of disagreeing with the tooltip.
75
+ * decide which categories are inert. Deriving both from the same builder is
76
+ * what makes "no tooltip rows" and "not hoverable" the same fact.
77
77
  *
78
78
  * Must be stable; memoize it at the call site.
79
79
  */
@@ -88,10 +88,6 @@ export interface ChartFrameProps {
88
88
  categoryTargets?: ChartCategoryTarget[];
89
89
  /** Applied to the target layer. Radial charts pass their centre translate. */
90
90
  categoryTargetsTransform?: string;
91
- /** Arrow keys wrap at the ends. True for radial charts. */
92
- wrapCategoryNavigation?: boolean;
93
- /** Overrides the accessible name of a category, which defaults to its tooltip. */
94
- describeCategory?: (index: number) => string;
95
91
  /** Half a category's width, so the tooltip sits beside the marks. */
96
92
  anchorHalfWidth?: number;
97
93
  /** Radial charts pass their centre, in plot coordinates. */
@@ -103,8 +99,14 @@ export interface ChartFrameProps {
103
99
  contentWhenEmpty?: ReactNode;
104
100
  showLegend?: boolean;
105
101
  legendItems: ChartLegendItem[];
106
- /** Defaults to the left margin, aligning the legend with the plot, not the gutter. */
102
+ /** Where the legend sits. RIGHT stacks it beside the plot, vertically centred. */
103
+ legendPlacement?: ChartLegendPlacement;
104
+ /**
105
+ * BOTTOM: left padding, defaulting to the left margin so the legend aligns
106
+ * with the plot rather than the axis gutter. RIGHT: the gap between the plot
107
+ * box and the legend, defaulting to {@link CHART_LEGEND_SIDE_GAP}.
108
+ */
107
109
  legendInset?: number;
108
110
  }
109
- declare const ChartFrame: ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip, buildCategoryTooltip, tooltipRenderer, categoryTargets, categoryTargetsTransform, wrapCategoryNavigation, describeCategory, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading, contentWhenEmpty, showLegend, legendItems, legendInset, }: ChartFrameProps) => import("react").JSX.Element;
111
+ declare const ChartFrame: ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip, buildCategoryTooltip, tooltipRenderer, categoryTargets, categoryTargetsTransform, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading, contentWhenEmpty, showLegend, legendItems, legendPlacement, legendInset, }: ChartFrameProps) => import("react").JSX.Element;
110
112
  export default ChartFrame;
@@ -1,16 +1,16 @@
1
- import { ChartKind } from "./types.js";
2
- import { describeTooltipModel } from "./chartFormat.js";
3
- import { ChartBackdrop, ChartPlotBox, ChartPlotGroup, ChartRoot, ChartSvg } from "./ChartPrimitives.js";
1
+ import { ChartKind, ChartLegendPlacement } from "./types.js";
2
+ import "./constants.js";
3
+ import { ChartPlotBox, ChartPlotGroup, 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
7
  import ChartTooltip from "./ChartTooltip.js";
8
8
  import { match } from "ts-pattern";
9
- import { useCallback, useId, useMemo } from "react";
9
+ import { useId, useMemo } from "react";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
11
  //#region src/charts/ChartFrame.tsx
12
- var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip = false, buildCategoryTooltip, tooltipRenderer, categoryTargets, categoryTargetsTransform, wrapCategoryNavigation, describeCategory, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading = false, contentWhenEmpty, showLegend = false, legendItems, legendInset }) => {
13
- const { width, height, plotWidth, plotHeight, margin, isMeasured, reservedHeight, reservedAspectRatio } = dimensions;
12
+ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction, kind, categoryCount, ariaLabel, ariaDescription, axes, children, plotOverlay, noTooltip = false, buildCategoryTooltip, tooltipRenderer, categoryTargets, categoryTargetsTransform, anchorHalfWidth, placeAwayFromX, verticalAlign, isEmpty, isLoading = false, contentWhenEmpty, showLegend = false, legendItems, legendPlacement = ChartLegendPlacement.BOTTOM, legendInset }) => {
13
+ const { width, height, margin, isMeasured, reservedHeight, reservedAspectRatio } = dimensions;
14
14
  const generatedId = useId();
15
15
  const descriptionId = ariaDescription ? `${generatedId}-desc` : void 0;
16
16
  /**
@@ -48,41 +48,39 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
48
48
  margin.top
49
49
  ]);
50
50
  /**
51
- * Name each category from its own tooltip, unless the chart says otherwise.
51
+ * Categories whose tooltip would have no rows.
52
52
  *
53
- * Same builder as the visible tooltip, so the two cannot describe the chart
54
- * differently.
53
+ * Decided here from the same builder that feeds the visible tooltip so
54
+ * "no data at this X" means exactly one thing in every chart: an all-zero
55
+ * bar group and an all-null line column both produce a rowless model, and
56
+ * neither should be hoverable. Their targets are simply not rendered.
55
57
  */
56
- const nameCategory = useCallback((index) => {
57
- if (describeCategory) return describeCategory(index);
58
- const model = buildCategoryTooltip(index);
59
- return model ? describeTooltipModel(model) : "";
60
- }, [describeCategory, buildCategoryTooltip]);
61
- /**
62
- * Whether the marks are reachable at all.
63
- *
64
- * Drives the SVG's role, which is the real cost of keyboard access:
65
- * `role="img"` prunes the whole subtree from the accessibility tree, so
66
- * nothing inside it can ever be focused *and* announced. A chart with live
67
- * targets therefore has to be a `group` — and everything that is not a target
68
- * has to be explicitly hidden, since under `group` it would otherwise become
69
- * traversable. When there is nothing to reach, `img` stays: it announces one
70
- * summary rather than an empty group.
71
- */
72
- const hasFocusableTargets = isMeasured && !noTooltip && !isLoading && !isEmpty && (categoryTargets?.length ?? 0) > 0;
58
+ const inertIndices = useMemo(() => {
59
+ const inert = /* @__PURE__ */ new Set();
60
+ (categoryTargets ?? []).forEach((_, index) => {
61
+ const model = buildCategoryTooltip(index);
62
+ if (!model || model.rows.length === 0) inert.add(index);
63
+ });
64
+ return inert;
65
+ }, [categoryTargets, buildCategoryTooltip]);
66
+ const hasTargets = isMeasured && !noTooltip && !isLoading && !isEmpty && (categoryTargets?.length ?? 0) > 0;
67
+ const legendRight = legendPlacement === ChartLegendPlacement.RIGHT;
73
68
  return /* @__PURE__ */ jsxs(ChartRoot, {
74
69
  $fillWidth: fillWidth,
75
70
  $fillHeight: fillHeight,
71
+ $legendRight: legendRight,
76
72
  children: [/* @__PURE__ */ jsxs(ChartPlotBox, {
77
73
  ref: containerRef,
78
74
  $reservedHeight: reservedHeight,
79
75
  $reservedAspectRatio: reservedAspectRatio,
80
76
  $fillHeight: fillHeight,
77
+ $legendRight: legendRight,
78
+ $squareFromHeight: legendRight && fillWidth && reservedAspectRatio === void 0,
81
79
  children: [
82
80
  isMeasured && /* @__PURE__ */ jsxs(ChartSvg, {
83
81
  width,
84
82
  height,
85
- role: hasFocusableTargets ? "group" : "img",
83
+ role: "img",
86
84
  "aria-label": resolvedAriaLabel,
87
85
  "aria-describedby": descriptionId,
88
86
  children: [ariaDescription && /* @__PURE__ */ jsx("desc", {
@@ -92,25 +90,16 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
92
90
  transform: `translate(${margin.left}, ${margin.top})`,
93
91
  children: [
94
92
  axes,
95
- /* @__PURE__ */ jsx("g", {
96
- "aria-hidden": "true",
97
- children: /* @__PURE__ */ jsxs(ChartPlotGroup, {
98
- "data-active": interaction.isEngaged ? "true" : "false",
99
- children: [/* @__PURE__ */ jsx(ChartBackdrop, {
100
- width: plotWidth,
101
- height: plotHeight,
102
- "data-interactive": interaction.pinnedIndex !== null ? "true" : "false",
103
- onClick: interaction.onBackdropClick
104
- }), children]
105
- })
93
+ /* @__PURE__ */ jsx(ChartPlotGroup, {
94
+ "data-active": interaction.isEngaged ? "true" : "false",
95
+ children
106
96
  }),
107
- hasFocusableTargets && categoryTargets && /* @__PURE__ */ jsx("g", {
97
+ hasTargets && categoryTargets && /* @__PURE__ */ jsx("g", {
108
98
  transform: categoryTargetsTransform,
109
99
  children: /* @__PURE__ */ jsx(ChartCategoryTargets, {
110
100
  targets: categoryTargets,
111
101
  interaction,
112
- getAriaLabel: nameCategory,
113
- wrap: wrapCategoryNavigation
102
+ inertIndices
114
103
  })
115
104
  })
116
105
  ]
@@ -130,12 +119,11 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
130
119
  ]
131
120
  }), showLegend && /* @__PURE__ */ jsx(ChartLegend, {
132
121
  items: legendItems,
133
- inset: legendInset ?? margin.left,
122
+ inset: legendInset ?? (legendRight ? 16 : margin.left),
123
+ vertical: legendRight,
134
124
  hoveredKey: interaction.hoveredSeriesKey,
135
- pinnedKey: interaction.pinnedSeriesKey,
136
125
  onItemEnter: interaction.onSeriesEnter,
137
- onItemLeave: interaction.onSeriesLeave,
138
- onItemClick: interaction.onSeriesClick
126
+ onItemLeave: interaction.onSeriesLeave
139
127
  })]
140
128
  });
141
129
  };
@@ -5,20 +5,19 @@ interface ChartLegendProps {
5
5
  inset: number;
6
6
  /** Series key currently hovered anywhere in the chart. */
7
7
  hoveredKey?: string | null;
8
- /** Series key currently pinned. Drives `aria-pressed`. */
9
- pinnedKey?: string | null;
8
+ /** Stack entries in a column — the side-placed layout. */
9
+ vertical?: boolean;
10
10
  /**
11
11
  * Supplying these makes the legend interactive. Omit them and it renders as
12
12
  * inert text, with no button semantics for a screen reader to announce.
13
13
  */
14
14
  onItemEnter?: (key: string) => void;
15
15
  onItemLeave?: () => void;
16
- onItemClick?: (key: string) => void;
17
16
  }
18
17
  /**
19
18
  * Legend for the chart components.
20
19
  *
21
- * Hovering an entry isolates its series and clicking pins that isolation.
20
+ * Hovering or focusing an entry isolates its series; leaving releases it.
22
21
  *
23
22
  * Deliberately *not* a visibility toggle: hiding a series would have to rescale
24
23
  * the value axis to stay honest — otherwise the remaining bars sit mysteriously
@@ -26,5 +25,5 @@ interface ChartLegendProps {
26
25
  * Isolation only changes opacity, so the domain is untouched and the numbers
27
26
  * keep meaning what they did.
28
27
  */
29
- declare const ChartLegend: ({ items, inset, hoveredKey, pinnedKey, onItemEnter, onItemLeave, onItemClick, }: ChartLegendProps) => import("react").JSX.Element | null;
28
+ declare const ChartLegend: ({ items, inset, hoveredKey, vertical, onItemEnter, onItemLeave, }: ChartLegendProps) => import("react").JSX.Element | null;
30
29
  export default ChartLegend;
@@ -7,7 +7,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  /**
8
8
  * Legend for the chart components.
9
9
  *
10
- * Hovering an entry isolates its series and clicking pins that isolation.
10
+ * Hovering or focusing an entry isolates its series; leaving releases it.
11
11
  *
12
12
  * Deliberately *not* a visibility toggle: hiding a series would have to rescale
13
13
  * the value axis to stay honest — otherwise the remaining bars sit mysteriously
@@ -15,12 +15,13 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
15
  * Isolation only changes opacity, so the domain is untouched and the numbers
16
16
  * keep meaning what they did.
17
17
  */
18
- var ChartLegend = ({ items, inset, hoveredKey = null, pinnedKey = null, onItemEnter, onItemLeave, onItemClick }) => {
18
+ var ChartLegend = ({ items, inset, hoveredKey = null, vertical = false, onItemEnter, onItemLeave }) => {
19
19
  if (items.length === 0) return null;
20
- const isInteractive = Boolean(onItemEnter ?? onItemClick);
21
- const activeKey = pinnedKey ?? hoveredKey;
20
+ const isInteractive = Boolean(onItemEnter);
21
+ const activeKey = hoveredKey;
22
22
  return /* @__PURE__ */ jsx(ChartLegendRow, {
23
23
  $inset: inset,
24
+ $vertical: vertical,
24
25
  "data-active": activeKey !== null ? "true" : "false",
25
26
  children: items.map((item) => {
26
27
  const state = activeKey === null || item.key === activeKey ? ChartMarkState.ACTIVE : ChartMarkState.MUTED;
@@ -38,12 +39,10 @@ var ChartLegend = ({ items, inset, hoveredKey = null, pinnedKey = null, onItemEn
38
39
  return /* @__PURE__ */ jsx(ChartLegendItemButton, {
39
40
  type: "button",
40
41
  "data-state": state,
41
- "aria-pressed": pinnedKey === item.key,
42
42
  onMouseEnter: () => onItemEnter?.(item.key),
43
43
  onMouseLeave: () => onItemLeave?.(),
44
44
  onFocus: () => onItemEnter?.(item.key),
45
45
  onBlur: () => onItemLeave?.(),
46
- onClick: () => onItemClick?.(item.key),
47
46
  children: content
48
47
  }, item.key);
49
48
  })
@@ -4,10 +4,18 @@
4
4
  * `min-width: 0` is load-bearing rather than cosmetic — without it a chart in a
5
5
  * flex row feeds its min-content width back into the ResizeObserver and the
6
6
  * measurement can oscillate.
7
+ *
8
+ * `$legendRight` rotates the layout: plot box and legend become a row with
9
+ * `align-items: center`, which is the whole of "the legend is vertically
10
+ * centred on the pie" — the pie is inscribed centred in its box, so centring
11
+ * the legend against the box centres it against the pie. It also holds in the
12
+ * other direction: a legend taller than the box centres the *pie* against the
13
+ * legend instead of pinning it to the top.
7
14
  */
8
15
  export declare const ChartRoot: import("@linaria/react").StyledComponent<import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
9
16
  $fillWidth?: boolean;
10
17
  $fillHeight?: boolean;
18
+ $legendRight?: boolean;
11
19
  }>;
12
20
  /**
13
21
  * Holds the plot, anchors the tooltip, and is the element that gets measured.
@@ -29,6 +37,8 @@ export declare const ChartPlotBox: import("@linaria/react").StyledComponent<impo
29
37
  $reservedHeight?: number;
30
38
  $reservedAspectRatio?: number;
31
39
  $fillHeight?: boolean;
40
+ $legendRight?: boolean;
41
+ $squareFromHeight?: boolean;
32
42
  }>;
33
43
  /**
34
44
  * `overflow: visible` lets a value label above a full-height bar paint outside
@@ -45,32 +55,6 @@ export declare const ChartLinePath: import("@linaria/react").StyledComponent<imp
45
55
  export declare const ChartAreaPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
46
56
  export declare const ChartPointCircle: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGCircleElement> & Record<never, unknown>>;
47
57
  export declare const ChartArcPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
48
- /**
49
- * Wrapper for the category target layer.
50
- *
51
- * Exists to carry the focus-ring colour: the targets themselves are plain
52
- * `styled.x` because they receive refs (see the module note above), so they
53
- * cannot read the theme directly. Setting `color` here lets them use
54
- * `currentColor` and still take the token rather than inheriting whatever the
55
- * consumer's page happens to set.
56
- */
57
- export declare const ChartCategoryTargetLayer: import("react").ComponentType<Pick<import("react").SVGProps<SVGGElement> & {
58
- theme: import("../theme").Theme;
59
- } & {
60
- as?: React.ElementType;
61
- }, "as" | keyof import("react").SVGProps<SVGGElement>> & {
62
- theme?: import("@callstack/react-theme-provider").$DeepPartial<import("../theme").Theme> | undefined;
63
- }> & import("@callstack/react-theme-provider/typings/hoist-non-react-statics").NonReactStatics<import("react").ComponentType<import("react").SVGProps<SVGGElement> & {
64
- theme: import("../theme").Theme;
65
- } & {
66
- as?: React.ElementType;
67
- }> & {
68
- __wyw_meta: unknown;
69
- } & import("react").FunctionComponent<import("react").SVGProps<SVGGElement> & {
70
- theme: import("../theme").Theme;
71
- } & {
72
- as?: React.ElementType;
73
- }>, {}>;
74
58
  /**
75
59
  * Per-category hit target, rendered above the marks.
76
60
  *
@@ -78,6 +62,9 @@ export declare const ChartCategoryTargetLayer: import("react").ComponentType<Pic
78
62
  * bar is as easy to hover as a two-hundred-pixel one, and crossing between
79
63
  * segments inside a category fires no events at all — so the tooltip holds
80
64
  * still instead of chasing each segment.
65
+ *
66
+ * No cursor and no focus treatment: the target only previews on hover, and a
67
+ * pointer cursor would promise a click that does nothing.
81
68
  */
82
69
  export declare const ChartHitTarget: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGRectElement> & Record<never, unknown>>;
83
70
  /**
@@ -85,12 +72,10 @@ export declare const ChartHitTarget: import("@linaria/react").StyledComponent<im
85
72
  *
86
73
  * A pie has no category axis to band, so its targets are the arc shapes
87
74
  * themselves — drawn again, transparent, above the marks. That keeps hit
88
- * testing and focus identical across all three charts instead of the pie
89
- * hit-testing its visible arcs directly.
75
+ * testing identical across all three charts instead of the pie hit-testing its
76
+ * visible arcs directly.
90
77
  */
91
78
  export declare const ChartHitTargetPath: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGPathElement> & Record<never, unknown>>;
92
- /** Full-plot backdrop; clicking it clears a pinned category. */
93
- export declare const ChartBackdrop: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGRectElement> & Record<never, unknown>>;
94
79
  /**
95
80
  * Solid hairline rather than a dashed rule, and drawn at every tick including
96
81
  * zero — the line under the bars is what makes them read as resting on a
@@ -213,14 +198,16 @@ export declare const ChartEmptyOverlay: import("react").ComponentType<Pick<impor
213
198
  */
214
199
  export declare const ChartLegendRow: import("@linaria/react").StyledComponent<import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
215
200
  $inset: number;
201
+ $vertical?: boolean;
216
202
  }>;
217
203
  /**
218
204
  * One legend entry, when the legend is interactive.
219
205
  *
220
- * A real `<button>` rather than a styled div: it is clickable, so it has to be
221
- * reachable and operable by keyboard. `aria-pressed` carries the pinned state
222
- * the only thing here a screen reader can truthfully announce, since the plot
223
- * itself is `role="img"` and its marks are not in the accessibility tree.
206
+ * A real `<button>` rather than a styled div so it sits in the tab order:
207
+ * focusing an entry previews its series exactly as hovering does, which is the
208
+ * one keyboard affordance the charts keep the plot itself is `role="img"` and
209
+ * its marks are not in the accessibility tree. Activating it does nothing;
210
+ * isolation follows the pointer or the focus and releases with it.
224
211
  *
225
212
  * Shares the mark opacity ramp via `data-state`, so an entry recedes in step
226
213
  * with the series it names.