@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,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
  /**
@@ -22,7 +22,8 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
22
22
  * showing three. The two charts previously counted different things for the
23
23
  * same situation, which is the drift this removes.
24
24
  */
25
- const resolvedAriaLabel = ariaLabel ?? match(kind).with(ChartKind.PIE, () => `Pie chart with ${categoryCount} slices`).with(ChartKind.BAR, () => `Bar chart with ${categoryCount} categories and ${legendItems.length} series`).with(ChartKind.LINE, () => `Line chart with ${categoryCount} categories and ${legendItems.length} series`).exhaustive();
25
+ const kindLabel = match(kind).with(ChartKind.PIE, () => "Pie").with(ChartKind.BAR, () => "Bar").with(ChartKind.LINE, () => "Line").exhaustive();
26
+ const resolvedAriaLabel = ariaLabel ?? (isLoading ? `${kindLabel} chart, loading` : match(kind).with(ChartKind.PIE, () => `Pie chart with ${categoryCount} slices`).with(ChartKind.BAR, () => `Bar chart with ${categoryCount} categories and ${legendItems.length} series`).with(ChartKind.LINE, () => `Line chart with ${categoryCount} categories and ${legendItems.length} series`).exhaustive());
26
27
  const tooltipModel = useMemo(() => noTooltip || interaction.activeIndex === null ? null : buildCategoryTooltip(interaction.activeIndex), [
27
28
  noTooltip,
28
29
  buildCategoryTooltip,
@@ -48,41 +49,39 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
48
49
  margin.top
49
50
  ]);
