basalt-ui 1.14.0 → 1.16.0

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.
Files changed (176) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +38 -2
  4. package/agent/rules/basalt-charts.md +279 -108
  5. package/agent/rules/basalt-mantine.md +4 -2
  6. package/agent/skills/basalt-charts/SKILL.md +114 -64
  7. package/agent/skills/basalt-design/SKILL.md +4 -3
  8. package/agent/templates/CLAUDE-block.md.tpl +9 -6
  9. package/configs/oxlint-plugin.js +152 -0
  10. package/configs/oxlint-plugin.test.ts +107 -0
  11. package/configs/oxlint.json +2 -0
  12. package/dist/charts/cursor/resolve.d.ts +33 -0
  13. package/dist/charts/cursor/resolve.d.ts.map +1 -0
  14. package/dist/charts/cursor/resolve.js +61 -0
  15. package/dist/charts/cursor/resolve.test.d.ts +2 -0
  16. package/dist/charts/cursor/resolve.test.d.ts.map +1 -0
  17. package/dist/charts/cursor/scope.d.ts +17 -0
  18. package/dist/charts/cursor/scope.d.ts.map +1 -0
  19. package/dist/charts/cursor/scope.js +20 -0
  20. package/dist/charts/cursor/store.d.ts +31 -0
  21. package/dist/charts/cursor/store.d.ts.map +1 -0
  22. package/dist/charts/cursor/store.js +25 -0
  23. package/dist/charts/cursor/store.test.d.ts +2 -0
  24. package/dist/charts/cursor/store.test.d.ts.map +1 -0
  25. package/dist/charts/hooks/useChartCursor.d.ts +39 -0
  26. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -0
  27. package/dist/charts/hooks/useChartCursor.js +104 -0
  28. package/dist/charts/hooks/useChartSize.d.ts +8 -4
  29. package/dist/charts/hooks/useChartSize.d.ts.map +1 -1
  30. package/dist/charts/index.d.ts +16 -13
  31. package/dist/charts/index.d.ts.map +1 -1
  32. package/dist/charts/index.js +38 -19
  33. package/dist/charts/kinds/Bars.d.ts +45 -54
  34. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  35. package/dist/charts/kinds/Bars.js +235 -392
  36. package/dist/charts/kinds/Bars.test.d.ts +2 -0
  37. package/dist/charts/kinds/Bars.test.d.ts.map +1 -0
  38. package/dist/charts/kinds/Donut.d.ts +5 -4
  39. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  40. package/dist/charts/kinds/Donut.js +17 -14
  41. package/dist/charts/kinds/DualPanel.d.ts +13 -0
  42. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  43. package/dist/charts/kinds/DualPanel.js +185 -92
  44. package/dist/charts/kinds/DualPanel.test.d.ts +2 -0
  45. package/dist/charts/kinds/DualPanel.test.d.ts.map +1 -0
  46. package/dist/charts/kinds/Heatmap.d.ts +15 -11
  47. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  48. package/dist/charts/kinds/Heatmap.js +38 -22
  49. package/dist/charts/kinds/MultiLine.d.ts +27 -35
  50. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  51. package/dist/charts/kinds/MultiLine.js +122 -235
  52. package/dist/charts/kinds/MultiLine.test.d.ts +2 -0
  53. package/dist/charts/kinds/MultiLine.test.d.ts.map +1 -0
  54. package/dist/charts/kinds/StackedArea.d.ts +21 -10
  55. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  56. package/dist/charts/kinds/StackedArea.js +74 -132
  57. package/dist/charts/kinds/ZonedLine.d.ts +24 -40
  58. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  59. package/dist/charts/kinds/ZonedLine.js +104 -184
  60. package/dist/charts/kinds/ZonedLine.test.d.ts +2 -0
  61. package/dist/charts/kinds/ZonedLine.test.d.ts.map +1 -0
  62. package/dist/charts/layout/auto-margin.d.ts +60 -0
  63. package/dist/charts/layout/auto-margin.d.ts.map +1 -0
  64. package/dist/charts/layout/auto-margin.js +37 -0
  65. package/dist/charts/layout/auto-margin.test.d.ts +2 -0
  66. package/dist/charts/layout/auto-margin.test.d.ts.map +1 -0
  67. package/dist/charts/primitives/CartesianChart.d.ts +161 -0
  68. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
  69. package/dist/charts/primitives/CartesianChart.js +359 -0
  70. package/dist/charts/primitives/CartesianChart.test.d.ts +2 -0
  71. package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -0
  72. package/dist/charts/primitives/ChartFrame.d.ts +16 -5
  73. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  74. package/dist/charts/primitives/ChartFrame.js +17 -2
  75. package/dist/charts/primitives/ChartLegend.d.ts +15 -1
  76. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  77. package/dist/charts/primitives/ChartLegend.js +25 -3
  78. package/dist/charts/primitives/ChartLegend.test.d.ts +2 -0
  79. package/dist/charts/primitives/ChartLegend.test.d.ts.map +1 -0
  80. package/dist/charts/primitives/ChartTooltip.d.ts +16 -16
  81. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  82. package/dist/charts/primitives/ChartTooltip.js +45 -19
  83. package/dist/charts/primitives/HoverOverlay.d.ts +19 -4
  84. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  85. package/dist/charts/primitives/HoverOverlay.js +22 -1
  86. package/dist/charts/primitives/XZoneRects.d.ts +54 -0
  87. package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
  88. package/dist/charts/primitives/XZoneRects.js +65 -0
  89. package/dist/charts/primitives/XZoneRects.test.d.ts +2 -0
  90. package/dist/charts/primitives/XZoneRects.test.d.ts.map +1 -0
  91. package/dist/charts/series.d.ts +28 -3
  92. package/dist/charts/series.d.ts.map +1 -1
  93. package/dist/charts/series.js +5 -2
  94. package/dist/charts/series.test.d.ts +2 -0
  95. package/dist/charts/series.test.d.ts.map +1 -0
  96. package/dist/charts/utils/measure-text.d.ts +25 -0
  97. package/dist/charts/utils/measure-text.d.ts.map +1 -0
  98. package/dist/charts/utils/measure-text.js +64 -0
  99. package/dist/charts/utils/measure-text.test.d.ts +2 -0
  100. package/dist/charts/utils/measure-text.test.d.ts.map +1 -0
  101. package/dist/content/fence-block.d.ts.map +1 -1
  102. package/dist/surfaces.d.ts +1 -1
  103. package/dist/surfaces.d.ts.map +1 -1
  104. package/dist/surfaces.js +1 -1
  105. package/dist/tokens/chart-margin.test.d.ts +2 -0
  106. package/dist/tokens/chart-margin.test.d.ts.map +1 -0
  107. package/dist/tokens/index.d.ts +29 -0
  108. package/dist/tokens/index.d.ts.map +1 -1
  109. package/dist/tokens/index.js +10 -0
  110. package/dist/tokens/palette.d.ts +4 -0
  111. package/dist/tokens/palette.d.ts.map +1 -1
  112. package/dist/tokens/palette.js +3 -0
  113. package/llms.txt +3 -3
  114. package/package.json +1 -1
  115. package/src/charts/cursor/resolve.test.ts +61 -0
  116. package/src/charts/cursor/resolve.ts +102 -0
  117. package/src/charts/cursor/scope.tsx +32 -0
  118. package/src/charts/cursor/store.test.ts +46 -0
  119. package/src/charts/cursor/store.ts +53 -0
  120. package/src/charts/hooks/useChartCursor.ts +163 -0
  121. package/src/charts/hooks/useChartSize.ts +8 -4
  122. package/src/charts/index.ts +35 -14
  123. package/src/charts/kinds/Bars.test.tsx +98 -0
  124. package/src/charts/kinds/Bars.tsx +350 -467
  125. package/src/charts/kinds/Donut.tsx +31 -23
  126. package/src/charts/kinds/DualPanel.test.tsx +131 -0
  127. package/src/charts/kinds/DualPanel.tsx +301 -135
  128. package/src/charts/kinds/Heatmap.tsx +72 -47
  129. package/src/charts/kinds/MultiLine.test.tsx +37 -0
  130. package/src/charts/kinds/MultiLine.tsx +171 -290
  131. package/src/charts/kinds/StackedArea.tsx +116 -175
  132. package/src/charts/kinds/ZonedLine.test.tsx +37 -0
  133. package/src/charts/kinds/ZonedLine.tsx +149 -255
  134. package/src/charts/layout/auto-margin.test.ts +42 -0
  135. package/src/charts/layout/auto-margin.ts +110 -0
  136. package/src/charts/primitives/CartesianChart.test.tsx +160 -0
  137. package/src/charts/primitives/CartesianChart.tsx +598 -0
  138. package/src/charts/primitives/ChartFrame.test.tsx +33 -0
  139. package/src/charts/primitives/ChartFrame.tsx +40 -4
  140. package/src/charts/primitives/ChartLegend.test.tsx +78 -0
  141. package/src/charts/primitives/ChartLegend.tsx +52 -2
  142. package/src/charts/primitives/ChartTooltip.tsx +67 -37
  143. package/src/charts/primitives/HoverOverlay.tsx +39 -3
  144. package/src/charts/primitives/XZoneRects.test.tsx +123 -0
  145. package/src/charts/primitives/XZoneRects.tsx +132 -0
  146. package/src/charts/series.test.ts +112 -0
  147. package/src/charts/series.ts +33 -5
  148. package/src/charts/utils/measure-text.test.ts +36 -0
  149. package/src/charts/utils/measure-text.ts +109 -0
  150. package/src/content/fence-block.tsx +2 -3
  151. package/src/surfaces.ts +2 -1
  152. package/src/theme/spacing.test.ts +2 -0
  153. package/src/tokens/chart-margin.test.ts +39 -0
  154. package/src/tokens/density.test.ts +1 -0
  155. package/src/tokens/index.ts +36 -0
  156. package/src/tokens/palette.ts +3 -0
  157. package/dist/charts/hooks/useChartTooltip.d.ts +0 -13
  158. package/dist/charts/hooks/useChartTooltip.d.ts.map +0 -1
  159. package/dist/charts/hooks/useChartTooltip.js +0 -26
  160. package/dist/charts/hooks/useHoverSync.d.ts +0 -46
  161. package/dist/charts/hooks/useHoverSync.d.ts.map +0 -1
  162. package/dist/charts/hooks/useHoverSync.js +0 -75
  163. package/dist/charts/hover-context.d.ts +0 -21
  164. package/dist/charts/hover-context.d.ts.map +0 -1
  165. package/dist/charts/hover-context.js +0 -12
  166. package/dist/charts/hover-sync.d.ts +0 -14
  167. package/dist/charts/hover-sync.d.ts.map +0 -1
  168. package/dist/charts/hover-sync.js +0 -17
  169. package/dist/charts/primitives/ResponsiveChart.d.ts +0 -45
  170. package/dist/charts/primitives/ResponsiveChart.d.ts.map +0 -1
  171. package/dist/charts/primitives/ResponsiveChart.js +0 -24
  172. package/src/charts/hooks/useChartTooltip.ts +0 -31
  173. package/src/charts/hooks/useHoverSync.ts +0 -152
  174. package/src/charts/hover-context.ts +0 -28
  175. package/src/charts/hover-sync.tsx +0 -37
  176. package/src/charts/primitives/ResponsiveChart.tsx +0 -64
