basalt-ui 1.14.0 → 1.15.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 (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +38 -2
  4. package/agent/rules/basalt-charts.md +260 -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/configs/oxlint-plugin.js +152 -0
  9. package/configs/oxlint-plugin.test.ts +107 -0
  10. package/configs/oxlint.json +2 -0
  11. package/dist/charts/cursor/resolve.d.ts +33 -0
  12. package/dist/charts/cursor/resolve.d.ts.map +1 -0
  13. package/dist/charts/cursor/resolve.js +61 -0
  14. package/dist/charts/cursor/resolve.test.d.ts +2 -0
  15. package/dist/charts/cursor/resolve.test.d.ts.map +1 -0
  16. package/dist/charts/cursor/scope.d.ts +17 -0
  17. package/dist/charts/cursor/scope.d.ts.map +1 -0
  18. package/dist/charts/cursor/scope.js +20 -0
  19. package/dist/charts/cursor/store.d.ts +31 -0
  20. package/dist/charts/cursor/store.d.ts.map +1 -0
  21. package/dist/charts/cursor/store.js +25 -0
  22. package/dist/charts/cursor/store.test.d.ts +2 -0
  23. package/dist/charts/cursor/store.test.d.ts.map +1 -0
  24. package/dist/charts/hooks/useChartCursor.d.ts +39 -0
  25. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -0
  26. package/dist/charts/hooks/useChartCursor.js +104 -0
  27. package/dist/charts/hooks/useChartSize.d.ts +8 -4
  28. package/dist/charts/hooks/useChartSize.d.ts.map +1 -1
  29. package/dist/charts/index.d.ts +16 -13
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/index.js +38 -19
  32. package/dist/charts/kinds/Bars.d.ts +25 -47
  33. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  34. package/dist/charts/kinds/Bars.js +230 -390
  35. package/dist/charts/kinds/Donut.d.ts +5 -4
  36. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  37. package/dist/charts/kinds/Donut.js +17 -14
  38. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  39. package/dist/charts/kinds/DualPanel.js +136 -90
  40. package/dist/charts/kinds/Heatmap.d.ts +15 -11
  41. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  42. package/dist/charts/kinds/Heatmap.js +38 -22
  43. package/dist/charts/kinds/MultiLine.d.ts +27 -35
  44. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  45. package/dist/charts/kinds/MultiLine.js +114 -237
  46. package/dist/charts/kinds/StackedArea.d.ts +21 -10
  47. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  48. package/dist/charts/kinds/StackedArea.js +74 -132
  49. package/dist/charts/kinds/ZonedLine.d.ts +24 -40
  50. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  51. package/dist/charts/kinds/ZonedLine.js +103 -184
  52. package/dist/charts/layout/auto-margin.d.ts +60 -0
  53. package/dist/charts/layout/auto-margin.d.ts.map +1 -0
  54. package/dist/charts/layout/auto-margin.js +37 -0
  55. package/dist/charts/layout/auto-margin.test.d.ts +2 -0
  56. package/dist/charts/layout/auto-margin.test.d.ts.map +1 -0
  57. package/dist/charts/primitives/CartesianChart.d.ts +143 -0
  58. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
  59. package/dist/charts/primitives/CartesianChart.js +349 -0
  60. package/dist/charts/primitives/CartesianChart.test.d.ts +2 -0
  61. package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -0
  62. package/dist/charts/primitives/ChartFrame.d.ts +16 -5
  63. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  64. package/dist/charts/primitives/ChartFrame.js +17 -2
  65. package/dist/charts/primitives/ChartLegend.d.ts +12 -1
  66. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartLegend.js +21 -3
  68. package/dist/charts/primitives/ChartLegend.test.d.ts +2 -0
  69. package/dist/charts/primitives/ChartLegend.test.d.ts.map +1 -0
  70. package/dist/charts/primitives/ChartTooltip.d.ts +16 -16
  71. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  72. package/dist/charts/primitives/ChartTooltip.js +45 -19
  73. package/dist/charts/primitives/HoverOverlay.d.ts +19 -4
  74. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  75. package/dist/charts/primitives/HoverOverlay.js +22 -1
  76. package/dist/charts/primitives/XZoneRects.d.ts +40 -0
  77. package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
  78. package/dist/charts/primitives/XZoneRects.js +28 -0
  79. package/dist/charts/primitives/XZoneRects.test.d.ts +2 -0
  80. package/dist/charts/primitives/XZoneRects.test.d.ts.map +1 -0
  81. package/dist/charts/series.d.ts +16 -1
  82. package/dist/charts/series.d.ts.map +1 -1
  83. package/dist/charts/series.js +3 -1
  84. package/dist/charts/series.test.d.ts +2 -0
  85. package/dist/charts/series.test.d.ts.map +1 -0
  86. package/dist/charts/utils/measure-text.d.ts +25 -0
  87. package/dist/charts/utils/measure-text.d.ts.map +1 -0
  88. package/dist/charts/utils/measure-text.js +64 -0
  89. package/dist/charts/utils/measure-text.test.d.ts +2 -0
  90. package/dist/charts/utils/measure-text.test.d.ts.map +1 -0
  91. package/dist/content/fence-block.d.ts.map +1 -1
  92. package/dist/surfaces.d.ts +1 -1
  93. package/dist/surfaces.d.ts.map +1 -1
  94. package/dist/surfaces.js +1 -1
  95. package/dist/tokens/chart-margin.test.d.ts +2 -0
  96. package/dist/tokens/chart-margin.test.d.ts.map +1 -0
  97. package/dist/tokens/index.d.ts +29 -0
  98. package/dist/tokens/index.d.ts.map +1 -1
  99. package/dist/tokens/index.js +10 -0
  100. package/dist/tokens/palette.d.ts +4 -0
  101. package/dist/tokens/palette.d.ts.map +1 -1
  102. package/dist/tokens/palette.js +3 -0
  103. package/llms.txt +3 -3
  104. package/package.json +1 -1
  105. package/src/charts/cursor/resolve.test.ts +61 -0
  106. package/src/charts/cursor/resolve.ts +102 -0
  107. package/src/charts/cursor/scope.tsx +32 -0
  108. package/src/charts/cursor/store.test.ts +46 -0
  109. package/src/charts/cursor/store.ts +53 -0
  110. package/src/charts/hooks/useChartCursor.ts +163 -0
  111. package/src/charts/hooks/useChartSize.ts +8 -4
  112. package/src/charts/index.ts +34 -14
  113. package/src/charts/kinds/Bars.tsx +327 -460
  114. package/src/charts/kinds/Donut.tsx +31 -23
  115. package/src/charts/kinds/DualPanel.tsx +228 -134
  116. package/src/charts/kinds/Heatmap.tsx +72 -47
  117. package/src/charts/kinds/MultiLine.tsx +158 -294
  118. package/src/charts/kinds/StackedArea.tsx +116 -175
  119. package/src/charts/kinds/ZonedLine.tsx +148 -255
  120. package/src/charts/layout/auto-margin.test.ts +42 -0
  121. package/src/charts/layout/auto-margin.ts +110 -0
  122. package/src/charts/primitives/CartesianChart.test.ts +99 -0
  123. package/src/charts/primitives/CartesianChart.tsx +566 -0
  124. package/src/charts/primitives/ChartFrame.test.tsx +33 -0
  125. package/src/charts/primitives/ChartFrame.tsx +40 -4
  126. package/src/charts/primitives/ChartLegend.test.tsx +48 -0
  127. package/src/charts/primitives/ChartLegend.tsx +45 -2
  128. package/src/charts/primitives/ChartTooltip.tsx +67 -37
  129. package/src/charts/primitives/HoverOverlay.tsx +39 -3
  130. package/src/charts/primitives/XZoneRects.test.tsx +63 -0
  131. package/src/charts/primitives/XZoneRects.tsx +67 -0
  132. package/src/charts/series.test.ts +61 -0
  133. package/src/charts/series.ts +18 -1
  134. package/src/charts/utils/measure-text.test.ts +36 -0
  135. package/src/charts/utils/measure-text.ts +109 -0
  136. package/src/content/fence-block.tsx +2 -3
  137. package/src/surfaces.ts +2 -1
  138. package/src/theme/spacing.test.ts +2 -0
  139. package/src/tokens/chart-margin.test.ts +39 -0
  140. package/src/tokens/density.test.ts +1 -0
  141. package/src/tokens/index.ts +36 -0
  142. package/src/tokens/palette.ts +3 -0
  143. package/dist/charts/hooks/useChartTooltip.d.ts +0 -13
  144. package/dist/charts/hooks/useChartTooltip.d.ts.map +0 -1
  145. package/dist/charts/hooks/useChartTooltip.js +0 -26
  146. package/dist/charts/hooks/useHoverSync.d.ts +0 -46
  147. package/dist/charts/hooks/useHoverSync.d.ts.map +0 -1
  148. package/dist/charts/hooks/useHoverSync.js +0 -75
  149. package/dist/charts/hover-context.d.ts +0 -21
  150. package/dist/charts/hover-context.d.ts.map +0 -1
  151. package/dist/charts/hover-context.js +0 -12
  152. package/dist/charts/hover-sync.d.ts +0 -14
  153. package/dist/charts/hover-sync.d.ts.map +0 -1
  154. package/dist/charts/hover-sync.js +0 -17
  155. package/dist/charts/primitives/ResponsiveChart.d.ts +0 -45
  156. package/dist/charts/primitives/ResponsiveChart.d.ts.map +0 -1
  157. package/dist/charts/primitives/ResponsiveChart.js +0 -24
  158. package/src/charts/hooks/useChartTooltip.ts +0 -31
  159. package/src/charts/hooks/useHoverSync.ts +0 -152
  160. package/src/charts/hover-context.ts +0 -28
  161. package/src/charts/hover-sync.tsx +0 -37
  162. package/src/charts/primitives/ResponsiveChart.tsx +0 -64
@@ -1,73 +1,51 @@
1
1
  import { curveMonotoneX } from '@visx/curve'
2
- import { GridRows } from '@visx/grid'
3
- import { Group } from '@visx/group'
4
- import { scaleLinear, scalePoint } from '@visx/scale'
5
2
  import { LinePath } from '@visx/shape'
6
- import { memo, useMemo, useState } from 'react'
3
+ import { memo, useMemo } from 'react'
7
4
  import type { ReactNode } from 'react'
8
- import { AxisBottomDate, AxisLeftNumeric } from '../primitives/Axes'
9
- import {
10
- ChartTooltip,
11
- TooltipBody,
12
- TooltipHeader,
13
- TooltipRow,
14
- useTooltipStyles,
15
- } from '../primitives/ChartTooltip'
16
- import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
17
- import { Crosshair, SeriesDot } from '../primitives/Crosshair'
18
- import { HoverOverlay } from '../primitives/HoverOverlay'
19
- import { ZoneRects } from '../primitives/ZoneRects'
5
+ import type { CartesianTooltipConfig, AxisConfig, PlotContext } from '../primitives/CartesianChart'
6
+ import { CartesianChart } from '../primitives/CartesianChart'
7
+ import type { XZoneSpec } from '../primitives/XZoneRects'
20
8
  import type { ZoneSpec } from '../primitives/ZoneRects'
21
- import { useHoverSync } from '../hooks/useHoverSync'
22
- import { deriveTooltipRows, LINE_OVERLAY_STROKE_WIDTH } from '../series'
9
+ import { LINE_OVERLAY_STROKE_WIDTH } from '../series'
23
10
  import type { ChartLegendConfig, ChartSeries } from '../series'
24
11
  import { VX } from '../../tokens'
25
- import { padAutoLower } from '../utils/domain'
26
- import { smartTicks, smartTicksEvery } from '../utils/ticks'
27
12
 
28
13
  export type MultiLineProps<T> = {
29
14
  data: T[]
30
- /** Fixed height in pixels, forwarded to the internal `ChartFrame`. Default 240. */
15
+ /** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
31
16
  height?: number
32
17
  chartId: string
33
18
  /** Extracts the x-axis category (date string) from a data point. */
34
19
  getX: (d: T) => string
35
- /** 1+ line series sharing one y-axis — the single source of truth for color, dash, legend, and
36
- * tooltip rows. */
20
+ /** 1+ line series sharing the y-axis(es) — the single source of truth for color, dash, legend,
21
+ * and tooltip rows. A series opts into the right axis with `axis: 'right'`. */
37
22
  series: ChartSeries<T>[]
38
- /** Fixed y-domain (e.g. [-2.5, 2.5]) or 'auto' to compute from all series. */
39
- yDomain: [number, number] | 'auto'
40
- /** When 'auto': upper bound is at least this value. */
41
- yAutoMaxFloor?: number
42
- /** When 'auto': lower bound is at most this value. Default 0. Pass `Infinity` to disable the
43
- * forced zero baseline — the lower bound then pads DOWN from the raw data minimum instead. */
44
- yAutoMinCeil?: number
45
- /** When 'auto': padding multiplier applied away from the domain. Default 1.1. The lower bound
46
- * always pads DOWN from `min(dataMin, yAutoMinCeil)` (never up, which would clip data) — see
47
- * {@link padAutoLower}. */
48
- yAutoPad?: number
49
- /** Horizontal value-range overlays (target zones), rendered behind the lines on the left scale. */
23
+ /** Left y-axis. Collapses the old `yDomain` / `yAutoMaxFloor` / `yAutoMinCeil` / `yAutoPad` /
24
+ * `numTicksY` / `formatYTick` prop soup into one object. */
25
+ y?: AxisConfig<T>
26
+ /** Right y-axis. Passing it is what makes the chart dual-axis — the margin widens by
27
+ * measurement, and `axis: 'right'` series read against it. */
28
+ y2?: AxisConfig<T>
29
+ /** Horizontal value-range overlays (target zones), rendered behind the lines. */
50
30
  zones?: ZoneSpec[]
31
+ /** Vertical x-range overlays (time windows), rendered behind the lines. Bounds are `getX`
32
+ * domain keys. */
33
+ xZones?: XZoneSpec[]
51
34
  /** Horizontal reference lines. Solid by default; set dashed: true for a dashed line. */
52
35
  refLines?: { value: number; color: string; dashed?: boolean }[]
53
- numTicksX?: number
54
- numTicksY?: number
55
- /** Tooltip + (per-series) value formatter. */
56
- formatValue: (v: number) => string
57
- /** Y-axis tick label formatter. Falls back to the scale's default. */
58
- formatYTick?: (v: number) => string
59
- /** Label shown at the right of the tooltip header (e.g. status badge). */
60
- tooltipLabel?: (d: T) => { text: string; color: string } | null
61
- /** Extra tooltip rows rendered after the per-series rows. */
62
- renderExtraTooltipRows?: (d: T) => ReactNode
36
+ /** Exact number of x ticks. Default: as many as fit. */
37
+ xTicks?: number
38
+ /** Tooltip config — `label` for a right-aligned header badge, `extraRows` for rows appended
39
+ * after the derived per-series rows. `false` disables the tooltip entirely. */
40
+ tooltip?: CartesianTooltipConfig<T> | false
63
41
  /** Marker glyph for per-point markers. Default 'circle'. */
64
42
  markerShape?: 'circle' | 'star'
65
- /** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
43
+ /** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
66
44
  * Default `{ placement: 'bottom' }`. */
67
45
  legend?: ChartLegendConfig | false
68
- /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="img"`). */
46
+ /** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="img"`). */
69
47
  ariaLabel?: string
70
- /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
48
+ /** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
71
49
  isPending?: boolean
72
50
  }
73
51
 
@@ -85,21 +63,50 @@ function starPath(cx: number, cy: number, r: number): string {
85
63
  return `M${pts.join('L')}Z`
86
64
  }
87
65
 
66
+ type LinePt<T> = { __d: T; __y: number }
67
+
68
+ /** Per-series valid points (null-value gaps dropped). Reused by the lines and the markers. */
69
+ function seriesPoints<T>(series: ChartSeries<T>, data: readonly T[]): LinePt<T>[] {
70
+ const pts: LinePt<T>[] = []
71
+ for (const d of data) {
72
+ const v = series.getValue(d)
73
+ if (v !== null && v !== undefined && !Number.isNaN(v)) pts.push({ __d: d, __y: v })
74
+ }
75
+ return pts
76
+ }
77
+
88
78
  /**
89
- * N series sharing one y-axis, with optional zones/reference lines, per-point markers
79
+ * N series sharing one or two y-axes, with optional zones/reference lines, per-point markers
90
80
  * (PR stars / status dots), a dashed companion line per series, legend-hover dimming, and a
91
81
  * shared-cursor tooltip. Generalizes the multi-line argo charts (e1RM trend, relative
92
82
  * progression, training load, fitness trends).
93
83
  *
94
- * Composes `ChartFrame` for measuring + the derived legend — `series` is the single array that
95
- * drives the plotted lines, the legend, and the tooltip rows.
84
+ * Composes `CartesianChart` for measuring, scales, grid, axes, cursor, and tooltip — this kind
85
+ * draws ONLY the marks (lines + per-point markers). `series` is the single array that drives the
86
+ * plotted lines, the legend, and the tooltip rows.
96
87
  *
97
88
  * X-axis is built from the full `data` array so the calendar is preserved even when a series
98
89
  * has nulls; each series line skips null points (creating visual gaps).
99
90
  */
100
91
  function MultiLineInner<T>(props: MultiLineProps<T>) {
101
- const { series, chartId, height, legend, ariaLabel, isPending } = props
102
- const [highlightedKey, setHighlightedKey] = useState<string | null>(null)
92
+ const {
93
+ data,
94
+ chartId,
95
+ getX,
96
+ series,
97
+ y,
98
+ y2,
99
+ zones,
100
+ xZones,
101
+ refLines,
102
+ xTicks,
103
+ tooltip,
104
+ markerShape = 'circle',
105
+ height,
106
+ legend,
107
+ ariaLabel,
108
+ isPending,
109
+ } = props
103
110
 
104
111
  // Default line overlays to the redesign's 1.9px stroke (docs/DESIGN-SPEC.md §5) — applied once
105
112
  // here so the plotted line, the derived legend swatch, and the derived tooltip row all agree.
@@ -109,265 +116,122 @@ function MultiLineInner<T>(props: MultiLineProps<T>) {
109
116
  )
110
117
 
111
118
  return (
112
- <ChartFrame
113
- series={styledSeries}
119
+ <CartesianChart
120
+ data={data}
114
121
  chartId={chartId}
122
+ getX={getX}
123
+ series={styledSeries}
124
+ {...(y !== undefined && { y })}
125
+ {...(y2 !== undefined && { y2 })}
126
+ {...(zones !== undefined && { zones })}
127
+ {...(xZones !== undefined && { xZones })}
128
+ {...(refLines !== undefined && { refLines })}
129
+ {...(xTicks !== undefined && { xTicks })}
130
+ {...(tooltip !== undefined && { tooltip })}
115
131
  {...(height !== undefined && { height })}
132
+ {...(legend !== undefined && { legend })}
116
133
  {...(ariaLabel !== undefined && { ariaLabel })}
117
134
  {...(isPending !== undefined && { isPending })}
118
- legend={resolveLegend(legend, {
119
- highlighted: highlightedKey,
120
- onHighlight: setHighlightedKey,
121
- })}
122
135
  >
123
- {(plot) => (
124
- <MultiLinePlot
125
- {...props}
126
- series={styledSeries}
127
- plot={plot}
128
- highlightedKey={highlightedKey}
129
- />
130
- )}
131
- </ChartFrame>
136
+ {(ctx: PlotContext<T>) => <MultiLineMarks getX={getX} markerShape={markerShape} ctx={ctx} />}
137
+ </CartesianChart>
132
138
  )
133
139
  }
134
140
 
135
- type MultiLinePlotProps<T> = MultiLineProps<T> & {
136
- plot: { width: number; height: number }
137
- highlightedKey: string | null
138
- }
139
-
140
- /** The measured plot — split from {@link MultiLineInner} so its scale/hover-sync hooks only run
141
- * once `ChartFrame` has resolved a non-empty plot rect (hooks can't run inside a conditionally
142
- * invoked render-prop callback). */
143
- function MultiLinePlot<T>(props: MultiLinePlotProps<T>) {
144
- const {
145
- data,
146
- chartId,
147
- getX,
148
- series,
149
- yDomain,
150
- yAutoPad = 1.1,
151
- yAutoMaxFloor,
152
- yAutoMinCeil = 0,
153
- zones = [],
154
- refLines = [],
155
- numTicksX,
156
- numTicksY = 5,
157
- formatValue,
158
- formatYTick,
159
- tooltipLabel,
160
- renderExtraTooltipRows,
161
- markerShape = 'circle',
162
- plot,
163
- highlightedKey,
164
- } = props
141
+ /** Draws lines + per-point markers for the visible series, dimming everything except the
142
+ * legend-highlighted series (and its dashed companions, matched by `parent`). */
143
+ function MultiLineMarks<T>({
144
+ getX,
145
+ markerShape,
146
+ ctx,
147
+ }: {
148
+ getX: (d: T) => string
149
+ markerShape: 'circle' | 'star'
150
+ ctx: PlotContext<T>
151
+ }) {
152
+ const { data, visible, xScale, yScale, y2Scale, highlighted } = ctx
153
+
154
+ // Memoized per (visible, data), NOT recomputed per render: the cursor is a shared store now, so
155
+ // every pointer frame re-renders every chart on the page. Without this, each frame walks the
156
+ // full series × data grid twice (lines, then markers) and allocates a fresh point array for
157
+ // each — the exact per-paint recompute the
158
+ // pre-rebuild kind memoized away.
159
+ const pointsBySeries = useMemo(() => {
160
+ const m = new Map<string, LinePt<T>[]>()
161
+ for (const s of visible) m.set(s.key, seriesPoints(s, data))
162
+ return m
163
+ }, [visible, data])
165
164
 
166
165
  // A series stays at full opacity when nothing is highlighted, when it IS the highlighted series,
167
166
  // or when it is a companion of the highlighted series (its `parent` matches).
168
167
  const dimOpacity = (s: ChartSeries<T>): number =>
169
- highlightedKey === null || s.key === highlightedKey || s.parent === highlightedKey ? 1 : 0.25
170
-
171
- const MARGIN = VX.margin
172
- const xMax = plot.width - MARGIN.left - MARGIN.right
173
- const yMax = plot.height - MARGIN.top - MARGIN.bottom
174
-
175
- const xScale = useMemo(
176
- () =>
177
- scalePoint<string>({
178
- // Full calendar — axis does not compress across nulls.
179
- domain: data.map(getX),
180
- range: [0, xMax],
181
- padding: 0.3,
182
- }),
183
- [data, xMax, getX],
184
- )
185
-
186
- const yScale = useMemo(() => {
187
- if (yDomain === 'auto') {
188
- let dataMax = -Infinity
189
- let dataMin = Infinity
190
- for (const d of data) {
191
- for (const s of series) {
192
- const v = s.getValue(d)
193
- if (v === null || v === undefined || Number.isNaN(v)) continue
194
- if (v > dataMax) dataMax = v
195
- if (v < dataMin) dataMin = v
196
- }
197
- }
198
- const safeMax = Number.isFinite(dataMax) ? dataMax : 0
199
- const safeMin = Number.isFinite(dataMin) ? dataMin : 0
200
- const upper = Math.max(safeMax, yAutoMaxFloor ?? safeMax) * yAutoPad
201
- const lower = padAutoLower(Math.min(safeMin, yAutoMinCeil), yAutoPad)
202
- return scaleLinear<number>({ domain: [lower, upper], range: [yMax, 0], nice: true })
203
- }
204
- return scaleLinear<number>({ domain: yDomain, range: [yMax, 0] })
205
- }, [data, series, yDomain, yMax, yAutoPad, yAutoMaxFloor, yAutoMinCeil])
168
+ highlighted === null || s.key === highlighted || s.parent === highlighted ? 1 : 0.25
206
169
 
207
- const tooltipStyles = useTooltipStyles()
208
- const { tip, tooltipRef, syncedPoint, isDirectHover, handleMouse, handleLeave } = useHoverSync<T>(
209
- {
210
- data,
211
- chartId,
212
- getKey: getX,
213
- xScale,
214
- marginLeft: MARGIN.left,
215
- },
216
- )
217
-
218
- const tickValues = useMemo(
219
- () =>
220
- numTicksX ? smartTicksEvery(data.map(getX), numTicksX) : smartTicks(data.map(getX), xMax),
221
- [data, xMax, getX, numTicksX],
222
- )
223
-
224
- // Per-series valid points, computed once per (data, series) change — not re-walked inside the
225
- // render map on every paint (parity with ZonedLine's single `valid` memo). Reused by the lines,
226
- // the per-point markers, and the synced dots.
227
- type LinePt = { __d: T; __y: number }
228
- const seriesPts = useMemo(() => {
229
- const out = new Map<string, LinePt[]>()
230
- for (const s of series) {
231
- const pts: LinePt[] = []
232
- for (const d of data) {
233
- const v = s.getValue(d)
234
- if (v !== null && v !== undefined && !Number.isNaN(v)) pts.push({ __d: d, __y: v })
235
- }
236
- out.set(s.key, pts)
237
- }
238
- return out
239
- }, [data, series])
240
-
241
- const tooltipLbl = tip ? (tooltipLabel?.(tip.data) ?? null) : null
170
+ const scaleFor = (s: ChartSeries<T>) =>
171
+ s.axis === 'right' && y2Scale !== null ? y2Scale : yScale
242
172
 
243
173
  return (
244
174
  <>
245
- <svg width={plot.width} height={plot.height}>
246
- <Group left={MARGIN.left} top={MARGIN.top}>
247
- <GridRows scale={yScale} width={xMax} stroke={VX.grid} numTicks={numTicksY} />
248
-
249
- <ZoneRects zones={zones} width={xMax} leftScale={yScale} />
250
-
251
- {refLines.map((r, i) => (
252
- <line
253
- key={`ref-${i}`}
254
- x1={0}
255
- x2={xMax}
256
- y1={yScale(r.value)}
257
- y2={yScale(r.value)}
258
- stroke={r.color}
259
- strokeDasharray={r.dashed ? VX.dashArray : undefined}
260
- />
261
- ))}
262
-
263
- {series.map((s) => {
264
- const valid = seriesPts.get(s.key) ?? []
265
- if (valid.length === 0) return null
266
- return (
267
- <LinePath<LinePt>
268
- key={`line-${s.key}`}
269
- data={valid}
270
- x={(p) => xScale(getX(p.__d)) ?? 0}
271
- y={(p) => yScale(p.__y)}
272
- stroke={s.color}
273
- strokeWidth={s.strokeWidth ?? VX.lineWidth}
274
- strokeDasharray={s.dash === 'dashed' ? VX.dashArray : undefined}
275
- strokeOpacity={dimOpacity(s)}
276
- curve={curveMonotoneX}
277
- />
278
- )
279
- })}
280
-
281
- {series.flatMap((s) => {
282
- const getMarker = s.getMarker
283
- if (!getMarker) return [] as ReactNode[]
284
- const op = dimOpacity(s)
285
- const markers: ReactNode[] = []
286
- for (const p of seriesPts.get(s.key) ?? []) {
287
- const m = getMarker(p.__d)
288
- if (m === null) continue
289
- const cx = xScale(getX(p.__d)) ?? 0
290
- const cy = yScale(p.__y)
291
- const color = m.color ?? s.color
292
- const r = m.r ?? (markerShape === 'star' ? STAR_R : VX.dotR)
293
- markers.push(
294
- markerShape === 'star' ? (
295
- <path
296
- key={`mk-${s.key}-${getX(p.__d)}`}
297
- d={starPath(cx, cy, r)}
298
- fill={color}
299
- stroke={VX.dotStroke}
300
- strokeWidth={1.5}
301
- fillOpacity={op}
302
- strokeOpacity={op}
303
- />
304
- ) : (
305
- <circle
306
- key={`mk-${s.key}-${getX(p.__d)}`}
307
- cx={cx}
308
- cy={cy}
309
- r={r}
310
- fill={color}
311
- stroke={VX.dotStroke}
312
- strokeWidth={2}
313
- fillOpacity={op}
314
- strokeOpacity={op}
315
- />
316
- ),
317
- )
318
- }
319
- return markers
320
- })}
321
-
322
- {syncedPoint &&
323
- (() => {
324
- const sx = xScale(getX(syncedPoint)) ?? 0
325
- return (
326
- <>
327
- <Crosshair x={sx} top={0} bottom={yMax} />
328
- {series.map((s) => {
329
- const v = s.getValue(syncedPoint)
330
- if (v === null || v === undefined || Number.isNaN(v)) return null
331
- return <SeriesDot key={`dot-${s.key}`} cx={sx} cy={yScale(v)} color={s.color} />
332
- })}
333
- </>
334
- )
335
- })()}
336
-
337
- <AxisLeftNumeric
338
- scale={yScale}
339
- numTicks={numTicksY}
340
- {...(formatYTick !== undefined && { tickFormat: formatYTick })}
175
+ {visible.map((s) => {
176
+ const valid = pointsBySeries.get(s.key) ?? []
177
+ if (valid.length === 0) return null
178
+ const scale = scaleFor(s)
179
+ return (
180
+ <LinePath<LinePt<T>>
181
+ key={`line-${s.key}`}
182
+ data={valid}
183
+ x={(p) => xScale(getX(p.__d)) ?? 0}
184
+ y={(p) => scale(p.__y)}
185
+ stroke={s.color}
186
+ strokeWidth={s.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH}
187
+ strokeDasharray={s.dash === 'dashed' ? VX.dashArray : undefined}
188
+ strokeOpacity={dimOpacity(s)}
189
+ curve={curveMonotoneX}
341
190
  />
342
- <AxisBottomDate top={yMax} scale={xScale} tickValues={tickValues} />
191
+ )
192
+ })}
343
193
 
344
- <HoverOverlay width={xMax} height={yMax} onMove={handleMouse} onLeave={handleLeave} />
345
- </Group>
346
- </svg>
347
- <ChartTooltip tip={isDirectHover ? tip : null} tooltipRef={tooltipRef} styles={tooltipStyles}>
348
- {tip && isDirectHover && (
349
- <>
350
- <TooltipHeader
351
- date={getX(tip.data)}
352
- {...(tooltipLbl !== null && { label: tooltipLbl.text, labelColor: tooltipLbl.color })}
353
- />
354
- <TooltipBody>
355
- {deriveTooltipRows(series, tip.data, formatValue).map((row) => (
356
- <TooltipRow
357
- key={row.key}
358
- color={row.color}
359
- label={row.label}
360
- value={row.value}
361
- shape={row.shape}
362
- dashed={row.dashed}
363
- {...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
364
- />
365
- ))}
366
- {renderExtraTooltipRows?.(tip.data)}
367
- </TooltipBody>
368
- </>
369
- )}
370
- </ChartTooltip>
194
+ {visible.flatMap((s) => {
195
+ const getMarker = s.getMarker
196
+ if (!getMarker) return [] as ReactNode[]
197
+ const scale = scaleFor(s)
198
+ const op = dimOpacity(s)
199
+ const markers: ReactNode[] = []
200
+ for (const p of pointsBySeries.get(s.key) ?? []) {
201
+ const m = getMarker(p.__d)
202
+ if (m === null) continue
203
+ const cx = xScale(getX(p.__d)) ?? 0
204
+ const cy = scale(p.__y)
205
+ const color = m.color ?? s.color
206
+ const r = m.r ?? (markerShape === 'star' ? STAR_R : VX.dotR)
207
+ markers.push(
208
+ markerShape === 'star' ? (
209
+ <path
210
+ key={`mk-${s.key}-${getX(p.__d)}`}
211
+ d={starPath(cx, cy, r)}
212
+ fill={color}
213
+ stroke={VX.dotStroke}
214
+ strokeWidth={1.5}
215
+ fillOpacity={op}
216
+ strokeOpacity={op}
217
+ />
218
+ ) : (
219
+ <circle
220
+ key={`mk-${s.key}-${getX(p.__d)}`}
221
+ cx={cx}
222
+ cy={cy}
223
+ r={r}
224
+ fill={color}
225
+ stroke={VX.dotStroke}
226
+ strokeWidth={2}
227
+ fillOpacity={op}
228
+ strokeOpacity={op}
229
+ />
230
+ ),
231
+ )
232
+ }
233
+ return markers
234
+ })}
371
235
  </>
372
236
  )
373
237
  }