50
51
  /**
51
- * Name each category from its own tooltip, unless the chart says otherwise.
52
+ * Categories whose tooltip would have no rows.
52
53
  *
53
- * Same builder as the visible tooltip, so the two cannot describe the chart
54
- * differently.
54
+ * Decided here from the same builder that feeds the visible tooltip so
55
+ * "no data at this X" means exactly one thing in every chart: an all-zero
56
+ * bar group and an all-null line column both produce a rowless model, and
57
+ * neither should be hoverable. Their targets are simply not rendered.
55
58
  */
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;
59
+ const inertIndices = useMemo(() => {
60
+ const inert = /* @__PURE__ */ new Set();
61
+ (categoryTargets ?? []).forEach((_, index) => {
62
+ const model = buildCategoryTooltip(index);
63
+ if (!model || model.rows.length === 0) inert.add(index);
64
+ });
65
+ return inert;
66
+ }, [categoryTargets, buildCategoryTooltip]);
67
+ const hasTargets = isMeasured && !noTooltip && !isLoading && !isEmpty && (categoryTargets?.length ?? 0) > 0;
68
+ const legendRight = legendPlacement === ChartLegendPlacement.RIGHT;
73
69
  return /* @__PURE__ */ jsxs(ChartRoot, {
74
70
  $fillWidth: fillWidth,
75
71
  $fillHeight: fillHeight,
72
+ $legendRight: legendRight,
76
73
  children: [/* @__PURE__ */ jsxs(ChartPlotBox, {
77
74
  ref: containerRef,
78
75
  $reservedHeight: reservedHeight,
79
76
  $reservedAspectRatio: reservedAspectRatio,
80
77
  $fillHeight: fillHeight,
78
+ $legendRight: legendRight,
79
+ $squareFromHeight: legendRight && fillWidth && reservedAspectRatio === void 0,
81
80
  children: [
82
81
  isMeasured && /* @__PURE__ */ jsxs(ChartSvg, {
83
82
  width,
84
83
  height,
85
- role: hasFocusableTargets ? "group" : "img",
84
+ role: "img",
86
85
  "aria-label": resolvedAriaLabel,
87
86
  "aria-describedby": descriptionId,
88
87
  children: [ariaDescription && /* @__PURE__ */ jsx("desc", {
@@ -92,25 +91,16 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
92
91
  transform: `translate(${margin.left}, ${margin.top})`,
93
92
  children: [
94
93
  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
- })
94
+ /* @__PURE__ */ jsx(ChartPlotGroup, {
95
+ "data-active": interaction.isEngaged ? "true" : "false",
96
+ children
106
97
  }),
107
- hasFocusableTargets && categoryTargets && /* @__PURE__ */ jsx("g", {
98
+ hasTargets && categoryTargets && /* @__PURE__ */ jsx("g", {
108
99
  transform: categoryTargetsTransform,
109
100
  children: /* @__PURE__ */ jsx(ChartCategoryTargets, {
110
101
  targets: categoryTargets,
111
102
  interaction,
112
- getAriaLabel: nameCategory,
113
- wrap: wrapCategoryNavigation
103
+ inertIndices
114
104
  })
115
105
  })
116
106
  ]
@@ -130,12 +120,12 @@ var ChartFrame = ({ containerRef, dimensions, fillWidth, fillHeight, interaction
130
120
  ]
131
121
  }), showLegend && /* @__PURE__ */ jsx(ChartLegend, {
132
122
  items: legendItems,
133
- inset: legendInset ?? margin.left,
123
+ inset: legendInset ?? (legendRight ? 16 : margin.left),
124
+ vertical: legendRight,
125
+ isLoading,
134
126
  hoveredKey: interaction.hoveredSeriesKey,
135
- pinnedKey: interaction.pinnedSeriesKey,
136
127
  onItemEnter: interaction.onSeriesEnter,
137
- onItemLeave: interaction.onSeriesLeave,
138
- onItemClick: interaction.onSeriesClick
128
+ onItemLeave: interaction.onSeriesLeave
139
129
  })]
140
130
  });
141
131
  };
@@ -5,20 +5,27 @@ 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
+ /**
11
+ * Render shimmer stubs instead of items.
12
+ *
13
+ * Keeps the legend's space reserved while the chart shows its skeleton, so
14
+ * data arriving doesn't shift the layout — which matters most beside a pie,
15
+ * where the legend's width decides the plot box's.
16
+ */
17
+ isLoading?: boolean;
10
18
  /**
11
19
  * Supplying these makes the legend interactive. Omit them and it renders as
12
20
  * inert text, with no button semantics for a screen reader to announce.
13
21
  */
14
22
  onItemEnter?: (key: string) => void;
15
23
  onItemLeave?: () => void;
16
- onItemClick?: (key: string) => void;
17
24
  }
18
25
  /**
19
26
  * Legend for the chart components.
20
27
  *
21
- * Hovering an entry isolates its series and clicking pins that isolation.
28
+ * Hovering or focusing an entry isolates its series; leaving releases it.
22
29
  *
23
30
  * Deliberately *not* a visibility toggle: hiding a series would have to rescale
24
31
  * the value axis to stay honest — otherwise the remaining bars sit mysteriously
@@ -26,5 +33,5 @@ interface ChartLegendProps {
26
33
  * Isolation only changes opacity, so the domain is untouched and the numbers
27
34
  * keep meaning what they did.
28
35
  */
29
- declare const ChartLegend: ({ items, inset, hoveredKey, pinnedKey, onItemEnter, onItemLeave, onItemClick, }: ChartLegendProps) => import("react").JSX.Element | null;
36
+ declare const ChartLegend: ({ items, inset, hoveredKey, vertical, isLoading, onItemEnter, onItemLeave, }: ChartLegendProps) => import("react").JSX.Element | null;
30
37
  export default ChartLegend;
@@ -1,13 +1,24 @@
1
1
  import Text, { TextSize } from "../text/Text.js";
2
+ import TextShimmer from "../text/TextShimmer.js";
2
3
  import Circle from "../shapes/Circle.js";
3
4
  import { ChartMarkState } from "./types.js";
4
5
  import { ChartLegendItemButton, ChartLegendItemStatic, ChartLegendRow } from "./ChartPrimitives.js";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/charts/ChartLegend.tsx
7
8
  /**
9
+ * Stub label widths in px, staggered so the placeholder reads as a legend
10
+ * rather than a repeated block. Three entries: enough to hold a legend's
11
+ * space without promising a series count.
12
+ */
13
+ var SKELETON_LABEL_WIDTHS = [
14
+ 40,
15
+ 56,
16
+ 48
17
+ ];
18
+ /**
8
19
  * Legend for the chart components.
9
20
  *
10
- * Hovering an entry isolates its series and clicking pins that isolation.
21
+ * Hovering or focusing an entry isolates its series; leaving releases it.
11
22
  *
12
23
  * Deliberately *not* a visibility toggle: hiding a series would have to rescale
13
24
  * the value axis to stay honest — otherwise the remaining bars sit mysteriously
@@ -15,12 +26,28 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
26
  * Isolation only changes opacity, so the domain is untouched and the numbers
16
27
  * keep meaning what they did.
17
28
  */
18
- var ChartLegend = ({ items, inset, hoveredKey = null, pinnedKey = null, onItemEnter, onItemLeave, onItemClick }) => {
29
+ var ChartLegend = ({ items, inset, hoveredKey = null, vertical = false, isLoading = false, onItemEnter, onItemLeave }) => {
30
+ if (isLoading) return /* @__PURE__ */ jsx(ChartLegendRow, {
31
+ $inset: inset,
32
+ $vertical: vertical,
33
+ "data-active": "false",
34
+ children: SKELETON_LABEL_WIDTHS.map((labelWidth, index) => /* @__PURE__ */ jsxs(ChartLegendItemStatic, {
35
+ "data-state": ChartMarkState.ACTIVE,
36
+ children: [/* @__PURE__ */ jsx(TextShimmer, {
37
+ height: 8,
38
+ width: 8
39
+ }), /* @__PURE__ */ jsx(TextShimmer, {
40
+ height: 10,
41
+ width: labelWidth
42
+ })]
43
+ }, index))
44
+ });
19
45
  if (items.length === 0) return null;
20
- const isInteractive = Boolean(onItemEnter ?? onItemClick);
21
- const activeKey = pinnedKey ?? hoveredKey;
46
+ const isInteractive = Boolean(onItemEnter);
47
+ const activeKey = hoveredKey;
22
48
  return /* @__PURE__ */ jsx(ChartLegendRow, {
23
49
  $inset: inset,
50
+ $vertical: vertical,
24
51
  "data-active": activeKey !== null ? "true" : "false",
25
52
  children: items.map((item) => {
26
53
  const state = activeKey === null || item.key === activeKey ? ChartMarkState.ACTIVE : ChartMarkState.MUTED;
@@ -38,12 +65,10 @@ var ChartLegend = ({ items, inset, hoveredKey = null, pinnedKey = null, onItemEn
38
65
  return /* @__PURE__ */ jsx(ChartLegendItemButton, {
39
66
  type: "button",
40
67
  "data-state": state,
41
- "aria-pressed": pinnedKey === item.key,
42
68
  onMouseEnter: () => onItemEnter?.(item.key),
43
69
  onMouseLeave: () => onItemLeave?.(),
44
70
  onFocus: () => onItemEnter?.(item.key),
45
71
  onBlur: () => onItemLeave?.(),
46
- onClick: () => onItemClick?.(item.key),
47
72
  children: content
48
73
  }, item.key);
49
74
  })
@@ -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
@@ -46,31 +56,18 @@ export declare const ChartAreaPath: import("@linaria/react").StyledComponent<imp
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
58
  /**
49
- * Wrapper for the category target layer.
59
+ * Wraps the marks while the chart is loading.
60
+ *
61
+ * Only motion lives here — the slow pulse shared with `TextShimmer` and
62
+ * `AvatarShimmer`, so a loading chart breathes in step with every other
63
+ * skeleton on the page. Colour arrives through the series resolver like any
64
+ * other mark colour, which is what lets bars and arcs ghost by `fill` and
65
+ * lines by `stroke` without per-shape CSS here.
50
66
  *
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.
67
+ * Under reduced motion the pulse pins at its midpoint rather than sitting at
68
+ * full strength, so the chart still reads as a placeholder and not as data.
56
69
  */
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
- }>, {}>;
70
+ export declare const ChartSkeletonGroup: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGGElement> & Record<never, unknown>>;
74
71
  /**
75
72
  * Per-category hit target, rendered above the marks.
76
73
  *
@@ -78,6 +75,9 @@ export declare const ChartCategoryTargetLayer: import("react").ComponentType<Pic
78
75
  * bar is as easy to hover as a two-hundred-pixel one, and crossing between
79
76
  * segments inside a category fires no events at all — so the tooltip holds
80
77
  * still instead of chasing each segment.
78
+ *
79
+ * No cursor and no focus treatment: the target only previews on hover, and a
80
+ * pointer cursor would promise a click that does nothing.
81
81
  */
82
82
  export declare const ChartHitTarget: import("@linaria/react").StyledComponent<import("react").SVGProps<SVGRectElement> & Record<never, unknown>>;
83
83
  /**
@@ -85,12 +85,10 @@ export declare const ChartHitTarget: import("@linaria/react").StyledComponent<im
85
85
  *
86
86
  * A pie has no category axis to band, so its targets are the arc shapes
87
87
  * themselves — drawn again, transparent, above the marks. That keeps hit
88
- * testing and focus identical across all three charts instead of the pie
89
- * hit-testing its visible arcs directly.
88
+ * testing identical across all three charts instead of the pie hit-testing its
89
+ * visible arcs directly.
90
90
  */
91
91
  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
92
  /**
95
93
  * Solid hairline rather than a dashed rule, and drawn at every tick including
96
94
  * zero — the line under the bars is what makes them read as resting on a
@@ -213,14 +211,16 @@ export declare const ChartEmptyOverlay: import("react").ComponentType<Pick<impor
213
211
  */
214
212
  export declare const ChartLegendRow: import("@linaria/react").StyledComponent<import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
215
213
  $inset: number;
214
+ $vertical?: boolean;
216
215
  }>;
217
216
  /**
218
217
  * One legend entry, when the legend is interactive.
219
218
  *
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.
219
+ * A real `<button>` rather than a styled div so it sits in the tab order:
220
+ * focusing an entry previews its series exactly as hovering does, which is the
221
+ * one keyboard affordance the charts keep the plot itself is `role="img"` and
222
+ * its marks are not in the accessibility tree. Activating it does nothing;
223
+ * isolation follows the pointer or the focus and releases with it.
224
224
  *
225
225
  * Shares the mark opacity ramp via `data-state`, so an entry recedes in step
226
226
  * with the series it names.