@@ -1,15 +1,13 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Group } from "@visx/group";
3
- import { memo, useMemo } from "react";
3
+ import { memo, useMemo, useState } from "react";
4
4
  import {
5
- ChartTooltip,
5
+ ChartTooltipFloat,
6
6
  TooltipBody,
7
7
  TooltipHeader,
8
- TooltipRow,
9
- useTooltipStyles
8
+ TooltipRow
10
9
  } from "../primitives/ChartTooltip.js";
11
- import { ChartPending } from "../primitives/ChartPending.js";
12
- import { useChartTooltip } from "../hooks/useChartTooltip.js";
10
+ import { ChartFrame } from "../primitives/ChartFrame.js";
13
11
  import { VX, alpha } from "../../tokens/index.js";
14
12
  function firstSeen(data, get) {
15
13
  const seen = /* @__PURE__ */ new Set();
@@ -31,10 +29,26 @@ const PAD_TOP = 8;
31
29
  const LEGEND_H = 8;
32
30
  const LEGEND_LABEL_H = 16;
33
31
  function HeatmapInner(props) {
32
+ const { chartId, height, aspectRatio, fill, ariaLabel, isPending } = props;
33
+ return /* @__PURE__ */ jsx(
34
+ ChartFrame,
35
+ {
36
+ series: [],
37
+ chartId,
38
+ legend: false,
39
+ ...height !== void 0 && { height },
40
+ ...aspectRatio !== void 0 && { aspectRatio },
41
+ ...fill !== void 0 && { fill },
42
+ ...ariaLabel !== void 0 && { ariaLabel },
43
+ ...isPending !== void 0 && { isPending },
44
+ children: (plot) => /* @__PURE__ */ jsx(HeatmapPlot, { ...props, plot })
45
+ }
46
+ );
47
+ }
48
+ function HeatmapPlot(props) {
34
49
  const {
35
50
  data,
36
- width,
37
- height,
51
+ plot,
38
52
  chartId,
39
53
  getRow,
40
54
  getCol,
@@ -49,11 +63,10 @@ function HeatmapInner(props) {
49
63
  rowLabel = (r) => r,
50
64
  colLabel = (c) => c,
51
65
  renderTooltip,
52
- legend,
53
- isPending
66
+ legend
54
67
  } = props;
55
- const tooltipStyles = useTooltipStyles();
56
- const { tip, show, hide, tooltipRef } = useChartTooltip();
68
+ const { width, height } = plot;
69
+ const [tip, setTip] = useState(null);
57
70
  const rows = useMemo(() => rowsProp ?? firstSeen(data, getRow), [rowsProp, data, getRow]);
58
71
  const cols = useMemo(() => colsProp ?? firstSeen(data, getCol), [colsProp, data, getCol]);
59
72
  const { lookup, max } = useMemo(() => {
@@ -66,20 +79,23 @@ function HeatmapInner(props) {
66
79
  }
67
80
  return { lookup: map, max: m };
68
81
  }, [data, getRow, getCol, getValue]);
69
- if (isPending) return /* @__PURE__ */ jsx(ChartPending, { width, height });
70
82
  const legendH = legend ? LEGEND_H + LEGEND_LABEL_H : 0;
71
83
  const gridW = Math.max(0, width - PAD_LEFT);
72
84
  const gridH = Math.max(0, height - PAD_TOP - PAD_BOTTOM - legendH);
73
85
  const cellW = cols.length > 0 ? gridW / cols.length : 0;
74
86
  const cellH = rows.length > 0 ? gridH / rows.length : 0;
75
87
  const legendGradientId = `${chartId}-heat-legend`;
88
+ const show = (row, col, value, event) => {
89
+ setTip({ row, col, value, anchor: { x: event.clientX, y: event.clientY } });
90
+ };
91
+ const hide = () => setTip(null);
76
92
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
77
93
  /* @__PURE__ */ jsxs("svg", { width, height, children: [
78
94
  /* @__PURE__ */ jsx(Group, { left: PAD_LEFT, top: PAD_TOP, children: rows.flatMap(
79
95
  (row, ri) => cols.map((col, ci) => {
80
96
  const value = lookup.get(cellKey(row, col));
81
97
  const has = value !== void 0;
82
- const fill = has ? alpha(color, intensity(value, max)) : alpha(VX.neutral, 0.04);
98
+ const cellFill = has ? alpha(color, intensity(value, max)) : alpha(VX.neutral, 0.04);
83
99
  return /* @__PURE__ */ jsx(
84
100
  "rect",
85
101
  {
@@ -88,9 +104,9 @@ function HeatmapInner(props) {
88
104
  width: Math.max(0, cellW - cellGap),
89
105
  height: Math.max(0, cellH - cellGap),
90
106
  rx: cellRadius,
91
- fill,
107
+ fill: cellFill,
92
108
  style: { cursor: has ? "pointer" : "default" },
93
- onPointerMove: (e) => has && show({ row, col, value }, e),
109
+ onPointerMove: (e) => has && show(row, col, value, e),
94
110
  onPointerLeave: hide,
95
111
  onPointerCancel: hide
96
112
  },
@@ -156,8 +172,8 @@ function HeatmapInner(props) {
156
172
  )
157
173
  ] })
158
174
  ] }),
159
- /* @__PURE__ */ jsx(ChartTooltip, { tip, tooltipRef, styles: tooltipStyles, children: tip !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
160
- /* @__PURE__ */ jsx(TooltipHeader, { date: rowLabel(tip.data.row), label: colLabel(tip.data.col) }),
175
+ tip !== null && /* @__PURE__ */ jsxs(ChartTooltipFloat, { anchor: tip.anchor, children: [
176
+ /* @__PURE__ */ jsx(TooltipHeader, { date: rowLabel(tip.row), label: colLabel(tip.col) }),
161
177
  /* @__PURE__ */ jsxs(TooltipBody, { children: [
162
178
  /* @__PURE__ */ jsx(
163
179
  TooltipRow,
@@ -165,12 +181,12 @@ function HeatmapInner(props) {
165
181
  color: alpha(color, 0.9),
166
182
  shape: "bar",
167
183
  label: "Value",
168
- value: formatValue(tip.data.value)
184
+ value: formatValue(tip.value)
169
185
  }
170
186
  ),
171
- renderTooltip?.(tip.data)
187
+ renderTooltip?.(tip)
172
188
  ] })
173
- ] }) })
189
+ ] })
174
190
  ] });
175
191
  }
