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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +38 -2
- package/agent/rules/basalt-charts.md +279 -108
- package/agent/rules/basalt-mantine.md +4 -2
- package/agent/skills/basalt-charts/SKILL.md +114 -64
- package/agent/skills/basalt-design/SKILL.md +4 -3
- package/agent/templates/CLAUDE-block.md.tpl +9 -6
- package/configs/oxlint-plugin.js +152 -0
- package/configs/oxlint-plugin.test.ts +107 -0
- package/configs/oxlint.json +2 -0
- package/dist/charts/cursor/resolve.d.ts +33 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -0
- package/dist/charts/cursor/resolve.js +61 -0
- package/dist/charts/cursor/resolve.test.d.ts +2 -0
- package/dist/charts/cursor/resolve.test.d.ts.map +1 -0
- package/dist/charts/cursor/scope.d.ts +17 -0
- package/dist/charts/cursor/scope.d.ts.map +1 -0
- package/dist/charts/cursor/scope.js +20 -0
- package/dist/charts/cursor/store.d.ts +31 -0
- package/dist/charts/cursor/store.d.ts.map +1 -0
- package/dist/charts/cursor/store.js +25 -0
- package/dist/charts/cursor/store.test.d.ts +2 -0
- package/dist/charts/cursor/store.test.d.ts.map +1 -0
- package/dist/charts/hooks/useChartCursor.d.ts +39 -0
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -0
- package/dist/charts/hooks/useChartCursor.js +104 -0
- package/dist/charts/hooks/useChartSize.d.ts +8 -4
- package/dist/charts/hooks/useChartSize.d.ts.map +1 -1
- package/dist/charts/index.d.ts +16 -13
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +38 -19
- package/dist/charts/kinds/Bars.d.ts +45 -54
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +235 -392
- package/dist/charts/kinds/Bars.test.d.ts +2 -0
- package/dist/charts/kinds/Bars.test.d.ts.map +1 -0
- package/dist/charts/kinds/Donut.d.ts +5 -4
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +17 -14
- package/dist/charts/kinds/DualPanel.d.ts +13 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +185 -92
- package/dist/charts/kinds/DualPanel.test.d.ts +2 -0
- package/dist/charts/kinds/DualPanel.test.d.ts.map +1 -0
- package/dist/charts/kinds/Heatmap.d.ts +15 -11
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +38 -22
- package/dist/charts/kinds/MultiLine.d.ts +27 -35
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +122 -235
- package/dist/charts/kinds/MultiLine.test.d.ts +2 -0
- package/dist/charts/kinds/MultiLine.test.d.ts.map +1 -0
- package/dist/charts/kinds/StackedArea.d.ts +21 -10
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +74 -132
- package/dist/charts/kinds/ZonedLine.d.ts +24 -40
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +104 -184
- package/dist/charts/kinds/ZonedLine.test.d.ts +2 -0
- package/dist/charts/kinds/ZonedLine.test.d.ts.map +1 -0
- package/dist/charts/layout/auto-margin.d.ts +60 -0
- package/dist/charts/layout/auto-margin.d.ts.map +1 -0
- package/dist/charts/layout/auto-margin.js +37 -0
- package/dist/charts/layout/auto-margin.test.d.ts +2 -0
- package/dist/charts/layout/auto-margin.test.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.d.ts +161 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.js +359 -0
- package/dist/charts/primitives/CartesianChart.test.d.ts +2 -0
- package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.d.ts +16 -5
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +17 -2
- package/dist/charts/primitives/ChartLegend.d.ts +15 -1
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +25 -3
- package/dist/charts/primitives/ChartLegend.test.d.ts +2 -0
- package/dist/charts/primitives/ChartLegend.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartTooltip.d.ts +16 -16
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +45 -19
- package/dist/charts/primitives/HoverOverlay.d.ts +19 -4
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +22 -1
- package/dist/charts/primitives/XZoneRects.d.ts +54 -0
- package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
- package/dist/charts/primitives/XZoneRects.js +65 -0
- package/dist/charts/primitives/XZoneRects.test.d.ts +2 -0
- package/dist/charts/primitives/XZoneRects.test.d.ts.map +1 -0
- package/dist/charts/series.d.ts +28 -3
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +5 -2
- package/dist/charts/series.test.d.ts +2 -0
- package/dist/charts/series.test.d.ts.map +1 -0
- package/dist/charts/utils/measure-text.d.ts +25 -0
- package/dist/charts/utils/measure-text.d.ts.map +1 -0
- package/dist/charts/utils/measure-text.js +64 -0
- package/dist/charts/utils/measure-text.test.d.ts +2 -0
- package/dist/charts/utils/measure-text.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/tokens/chart-margin.test.d.ts +2 -0
- package/dist/tokens/chart-margin.test.d.ts.map +1 -0
- package/dist/tokens/index.d.ts +29 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +10 -0
- package/dist/tokens/palette.d.ts +4 -0
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +3 -0
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/charts/cursor/resolve.test.ts +61 -0
- package/src/charts/cursor/resolve.ts +102 -0
- package/src/charts/cursor/scope.tsx +32 -0
- package/src/charts/cursor/store.test.ts +46 -0
- package/src/charts/cursor/store.ts +53 -0
- package/src/charts/hooks/useChartCursor.ts +163 -0
- package/src/charts/hooks/useChartSize.ts +8 -4
- package/src/charts/index.ts +35 -14
- package/src/charts/kinds/Bars.test.tsx +98 -0
- package/src/charts/kinds/Bars.tsx +350 -467
- package/src/charts/kinds/Donut.tsx +31 -23
- package/src/charts/kinds/DualPanel.test.tsx +131 -0
- package/src/charts/kinds/DualPanel.tsx +301 -135
- package/src/charts/kinds/Heatmap.tsx +72 -47
- package/src/charts/kinds/MultiLine.test.tsx +37 -0
- package/src/charts/kinds/MultiLine.tsx +171 -290
- package/src/charts/kinds/StackedArea.tsx +116 -175
- package/src/charts/kinds/ZonedLine.test.tsx +37 -0
- package/src/charts/kinds/ZonedLine.tsx +149 -255
- package/src/charts/layout/auto-margin.test.ts +42 -0
- package/src/charts/layout/auto-margin.ts +110 -0
- package/src/charts/primitives/CartesianChart.test.tsx +160 -0
- package/src/charts/primitives/CartesianChart.tsx +598 -0
- package/src/charts/primitives/ChartFrame.test.tsx +33 -0
- package/src/charts/primitives/ChartFrame.tsx +40 -4
- package/src/charts/primitives/ChartLegend.test.tsx +78 -0
- package/src/charts/primitives/ChartLegend.tsx +52 -2
- package/src/charts/primitives/ChartTooltip.tsx +67 -37
- package/src/charts/primitives/HoverOverlay.tsx +39 -3
- package/src/charts/primitives/XZoneRects.test.tsx +123 -0
- package/src/charts/primitives/XZoneRects.tsx +132 -0
- package/src/charts/series.test.ts +112 -0
- package/src/charts/series.ts +33 -5
- package/src/charts/utils/measure-text.test.ts +36 -0
- package/src/charts/utils/measure-text.ts +109 -0
- package/src/content/fence-block.tsx +2 -3
- package/src/surfaces.ts +2 -1
- package/src/theme/spacing.test.ts +2 -0
- package/src/tokens/chart-margin.test.ts +39 -0
- package/src/tokens/density.test.ts +1 -0
- package/src/tokens/index.ts +36 -0
- package/src/tokens/palette.ts +3 -0
- package/dist/charts/hooks/useChartTooltip.d.ts +0 -13
- package/dist/charts/hooks/useChartTooltip.d.ts.map +0 -1
- package/dist/charts/hooks/useChartTooltip.js +0 -26
- package/dist/charts/hooks/useHoverSync.d.ts +0 -46
- package/dist/charts/hooks/useHoverSync.d.ts.map +0 -1
- package/dist/charts/hooks/useHoverSync.js +0 -75
- package/dist/charts/hover-context.d.ts +0 -21
- package/dist/charts/hover-context.d.ts.map +0 -1
- package/dist/charts/hover-context.js +0 -12
- package/dist/charts/hover-sync.d.ts +0 -14
- package/dist/charts/hover-sync.d.ts.map +0 -1
- package/dist/charts/hover-sync.js +0 -17
- package/dist/charts/primitives/ResponsiveChart.d.ts +0 -45
- package/dist/charts/primitives/ResponsiveChart.d.ts.map +0 -1
- package/dist/charts/primitives/ResponsiveChart.js +0 -24
- package/src/charts/hooks/useChartTooltip.ts +0 -31
- package/src/charts/hooks/useHoverSync.ts +0 -152
- package/src/charts/hover-context.ts +0 -28
- package/src/charts/hover-sync.tsx +0 -37
- 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,
|
|
3
|
+
import { memo, useCallback, useMemo } from 'react'
|
|
7
4
|
import type { ReactNode } from 'react'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
/** Tooltip
|
|
56
|
-
|
|
57
|
-
|
|
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 `
|
|
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 `
|
|
46
|
+
/** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="img"`). */
|
|
69
47
|
ariaLabel?: string
|
|
70
|
-
/** Forwarded to `
|
|
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-
|
|
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 `
|
|
95
|
-
*
|
|
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 {
|
|
102
|
-
|
|
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,139 @@ function MultiLineInner<T>(props: MultiLineProps<T>) {
|
|
|
109
116
|
)
|
|
110
117
|
|
|
111
118
|
return (
|
|
112
|
-
<
|
|
113
|
-
|
|
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
|
-
{(
|
|
124
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
153
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
// or when it is a companion of the highlighted series (its `parent` matches).
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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],
|
|
166
|
+
// or when it is a companion of the highlighted series (its `parent` matches). `useCallback`
|
|
167
|
+
// (rather than a plain function) gives both a stable identity across renders where `highlighted`/
|
|
168
|
+
// `y2Scale` haven't changed, so `markerNodes` below can depend on THEM rather than the raw
|
|
169
|
+
// scale/highlighted values a closure would otherwise hide from the deps array.
|
|
170
|
+
const dimOpacity = useCallback(
|
|
171
|
+
(s: ChartSeries<T>): number =>
|
|
172
|
+
highlighted === null || s.key === highlighted || s.parent === highlighted ? 1 : 0.25,
|
|
173
|
+
[highlighted],
|
|
184
174
|
)
|
|
185
175
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
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])
|
|
206
|
-
|
|
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
|
-
},
|
|
176
|
+
const scaleFor = useCallback(
|
|
177
|
+
(s: ChartSeries<T>) => (s.axis === 'right' && y2Scale !== null ? y2Scale : yScale),
|
|
178
|
+
[y2Scale, yScale],
|
|
216
179
|
)
|
|
217
180
|
|
|
218
|
-
|
|
181
|
+
// Memoized for the same reason as `pointsBySeries` above — the shared cursor re-renders every
|
|
182
|
+
// mounted chart on every pointer frame, and this walks every visible series' points calling the
|
|
183
|
+
// consumer's `getMarker` each time otherwise.
|
|
184
|
+
const markerNodes = useMemo<ReactNode[]>(
|
|
219
185
|
() =>
|
|
220
|
-
|
|
221
|
-
|
|
186
|
+
visible.flatMap((s) => {
|
|
187
|
+
const getMarker = s.getMarker
|
|
188
|
+
if (!getMarker) return [] as ReactNode[]
|
|
189
|
+
const scale = scaleFor(s)
|
|
190
|
+
const op = dimOpacity(s)
|
|
191
|
+
const markers: ReactNode[] = []
|
|
192
|
+
for (const p of pointsBySeries.get(s.key) ?? []) {
|
|
193
|
+
const m = getMarker(p.__d)
|
|
194
|
+
if (m === null) continue
|
|
195
|
+
const cx = xScale(getX(p.__d)) ?? 0
|
|
196
|
+
const cy = scale(p.__y)
|
|
197
|
+
const color = m.color ?? s.color
|
|
198
|
+
const r = m.r ?? (markerShape === 'star' ? STAR_R : VX.dotR)
|
|
199
|
+
markers.push(
|
|
200
|
+
markerShape === 'star' ? (
|
|
201
|
+
<path
|
|
202
|
+
key={`mk-${s.key}-${getX(p.__d)}`}
|
|
203
|
+
d={starPath(cx, cy, r)}
|
|
204
|
+
fill={color}
|
|
205
|
+
stroke={VX.dotStroke}
|
|
206
|
+
strokeWidth={1.5}
|
|
207
|
+
fillOpacity={op}
|
|
208
|
+
strokeOpacity={op}
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<circle
|
|
212
|
+
key={`mk-${s.key}-${getX(p.__d)}`}
|
|
213
|
+
cx={cx}
|
|
214
|
+
cy={cy}
|
|
215
|
+
r={r}
|
|
216
|
+
fill={color}
|
|
217
|
+
stroke={VX.dotStroke}
|
|
218
|
+
strokeWidth={2}
|
|
219
|
+
fillOpacity={op}
|
|
220
|
+
strokeOpacity={op}
|
|
221
|
+
/>
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
return markers
|
|
226
|
+
}),
|
|
227
|
+
[visible, pointsBySeries, xScale, getX, scaleFor, dimOpacity, markerShape],
|
|
222
228
|
)
|
|
223
229
|
|
|
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
|
|
242
|
-
|
|
243
230
|
return (
|
|
244
231
|
<>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 })}
|
|
232
|
+
{visible.map((s) => {
|
|
233
|
+
const valid = pointsBySeries.get(s.key) ?? []
|
|
234
|
+
if (valid.length === 0) return null
|
|
235
|
+
const scale = scaleFor(s)
|
|
236
|
+
return (
|
|
237
|
+
<LinePath<LinePt<T>>
|
|
238
|
+
key={`line-${s.key}`}
|
|
239
|
+
data={valid}
|
|
240
|
+
x={(p) => xScale(getX(p.__d)) ?? 0}
|
|
241
|
+
y={(p) => scale(p.__y)}
|
|
242
|
+
stroke={s.color}
|
|
243
|
+
strokeWidth={s.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH}
|
|
244
|
+
strokeDasharray={s.dash === 'dashed' ? VX.dashArray : undefined}
|
|
245
|
+
strokeOpacity={dimOpacity(s) * (s.strokeOpacity ?? 1)}
|
|
246
|
+
curve={curveMonotoneX}
|
|
341
247
|
/>
|
|
342
|
-
|
|
248
|
+
)
|
|
249
|
+
})}
|
|
343
250
|
|
|
344
|
-
|
|
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>
|
|
251
|
+
{markerNodes}
|
|
371
252
|
</>
|
|
372
253
|
)
|
|
373
254
|
}
|