176
192
  const Heatmap = memo(HeatmapInner);
@@ -1,66 +1,58 @@
1
- import type { ReactNode } from 'react';
1
+ import type { CartesianTooltipConfig, AxisConfig } from '../primitives/CartesianChart.js';
2
+ import type { XZoneSpec } from '../primitives/XZoneRects.js';
2
3
  import type { ZoneSpec } from '../primitives/ZoneRects.js';
3
4
  import type { ChartLegendConfig, ChartSeries } from '../series.js';
4
5
  export type MultiLineProps<T> = {
5
6
  data: T[];
6
- /** Fixed height in pixels, forwarded to the internal `ChartFrame`. Default 240. */
7
+ /** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
7
8
  height?: number;
8
9
  chartId: string;
9
10
  /** Extracts the x-axis category (date string) from a data point. */
10
11
  getX: (d: T) => string;
11
- /** 1+ line series sharing one y-axis — the single source of truth for color, dash, legend, and
12
- * tooltip rows. */
12
+ /** 1+ line series sharing the y-axis(es) — the single source of truth for color, dash, legend,
13
+ * and tooltip rows. A series opts into the right axis with `axis: 'right'`. */
13
14
  series: ChartSeries<T>[];
14
- /** Fixed y-domain (e.g. [-2.5, 2.5]) or 'auto' to compute from all series. */
15
- yDomain: [number, number] | 'auto';
16
- /** When 'auto': upper bound is at least this value. */
17
- yAutoMaxFloor?: number;
18
- /** When 'auto': lower bound is at most this value. Default 0. Pass `Infinity` to disable the
19
- * forced zero baseline — the lower bound then pads DOWN from the raw data minimum instead. */
20
- yAutoMinCeil?: number;
21
- /** When 'auto': padding multiplier applied away from the domain. Default 1.1. The lower bound
22
- * always pads DOWN from `min(dataMin, yAutoMinCeil)` (never up, which would clip data) — see
23
- * {@link padAutoLower}. */
24
- yAutoPad?: number;
25
- /** Horizontal value-range overlays (target zones), rendered behind the lines on the left scale. */
15
+ /** Left y-axis. Collapses the old `yDomain` / `yAutoMaxFloor` / `yAutoMinCeil` / `yAutoPad` /
16
+ * `numTicksY` / `formatYTick` prop soup into one object. */
17
+ y?: AxisConfig<T>;
18
+ /** Right y-axis. Passing it is what makes the chart dual-axis — the margin widens by
19
+ * measurement, and `axis: 'right'` series read against it. */
20
+ y2?: AxisConfig<T>;
21
+ /** Horizontal value-range overlays (target zones), rendered behind the lines. */
26
22
  zones?: ZoneSpec[];
23
+ /** Vertical x-range overlays (time windows), rendered behind the lines. Bounds are `getX`
24
+ * domain keys. */
25
+ xZones?: XZoneSpec[];
27
26
  /** Horizontal reference lines. Solid by default; set dashed: true for a dashed line. */
28
27
  refLines?: {
29
28
  value: number;
30
29
  color: string;
31
30
  dashed?: boolean;
32
31
  }[];
33
- numTicksX?: number;
34
- numTicksY?: number;
35
- /** Tooltip + (per-series) value formatter. */
36
- formatValue: (v: number) => string;
37
- /** Y-axis tick label formatter. Falls back to the scale's default. */
38
- formatYTick?: (v: number) => string;
39
- /** Label shown at the right of the tooltip header (e.g. status badge). */
40
- tooltipLabel?: (d: T) => {
41
- text: string;
42
- color: string;
43
- } | null;
44
- /** Extra tooltip rows rendered after the per-series rows. */
45
- renderExtraTooltipRows?: (d: T) => ReactNode;
32
+ /** Exact number of x ticks. Default: as many as fit. */
33
+ xTicks?: number;
34
+ /** Tooltip config — `label` for a right-aligned header badge, `extraRows` for rows appended
35
+ * after the derived per-series rows. `false` disables the tooltip entirely. */
36
+ tooltip?: CartesianTooltipConfig<T> | false;
46
37
  /** Marker glyph for per-point markers. Default 'circle'. */
47
38
  markerShape?: 'circle' | 'star';
48
- /** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
39
+ /** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
49
40
  * Default `{ placement: 'bottom' }`. */
50
41
  legend?: ChartLegendConfig | false;
51
- /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="img"`). */
42
+ /** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="img"`). */
52
43
  ariaLabel?: string;
53
- /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
44
+ /** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
54
45
  isPending?: boolean;
55
46
  };
56
47
  /**
57
- * N series sharing one y-axis, with optional zones/reference lines, per-point markers
48
+ * N series sharing one or two y-axes, with optional zones/reference lines, per-point markers
58
49
  * (PR stars / status dots), a dashed companion line per series, legend-hover dimming, and a
59
50
  * shared-cursor tooltip. Generalizes the multi-line argo charts (e1RM trend, relative
60
51
  * progression, training load, fitness trends).
61
52
  *
62
- * Composes `ChartFrame` for measuring + the derived legend — `series` is the single array that
63
- * drives the plotted lines, the legend, and the tooltip rows.
53
+ * Composes `CartesianChart` for measuring, scales, grid, axes, cursor, and tooltip — this kind
54
+ * draws ONLY the marks (lines + per-point markers). `series` is the single array that drives the
55
+ * plotted lines, the legend, and the tooltip rows.
64
56
  *
65
57
  * X-axis is built from the full `data` array so the calendar is preserved even when a series
66
58
  * has nulls; each series line skips null points (creating visual gaps).
@@ -1 +1 @@
1
- {"version":3,"file":"MultiLine.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/MultiLine.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAatC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAGvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAK/D,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACtB;uBACmB;IACnB,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACxB,8EAA8E;IAC9E,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAClC,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;kGAC8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;+BAE2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mGAAmG;IACnG,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;IAClB,wFAAwF;IACxF,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8CAA8C;IAC9C,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IAClC,sEAAsE;IACtE,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IACnC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/D,6DAA6D;IAC7D,sBAAsB,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,SAAS,CAAA;IAC5C,4DAA4D;IAC5D,WAAW,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;IAC/B;4CACwC;IACxC,MAAM,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;IAClC,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAgBD;;;;;;;;;;;GAWG;AACH,iBAAS,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,+BAiClD;AAkPD;;;GAGG;AACH,eAAO,MAAM,SAAS,EAA2B,OAAO,cAAc,CAAA"}
1
+ {"version":3,"file":"MultiLine.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/MultiLine.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,UAAU,EAAe,MAAM,8BAA8B,CAAA;AAEnG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAG/D,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACtB;mFAC+E;IAC/E,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACxB;gEAC4D;IAC5D,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IACjB;kEAC8D;IAC9D,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IAClB,iFAAiF;IACjF,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;IAClB;sBACkB;IAClB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;IACpB,wFAAwF;IACxF,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAC/D,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mFAC+E;IAC/E,OAAO,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;IAC3C,4DAA4D;IAC5D,WAAW,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;IAC/B;4CACwC;IACxC,MAAM,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;IAClC,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AA4BD;;;;;;;;;;;;GAYG;AACH,iBAAS,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,+BAgDlD;AAqHD;;;GAGG;AACH,eAAO,MAAM,SAAS,EAA2B,OAAO,cAAc,CAAA"}
@@ -1,27 +1,10 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { curveMonotoneX } from "@visx/curve";
3
- import { GridRows } from "@visx/grid";
4
- import { Group } from "@visx/group";
5
- import { scaleLinear, scalePoint } from "@visx/scale";
6
3
  import { LinePath } from "@visx/shape";
7
- import { memo, useMemo, useState } from "react";
8
- import { AxisBottomDate, AxisLeftNumeric } from "../primitives/Axes.js";
9
- import {
10
- ChartTooltip,
11
- TooltipBody,
12
- TooltipHeader,
13
- TooltipRow,
14
- useTooltipStyles
15
- } from "../primitives/ChartTooltip.js";
16
- import { ChartFrame, resolveLegend } from "../primitives/ChartFrame.js";
17
- import { Crosshair, SeriesDot } from "../primitives/Crosshair.js";
18
- import { HoverOverlay } from "../primitives/HoverOverlay.js";
19
- import { ZoneRects } from "../primitives/ZoneRects.js";
20
- import { useHoverSync } from "../hooks/useHoverSync.js";
21
- import { deriveTooltipRows, LINE_OVERLAY_STROKE_WIDTH } from "../series.js";
4
+ import { memo, useCallback, useMemo } from "react";
5
+ import { CartesianChart } from "../primitives/CartesianChart.js";
6
+ import { LINE_OVERLAY_STROKE_WIDTH } from "../series.js";
22
7
  import { VX } from "../../tokens/index.js";
23
- import { padAutoLower } from "../utils/domain.js";
24
- import { smartTicks, smartTicksEvery } from "../utils/ticks.js";
25
8
  const STAR_R = 6;
26
9
  function starPath(cx, cy, r) {
27
10
  const inner = r * 0.4;
@@ -33,241 +16,145 @@ function starPath(cx, cy, r) {
33
16
  }
34
17
  return `M${pts.join("L")}Z`;
35
18
  }
19
+ function seriesPoints(series, data) {
20
+ const pts = [];
21
+ for (const d of data) {
22
+ const v = series.getValue(d);
23
+ if (v !== null && v !== void 0 && !Number.isNaN(v)) pts.push({ __d: d, __y: v });
24
+ }
25
+ return pts;
26
+ }
36
27
  function MultiLineInner(props) {
37
- const { series, chartId, height, legend, ariaLabel, isPending } = props;
38
- const [highlightedKey, setHighlightedKey] = useState(null);
28
+ const {
29
+ data,
30
+ chartId,
31
+ getX,
32
+ series,
33
+ y,
34
+ y2,
35
+ zones,
36
+ xZones,
37
+ refLines,
38
+ xTicks,
39
+ tooltip,
40
+ markerShape = "circle",
41
+ height,
42
+ legend,
43
+ ariaLabel,
44
+ isPending
45
+ } = props;
39
46
  const styledSeries = useMemo(
40
47
  () => series.map((s) => ({ ...s, strokeWidth: s.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH })),
41
48
  [series]
42
49
  );
43
50
  return /* @__PURE__ */ jsx(
44
- ChartFrame,
51
+ CartesianChart,
45
52
  {
46
- series: styledSeries,
53
+ data,
47
54
  chartId,
55
+ getX,
56
+ series: styledSeries,
57
+ ...y !== void 0 && { y },
58
+ ...y2 !== void 0 && { y2 },
59
+ ...zones !== void 0 && { zones },
60
+ ...xZones !== void 0 && { xZones },
61
+ ...refLines !== void 0 && { refLines },
62
+ ...xTicks !== void 0 && { xTicks },
63
+ ...tooltip !== void 0 && { tooltip },
48
64
  ...height !== void 0 && { height },
65
+ ...legend !== void 0 && { legend },
49
66
  ...ariaLabel !== void 0 && { ariaLabel },
50
67
  ...isPending !== void 0 && { isPending },
51
- legend: resolveLegend(legend, {
52
- highlighted: highlightedKey,
53
- onHighlight: setHighlightedKey
54
- }),
55
- children: (plot) => /* @__PURE__ */ jsx(
56
- MultiLinePlot,
57
- {
58
- ...props,
59
- series: styledSeries,
60
- plot,
61
- highlightedKey
62
- }
63
- )
68
+ children: (ctx) => /* @__PURE__ */ jsx(MultiLineMarks, { getX, markerShape, ctx })
64
69
  }
65
70
  );
66
71
  }
67
- function MultiLinePlot(props) {
68
- const {
69
- data,
70
- chartId,
71
- getX,
72
- series,
73
- yDomain,
74
- yAutoPad = 1.1,
75
- yAutoMaxFloor,
76
- yAutoMinCeil = 0,
77
- zones = [],
78
- refLines = [],
79
- numTicksX,
80
- numTicksY = 5,
81
- formatValue,
82
- formatYTick,
83
- tooltipLabel,
84
- renderExtraTooltipRows,
85
- markerShape = "circle",
86
- plot,
87
- highlightedKey
88
- } = props;
89
- const dimOpacity = (s) => highlightedKey === null || s.key === highlightedKey || s.parent === highlightedKey ? 1 : 0.25;
90
- const MARGIN = VX.margin;
91
- const xMax = plot.width - MARGIN.left - MARGIN.right;
92
- const yMax = plot.height - MARGIN.top - MARGIN.bottom;
93
- const xScale = useMemo(
94
- () => scalePoint({
95
- // Full calendar — axis does not compress across nulls.
96
- domain: data.map(getX),
97
- range: [0, xMax],
98
- padding: 0.3
99
- }),
100
- [data, xMax, getX]
101
- );
102
- const yScale = useMemo(() => {
103
- if (yDomain === "auto") {
104
- let dataMax = -Infinity;
105
- let dataMin = Infinity;
106
- for (const d of data) {
107
- for (const s of series) {
108
- const v = s.getValue(d);
109
- if (v === null || v === void 0 || Number.isNaN(v)) continue;
110
- if (v > dataMax) dataMax = v;
111
- if (v < dataMin) dataMin = v;
112
- }
113
- }
114
- const safeMax = Number.isFinite(dataMax) ? dataMax : 0;
115
- const safeMin = Number.isFinite(dataMin) ? dataMin : 0;
116
- const upper = Math.max(safeMax, yAutoMaxFloor ?? safeMax) * yAutoPad;
117
- const lower = padAutoLower(Math.min(safeMin, yAutoMinCeil), yAutoPad);
118
- return scaleLinear({ domain: [lower, upper], range: [yMax, 0], nice: true });
119
- }
120
- return scaleLinear({ domain: yDomain, range: [yMax, 0] });
121
- }, [data, series, yDomain, yMax, yAutoPad, yAutoMaxFloor, yAutoMinCeil]);
122
- const tooltipStyles = useTooltipStyles();
123
- const { tip, tooltipRef, syncedPoint, isDirectHover, handleMouse, handleLeave } = useHoverSync(
124
- {
125
- data,
126
- chartId,
127
- getKey: getX,
128
- xScale,
129
- marginLeft: MARGIN.left
130
- }
72
+ function MultiLineMarks({
73
+ getX,
74
+ markerShape,
75
+ ctx
76
+ }) {
77
+ const { data, visible, xScale, yScale, y2Scale, highlighted } = ctx;
78
+ const pointsBySeries = useMemo(() => {
79
+ const m = /* @__PURE__ */ new Map();
80
+ for (const s of visible) m.set(s.key, seriesPoints(s, data));
81
+ return m;
82
+ }, [visible, data]);
83
+ const dimOpacity = useCallback(
84
+ (s) => highlighted === null || s.key === highlighted || s.parent === highlighted ? 1 : 0.25,
85
+ [highlighted]
131
86
  );
132
- const tickValues = useMemo(
133
- () => numTicksX ? smartTicksEvery(data.map(getX), numTicksX) : smartTicks(data.map(getX), xMax),
134
- [data, xMax, getX, numTicksX]
87
+ const scaleFor = useCallback(
88
+ (s) => s.axis === "right" && y2Scale !== null ? y2Scale : yScale,
89
+ [y2Scale, yScale]
135
90
  );
136
- const seriesPts = useMemo(() => {
137
- const out = /* @__PURE__ */ new Map();
138
- for (const s of series) {
139
- const pts = [];
140
- for (const d of data) {
141
- const v = s.getValue(d);
142
- if (v !== null && v !== void 0 && !Number.isNaN(v)) pts.push({ __d: d, __y: v });
91
+ const markerNodes = useMemo(
92
+ () => visible.flatMap((s) => {
93
+ const getMarker = s.getMarker;
94
+ if (!getMarker) return [];
95
+ const scale = scaleFor(s);
96
+ const op = dimOpacity(s);
97
+ const markers = [];
98
+ for (const p of pointsBySeries.get(s.key) ?? []) {
99
+ const m = getMarker(p.__d);
100
+ if (m === null) continue;
101
+ const cx = xScale(getX(p.__d)) ?? 0;
102
+ const cy = scale(p.__y);
103
+ const color = m.color ?? s.color;
104
+ const r = m.r ?? (markerShape === "star" ? STAR_R : VX.dotR);
105
+ markers.push(
106
+ markerShape === "star" ? /* @__PURE__ */ jsx(
107
+ "path",
108
+ {
109
+ d: starPath(cx, cy, r),
110
+ fill: color,
111
+ stroke: VX.dotStroke,
112
+ strokeWidth: 1.5,
113
+ fillOpacity: op,
114
+ strokeOpacity: op
115
+ },
116
+ `mk-${s.key}-${getX(p.__d)}`
117
+ ) : /* @__PURE__ */ jsx(
118
+ "circle",
119
+ {
120
+ cx,
121
+ cy,
122
+ r,
123
+ fill: color,
124
+ stroke: VX.dotStroke,
125
+ strokeWidth: 2,
126
+ fillOpacity: op,
127
+ strokeOpacity: op
128
+ },
129
+ `mk-${s.key}-${getX(p.__d)}`
130
+ )
131
+ );
143
132
  }
144
- out.set(s.key, pts);
145
- }
146
- return out;
147
- }, [data, series]);
148
- const tooltipLbl = tip ? tooltipLabel?.(tip.data) ?? null : null;
133
+ return markers;
134
+ }),
135
+ [visible, pointsBySeries, xScale, getX, scaleFor, dimOpacity, markerShape]
136
+ );
149
137
  return /* @__PURE__ */ jsxs(Fragment, { children: [
150
- /* @__PURE__ */ jsx("svg", { width: plot.width, height: plot.height, children: /* @__PURE__ */ jsxs(Group, { left: MARGIN.left, top: MARGIN.top, children: [
151
- /* @__PURE__ */ jsx(GridRows, { scale: yScale, width: xMax, stroke: VX.grid, numTicks: numTicksY }),
152
- /* @__PURE__ */ jsx(ZoneRects, { zones, width: xMax, leftScale: yScale }),
153
- refLines.map((r, i) => /* @__PURE__ */ jsx(
154
- "line",
138
+ visible.map((s) => {
139
+ const valid = pointsBySeries.get(s.key) ?? [];
140
+ if (valid.length === 0) return null;
141
+ const scale = scaleFor(s);
142
+ return /* @__PURE__ */ jsx(
143
+ LinePath,
155
144
  {
156
- x1: 0,
157
- x2: xMax,
158
- y1: yScale(r.value),
159
- y2: yScale(r.value),
160
- stroke: r.color,
161
- strokeDasharray: r.dashed ? VX.dashArray : void 0
145
+ data: valid,
146
+ x: (p) => xScale(getX(p.__d)) ?? 0,
147
+ y: (p) => scale(p.__y),
148
+ stroke: s.color,
149
+ strokeWidth: s.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH,
150
+ strokeDasharray: s.dash === "dashed" ? VX.dashArray : void 0,
151
+ strokeOpacity: dimOpacity(s) * (s.strokeOpacity ?? 1),
152
+ curve: curveMonotoneX
162
153
  },
163
- `ref-${i}`
164
- )),
165
- series.map((s) => {
166
- const valid = seriesPts.get(s.key) ?? [];
167
- if (valid.length === 0) return null;
168
- return /* @__PURE__ */ jsx(
169
- LinePath,
170
- {
171
- data: valid,
172
- x: (p) => xScale(getX(p.__d)) ?? 0,
173
- y: (p) => yScale(p.__y),
174
- stroke: s.color,
175
- strokeWidth: s.strokeWidth ?? VX.lineWidth,
176
- strokeDasharray: s.dash === "dashed" ? VX.dashArray : void 0,
177
- strokeOpacity: dimOpacity(s),
178
- curve: curveMonotoneX
179
- },
180
- `line-${s.key}`
181
- );
182
- }),
183
- series.flatMap((s) => {
184
- const getMarker = s.getMarker;
185
- if (!getMarker) return [];
186
- const op = dimOpacity(s);
187
- const markers = [];
188
- for (const p of seriesPts.get(s.key) ?? []) {
189
- const m = getMarker(p.__d);
190
- if (m === null) continue;
191
- const cx = xScale(getX(p.__d)) ?? 0;
192
- const cy = yScale(p.__y);
193
- const color = m.color ?? s.color;
194
- const r = m.r ?? (markerShape === "star" ? STAR_R : VX.dotR);
195
- markers.push(
196
- markerShape === "star" ? /* @__PURE__ */ jsx(
197
- "path",
198
- {
199
- d: starPath(cx, cy, r),
200
- fill: color,
201
- stroke: VX.dotStroke,
202
- strokeWidth: 1.5,
203
- fillOpacity: op,
204
- strokeOpacity: op
205
- },
206
- `mk-${s.key}-${getX(p.__d)}`
207
- ) : /* @__PURE__ */ jsx(
208
- "circle",
209
- {
210
- cx,
211
- cy,
212
- r,
213
- fill: color,
214
- stroke: VX.dotStroke,
215
- strokeWidth: 2,
216
- fillOpacity: op,
217
- strokeOpacity: op
218
- },
219
- `mk-${s.key}-${getX(p.__d)}`
220
- )
221
- );
222
- }
223
- return markers;
224
- }),
225
- syncedPoint && (() => {
226
- const sx = xScale(getX(syncedPoint)) ?? 0;
227
- return /* @__PURE__ */ jsxs(Fragment, { children: [
228
- /* @__PURE__ */ jsx(Crosshair, { x: sx, top: 0, bottom: yMax }),
229
- series.map((s) => {
230
- const v = s.getValue(syncedPoint);
231
- if (v === null || v === void 0 || Number.isNaN(v)) return null;
232
- return /* @__PURE__ */ jsx(SeriesDot, { cx: sx, cy: yScale(v), color: s.color }, `dot-${s.key}`);
233
- })
234
- ] });
235
- })(),
236
- /* @__PURE__ */ jsx(
237
- AxisLeftNumeric,
238
- {
239
- scale: yScale,
240
- numTicks: numTicksY,
241
- ...formatYTick !== void 0 && { tickFormat: formatYTick }
242
- }
243
- ),
244
- /* @__PURE__ */ jsx(AxisBottomDate, { top: yMax, scale: xScale, tickValues }),
245
- /* @__PURE__ */ jsx(HoverOverlay, { width: xMax, height: yMax, onMove: handleMouse, onLeave: handleLeave })
246
- ] }) }),
247
- /* @__PURE__ */ jsx(ChartTooltip, { tip: isDirectHover ? tip : null, tooltipRef, styles: tooltipStyles, children: tip && isDirectHover && /* @__PURE__ */ jsxs(Fragment, { children: [
248
- /* @__PURE__ */ jsx(
249
- TooltipHeader,
250
- {
251
- date: getX(tip.data),
252
- ...tooltipLbl !== null && { label: tooltipLbl.text, labelColor: tooltipLbl.color }
253
- }
254
- ),
255
- /* @__PURE__ */ jsxs(TooltipBody, { children: [
256
- deriveTooltipRows(series, tip.data, formatValue).map((row) => /* @__PURE__ */ jsx(
257
- TooltipRow,
258
- {
259
- color: row.color,
260
- label: row.label,
261
- value: row.value,
262
- shape: row.shape,
263
- dashed: row.dashed,
264
- ...row.strokeWidth !== void 0 && { strokeWidth: row.strokeWidth }
265
- },
266
- row.key
267
- )),
268
- renderExtraTooltipRows?.(tip.data)
269
- ] })
270
- ] }) })
154
+ `line-${s.key}`
155
+ );
156
+ }),
157
+ markerNodes
271
158
  ] });
272
159
  }
273
160
  const MultiLine = memo(MultiLineInner);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MultiLine.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiLine.test.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/MultiLine.test.tsx"],"names":[],"mappings":""}