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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +38 -2
- package/agent/rules/basalt-charts.md +260 -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/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 +25 -47
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +230 -390
- 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.map +1 -1
- package/dist/charts/kinds/DualPanel.js +136 -90
- 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 +114 -237
- 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 +103 -184
- 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 +143 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.js +349 -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 +12 -1
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +21 -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 +40 -0
- package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
- package/dist/charts/primitives/XZoneRects.js +28 -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 +16 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +3 -1
- 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 +34 -14
- package/src/charts/kinds/Bars.tsx +327 -460
- package/src/charts/kinds/Donut.tsx +31 -23
- package/src/charts/kinds/DualPanel.tsx +228 -134
- package/src/charts/kinds/Heatmap.tsx +72 -47
- package/src/charts/kinds/MultiLine.tsx +158 -294
- package/src/charts/kinds/StackedArea.tsx +116 -175
- package/src/charts/kinds/ZonedLine.tsx +148 -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.ts +99 -0
- package/src/charts/primitives/CartesianChart.tsx +566 -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 +48 -0
- package/src/charts/primitives/ChartLegend.tsx +45 -2
- package/src/charts/primitives/ChartTooltip.tsx +67 -37
- package/src/charts/primitives/HoverOverlay.tsx +39 -3
- package/src/charts/primitives/XZoneRects.test.tsx +63 -0
- package/src/charts/primitives/XZoneRects.tsx +67 -0
- package/src/charts/series.test.ts +61 -0
- package/src/charts/series.ts +18 -1
- 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,29 +1,14 @@
|
|
|
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
|
|
7
|
-
import
|
|
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'
|
|
3
|
+
import { memo, useMemo } from 'react'
|
|
4
|
+
import { VX } from '../../tokens'
|
|
5
|
+
import type { ChartMargin } from '../../tokens'
|
|
6
|
+
import { CartesianChart } from '../primitives/CartesianChart'
|
|
7
|
+
import type { AxisConfig, CartesianTooltipConfig, PlotContext } from '../primitives/CartesianChart'
|
|
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
|
-
import { VX } from '../../tokens'
|
|
25
11
|
import { padAutoLower } from '../utils/domain'
|
|
26
|
-
import { smartTicks, smartTicksEvery } from '../utils/ticks'
|
|
27
12
|
|
|
28
13
|
export type BarsBar = {
|
|
29
14
|
/** Field key — `getValue(d, key)` extracts the number (null = skip this slot, not domain hole). */
|
|
@@ -33,7 +18,7 @@ export type BarsBar = {
|
|
|
33
18
|
/** Fill color (a resolved CSS color / token ref). Per `docs/DESIGN-SPEC.md` §5, the primary bar
|
|
34
19
|
* series should typically resolve to `VX.accent`, a secondary companion to `VX.faint`. */
|
|
35
20
|
color: string
|
|
36
|
-
/** Per-series tooltip value formatter — overrides
|
|
21
|
+
/** Per-series tooltip value formatter — overrides the left/right axis's own format. */
|
|
37
22
|
formatValue?: (v: number) => string
|
|
38
23
|
/** Y axis to plot against. Honored only in grouped layout. Default 'left'. */
|
|
39
24
|
axisSide?: 'left' | 'right'
|
|
@@ -62,25 +47,9 @@ export type BarsRefLine = {
|
|
|
62
47
|
axisSide?: 'left' | 'right'
|
|
63
48
|
}
|
|
64
49
|
|
|
65
|
-
export type BarsAxisConfig = {
|
|
66
|
-
/** Fixed [min, max] or 'auto' (computed from bars + lines + zones + refLines on this axis). */
|
|
67
|
-
domain: [number, number] | 'auto'
|
|
68
|
-
/** Auto-domain padding multiplier. Default 1.1. The lower bound always pads DOWN from
|
|
69
|
-
* `min(dataMin, autoMinCeil)` (never up, which would clip data) — see {@link padAutoLower}. */
|
|
70
|
-
autoPad?: number
|
|
71
|
-
/** Auto-domain lower bound ceiling — never above this. Default 0. */
|
|
72
|
-
autoMinCeil?: number
|
|
73
|
-
/** Auto-domain upper bound floor — never below this. */
|
|
74
|
-
autoMaxFloor?: number
|
|
75
|
-
/** Tick label formatter (e.g. (v) => `${v}h`). */
|
|
76
|
-
formatTick?: (v: number) => string
|
|
77
|
-
/** Number of ticks. Default 5. */
|
|
78
|
-
numTicks?: number
|
|
79
|
-
}
|
|
80
|
-
|
|
81
50
|
export type BarsProps<T> = {
|
|
82
51
|
data: T[]
|
|
83
|
-
/** Fixed height in pixels, forwarded to
|
|
52
|
+
/** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
|
|
84
53
|
height?: number
|
|
85
54
|
chartId: string
|
|
86
55
|
getX: (d: T) => string
|
|
@@ -100,10 +69,15 @@ export type BarsProps<T> = {
|
|
|
100
69
|
/** Dashed/solid horizontal reference lines. */
|
|
101
70
|
refLines?: BarsRefLine[]
|
|
102
71
|
|
|
103
|
-
/** Left axis config
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
72
|
+
/** Left axis config — bars always live here. In `barLayout: 'stacked'` (the default), `domain`
|
|
73
|
+
* defaults to the summed per-point stack total (bars + any left-axis lines), not a per-series
|
|
74
|
+
* max — the same function-domain seam `StackedArea` uses — unless overridden with a fixed tuple
|
|
75
|
+
* or a custom function. Optional, like every other kind's — omit it for a plain auto domain. */
|
|
76
|
+
y?: AxisConfig<T>
|
|
77
|
+
/** Right axis config. Passing it is what makes the chart dual-axis: it draws the right axis and
|
|
78
|
+
* widens the right margin by measurement. Line overlays (and grouped-layout bars) opt in with
|
|
79
|
+
* `axisSide: 'right'`. */
|
|
80
|
+
y2?: AxisConfig<T>
|
|
107
81
|
|
|
108
82
|
/** Bar width as fraction of slot width. Default 0.6. */
|
|
109
83
|
barWidthRatio?: number
|
|
@@ -119,27 +93,19 @@ export type BarsProps<T> = {
|
|
|
119
93
|
/** Per-bar opacity override. Receives the data point + bar key. Default 0.85. */
|
|
120
94
|
barOpacity?: (d: T, key: string) => number
|
|
121
95
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/** Default value formatter if a series doesn't define its own. */
|
|
131
|
-
formatValue?: (v: number) => string
|
|
132
|
-
/** X-axis tick count override. */
|
|
133
|
-
numTicksX?: number
|
|
134
|
-
/** Override the left margin (defaults to VX.margin.left = 44). Useful when
|
|
135
|
-
* y-axis labels are unusually wide (e.g. five-digit step counts). */
|
|
136
|
-
marginLeft?: number
|
|
137
|
-
/** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
|
|
96
|
+
/** X tick count override. Default: as many as fit. */
|
|
97
|
+
xTicks?: number
|
|
98
|
+
|
|
99
|
+
/** `false` disables the tooltip entirely (and with it the crosshair dots). */
|
|
100
|
+
tooltip?: CartesianTooltipConfig<T> | false
|
|
101
|
+
/** Per-side overrides of the measured margins — the escape hatch, applied last. */
|
|
102
|
+
margin?: Partial<ChartMargin>
|
|
103
|
+
/** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
|
|
138
104
|
* Default `{ placement: 'bottom' }`. */
|
|
139
105
|
legend?: ChartLegendConfig | false
|
|
140
|
-
/** Accessible text alternative, forwarded
|
|
106
|
+
/** Accessible text alternative, forwarded as `aria-label` (+ `role="img"`). */
|
|
141
107
|
ariaLabel?: string
|
|
142
|
-
/** Forwarded to `
|
|
108
|
+
/** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
|
|
143
109
|
isPending?: boolean
|
|
144
110
|
}
|
|
145
111
|
|
|
@@ -148,27 +114,37 @@ export type BarsProps<T> = {
|
|
|
148
114
|
* overlays on left/right axes, horizontal zones, and reference lines. Covers
|
|
149
115
|
* diverging-stack + bar+line + any future stacked-bar preset.
|
|
150
116
|
*
|
|
151
|
-
* Composes `
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `highlightedKey` prop).
|
|
117
|
+
* Composes `CartesianChart` (`docs/CHARTS-SPEC.md` §2) — margin, scales, grid, axes, the shared
|
|
118
|
+
* cursor, the crosshair + per-series dots, and the tooltip are ALL the primitive's job. This file
|
|
119
|
+
* draws only the bar rects, the line overlays, and (for a diverging stack) the zero baseline.
|
|
155
120
|
*
|
|
156
121
|
* X-axis is built from the full `data` array so the calendar is preserved even
|
|
157
122
|
* with per-bar nulls (nulls become visual gaps, not domain holes).
|
|
158
123
|
*/
|
|
159
124
|
function BarsInner<T>(props: BarsProps<T>) {
|
|
160
125
|
const {
|
|
126
|
+
data,
|
|
127
|
+
height,
|
|
128
|
+
chartId,
|
|
129
|
+
getX,
|
|
130
|
+
getValue,
|
|
161
131
|
positiveBars,
|
|
162
132
|
negativeBars = [],
|
|
163
133
|
lines = [],
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
134
|
+
zones,
|
|
135
|
+
refLines,
|
|
136
|
+
y,
|
|
137
|
+
y2,
|
|
138
|
+
barWidthRatio = 0.6,
|
|
139
|
+
barLayout = 'stacked',
|
|
140
|
+
barOpacity,
|
|
141
|
+
xTicks,
|
|
142
|
+
tooltip,
|
|
143
|
+
margin,
|
|
167
144
|
legend,
|
|
168
145
|
ariaLabel,
|
|
169
146
|
isPending,
|
|
170
147
|
} = props
|
|
171
|
-
const [highlightedKey, setHighlightedKey] = useState<string | null>(null)
|
|
172
148
|
|
|
173
149
|
const barSeries = useMemo<ChartSeries<T>[]>(
|
|
174
150
|
() =>
|
|
@@ -178,6 +154,7 @@ function BarsInner<T>(props: BarsProps<T>) {
|
|
|
178
154
|
color: b.color,
|
|
179
155
|
mark: 'bar' as const,
|
|
180
156
|
fillOpacity: 0.85,
|
|
157
|
+
...(b.axisSide !== undefined && { axis: b.axisSide }),
|
|
181
158
|
getValue: (d: T) => getValue(d, b.key),
|
|
182
159
|
...(b.formatValue !== undefined && { formatValue: b.formatValue }),
|
|
183
160
|
})),
|
|
@@ -193,440 +170,330 @@ function BarsInner<T>(props: BarsProps<T>) {
|
|
|
193
170
|
mark: 'line' as const,
|
|
194
171
|
dash: ln.dashed ? ('dashed' as const) : ('solid' as const),
|
|
195
172
|
strokeWidth: ln.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH,
|
|
173
|
+
...(ln.axisSide !== undefined && { axis: ln.axisSide }),
|
|
196
174
|
getValue: (d: T) => getValue(d, ln.key),
|
|
197
175
|
...(ln.formatValue !== undefined && { formatValue: ln.formatValue }),
|
|
198
176
|
})),
|
|
199
177
|
[lines, getValue],
|
|
200
178
|
)
|
|
201
179
|
|
|
202
|
-
const
|
|
180
|
+
const series = useMemo<ChartSeries<T>[]>(
|
|
203
181
|
() => [...barSeries, ...lineSeries],
|
|
204
182
|
[barSeries, lineSeries],
|
|
205
183
|
)
|
|
206
184
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)}
|
|
228
|
-
</ChartFrame>
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
type BarsPlotProps<T> = BarsProps<T> & {
|
|
233
|
-
plot: { width: number; height: number }
|
|
234
|
-
highlightedKey: string | null
|
|
235
|
-
barSeries: ChartSeries<T>[]
|
|
236
|
-
lineSeries: ChartSeries<T>[]
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** The measured plot — split from {@link BarsInner} so its scale/hover-sync hooks only run once
|
|
240
|
-
* `ChartFrame` has resolved a non-empty plot rect. */
|
|
241
|
-
function BarsPlot<T>(props: BarsPlotProps<T>) {
|
|
242
|
-
const {
|
|
243
|
-
data,
|
|
244
|
-
chartId,
|
|
245
|
-
getX,
|
|
246
|
-
getValue,
|
|
247
|
-
positiveBars,
|
|
248
|
-
negativeBars = [],
|
|
249
|
-
lines = [],
|
|
250
|
-
zones = [],
|
|
251
|
-
refLines = [],
|
|
252
|
-
leftAxis,
|
|
253
|
-
rightAxis,
|
|
254
|
-
barWidthRatio = 0.6,
|
|
255
|
-
barLayout = 'stacked',
|
|
256
|
-
barOpacity,
|
|
257
|
-
tooltipLabel,
|
|
258
|
-
renderPrefixTooltipRows,
|
|
259
|
-
renderExtraTooltipRows,
|
|
260
|
-
hideBarTooltipRows = false,
|
|
261
|
-
formatValue = (v) => String(Math.round(v)),
|
|
262
|
-
numTicksX,
|
|
263
|
-
marginLeft,
|
|
264
|
-
plot,
|
|
265
|
-
highlightedKey,
|
|
266
|
-
barSeries,
|
|
267
|
-
lineSeries,
|
|
268
|
-
} = props
|
|
269
|
-
|
|
270
|
-
const dimOpacity = (key: string): number =>
|
|
271
|
-
highlightedKey === null || highlightedKey === key ? 1 : 0.15
|
|
272
|
-
|
|
273
|
-
// Widen right margin when a right axis is rendered — labels need ~36px to fit "100"/"60m" etc.
|
|
274
|
-
const MARGIN = useMemo(
|
|
275
|
-
() => ({
|
|
276
|
-
...VX.margin,
|
|
277
|
-
left: marginLeft ?? VX.margin.left,
|
|
278
|
-
right: rightAxis ? Math.max(VX.margin.right, 40) : VX.margin.right,
|
|
279
|
-
}),
|
|
280
|
-
[rightAxis, marginLeft],
|
|
281
|
-
)
|
|
282
|
-
const xMax = plot.width - MARGIN.left - MARGIN.right
|
|
283
|
-
const yMax = plot.height - MARGIN.top - MARGIN.bottom
|
|
284
|
-
|
|
285
|
-
const xScale = useMemo(
|
|
286
|
-
() => scalePoint<string>({ domain: data.map(getX), range: [0, xMax], padding: 0.3 }),
|
|
287
|
-
[data, xMax, getX],
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
const leftYScale = useMemo(() => {
|
|
291
|
-
if (leftAxis.domain === 'auto') {
|
|
292
|
-
const autoPad = leftAxis.autoPad ?? 1.1
|
|
293
|
-
let maxSum = 0
|
|
294
|
-
let minSum = 0
|
|
295
|
-
if (barLayout === 'stacked') {
|
|
296
|
-
for (const d of data) {
|
|
185
|
+
// `stacked` needs the SUMMED per-point total (bars stack on top of each other), not a per-series
|
|
186
|
+
// max — the built-in 'auto' domain (per-series max) would clip a multi-bar stack. Left-axis line
|
|
187
|
+
// overlays fold into the same min/max comparison, matching the pre-CartesianChart auto-domain
|
|
188
|
+
// math. Skipped when the caller already supplies a fixed tuple or their own domain function.
|
|
189
|
+
const yConfig = useMemo<AxisConfig<T>>(() => {
|
|
190
|
+
// `y ?? {}` INSIDE the memo, never a `y = {}` destructuring default: the default builds a
|
|
191
|
+
// fresh object every render, which busts this memo and cascades into `CartesianChart`'s own
|
|
192
|
+
// margin/scale memos. Same pattern as `StackedArea`.
|
|
193
|
+
const base = y ?? {}
|
|
194
|
+
if (barLayout !== 'stacked' || (base.domain !== undefined && base.domain !== 'auto'))
|
|
195
|
+
return base
|
|
196
|
+
const pad = base.autoPad ?? 1.1
|
|
197
|
+
return {
|
|
198
|
+
...base,
|
|
199
|
+
// Visible-only, so a legend toggle shrinks the axis rather than leaving a gap.
|
|
200
|
+
domain: (rows: readonly T[], visible: readonly ChartSeries<T>[]) => {
|
|
201
|
+
const shown = new Set(visible.map((s) => s.key))
|
|
202
|
+
let maxSum = 0
|
|
203
|
+
let minSum = 0
|
|
204
|
+
for (const d of rows) {
|
|
297
205
|
let pos = 0
|
|
298
206
|
for (const b of positiveBars) {
|
|
207
|
+
if (!shown.has(b.key)) continue
|
|
299
208
|
const v = getValue(d, b.key)
|
|
300
209
|
if (v !== null && !Number.isNaN(v) && v > 0) pos += v
|
|
301
210
|
}
|
|
302
211
|
let neg = 0
|
|
303
212
|
for (const b of negativeBars) {
|
|
213
|
+
if (!shown.has(b.key)) continue
|
|
304
214
|
const v = getValue(d, b.key)
|
|
305
215
|
if (v !== null && !Number.isNaN(v) && v > 0) neg -= v
|
|
306
216
|
}
|
|
307
217
|
if (pos > maxSum) maxSum = pos
|
|
308
218
|
if (neg < minSum) minSum = neg
|
|
309
219
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for (const
|
|
313
|
-
|
|
314
|
-
const v = getValue(d, b.key)
|
|
220
|
+
for (const ln of lines) {
|
|
221
|
+
if ((ln.axisSide ?? 'left') !== 'left' || !shown.has(ln.key)) continue
|
|
222
|
+
for (const d of rows) {
|
|
223
|
+
const v = getValue(d, ln.key)
|
|
315
224
|
if (v === null || Number.isNaN(v)) continue
|
|
316
225
|
if (v > maxSum) maxSum = v
|
|
226
|
+
if (v < minSum) minSum = v
|
|
317
227
|
}
|
|
318
228
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const v = getValue(d, ln.key)
|
|
324
|
-
if (v === null || Number.isNaN(v)) continue
|
|
325
|
-
if (v > maxSum) maxSum = v
|
|
326
|
-
if (v < minSum) minSum = v
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
const upper = Math.max(maxSum, leftAxis.autoMaxFloor ?? maxSum) * autoPad
|
|
330
|
-
const ceil = leftAxis.autoMinCeil ?? 0
|
|
331
|
-
const lower = padAutoLower(Math.min(minSum, ceil), autoPad)
|
|
332
|
-
return scaleLinear<number>({ domain: [lower, upper], range: [yMax, 0], nice: true })
|
|
229
|
+
const upper = Math.max(maxSum, base.autoMaxFloor ?? maxSum) * pad
|
|
230
|
+
const ceil = base.autoMinCeil ?? 0
|
|
231
|
+
return [padAutoLower(Math.min(minSum, ceil), pad), upper]
|
|
232
|
+
},
|
|
333
233
|
}
|
|
334
|
-
|
|
335
|
-
}, [data, leftAxis, positiveBars, negativeBars, lines, getValue, yMax, barLayout])
|
|
336
|
-
|
|
337
|
-
const rightYScale = useMemo(() => {
|
|
338
|
-
if (!rightAxis) return null
|
|
339
|
-
if (rightAxis.domain === 'auto') {
|
|
340
|
-
const autoPad = rightAxis.autoPad ?? 1.1
|
|
341
|
-
let max = -Infinity
|
|
342
|
-
let min = Infinity
|
|
343
|
-
if (barLayout === 'grouped') {
|
|
344
|
-
for (const d of data) {
|
|
345
|
-
for (const b of positiveBars) {
|
|
346
|
-
if ((b.axisSide ?? 'left') !== 'right') continue
|
|
347
|
-
const v = getValue(d, b.key)
|
|
348
|
-
if (v === null || Number.isNaN(v)) continue
|
|
349
|
-
if (v > max) max = v
|
|
350
|
-
if (v < min) min = v
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
for (const ln of lines) {
|
|
355
|
-
if ((ln.axisSide ?? 'left') !== 'right') continue
|
|
356
|
-
for (const d of data) {
|
|
357
|
-
const v = getValue(d, ln.key)
|
|
358
|
-
if (v === null || Number.isNaN(v)) continue
|
|
359
|
-
if (v > max) max = v
|
|
360
|
-
if (v < min) min = v
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
for (const z of zones) {
|
|
364
|
-
if ((z.axisSide ?? 'left') !== 'right') continue
|
|
365
|
-
if (Number.isFinite(z.to) && z.to > max) max = z.to
|
|
366
|
-
if (Number.isFinite(z.from) && z.from < min) min = z.from
|
|
367
|
-
}
|
|
368
|
-
for (const r of refLines) {
|
|
369
|
-
if ((r.axisSide ?? 'left') !== 'right') continue
|
|
370
|
-
if (r.value > max) max = r.value
|
|
371
|
-
if (r.value < min) min = r.value
|
|
372
|
-
}
|
|
373
|
-
const safeMax = Number.isFinite(max) ? max : 0
|
|
374
|
-
const safeMin = Number.isFinite(min) ? min : 0
|
|
375
|
-
const upper = Math.max(safeMax, rightAxis.autoMaxFloor ?? safeMax) * autoPad
|
|
376
|
-
const ceil = rightAxis.autoMinCeil ?? 0
|
|
377
|
-
const lower = padAutoLower(Math.min(safeMin, ceil), autoPad)
|
|
378
|
-
return scaleLinear<number>({ domain: [lower, upper], range: [yMax, 0], nice: true })
|
|
379
|
-
}
|
|
380
|
-
return scaleLinear<number>({ domain: rightAxis.domain, range: [yMax, 0] })
|
|
381
|
-
}, [data, rightAxis, lines, zones, refLines, positiveBars, getValue, yMax, barLayout])
|
|
382
|
-
|
|
383
|
-
const tooltipStyles = useTooltipStyles()
|
|
384
|
-
const { tip, tooltipRef, syncedPoint, isDirectHover, handleMouse, handleLeave } = useHoverSync<T>(
|
|
385
|
-
{
|
|
386
|
-
data,
|
|
387
|
-
chartId,
|
|
388
|
-
getKey: getX,
|
|
389
|
-
xScale,
|
|
390
|
-
marginLeft: MARGIN.left,
|
|
391
|
-
},
|
|
392
|
-
)
|
|
234
|
+
}, [y, barLayout, positiveBars, negativeBars, lines, getValue])
|
|
393
235
|
|
|
394
|
-
const
|
|
236
|
+
const mappedRefLines = useMemo(
|
|
395
237
|
() =>
|
|
396
|
-
|
|
397
|
-
|
|
238
|
+
refLines?.map((r) => ({
|
|
239
|
+
value: r.value,
|
|
240
|
+
color: r.color,
|
|
241
|
+
...(r.dashed !== undefined && { dashed: r.dashed }),
|
|
242
|
+
...(r.axisSide !== undefined && { axis: r.axisSide }),
|
|
243
|
+
})),
|
|
244
|
+
[refLines],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<CartesianChart
|
|
249
|
+
data={data}
|
|
250
|
+
chartId={chartId}
|
|
251
|
+
getX={getX}
|
|
252
|
+
series={series}
|
|
253
|
+
y={yConfig}
|
|
254
|
+
{...(y2 !== undefined && { y2 })}
|
|
255
|
+
{...(xTicks !== undefined && { xTicks })}
|
|
256
|
+
{...(zones !== undefined && { zones })}
|
|
257
|
+
{...(mappedRefLines !== undefined && { refLines: mappedRefLines })}
|
|
258
|
+
{...(height !== undefined && { height })}
|
|
259
|
+
{...(legend !== undefined && { legend })}
|
|
260
|
+
{...(tooltip !== undefined && { tooltip })}
|
|
261
|
+
{...(margin !== undefined && { margin })}
|
|
262
|
+
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
263
|
+
{...(isPending !== undefined && { isPending })}
|
|
264
|
+
>
|
|
265
|
+
{(ctx) => (
|
|
266
|
+
<BarsMarks
|
|
267
|
+
getX={getX}
|
|
268
|
+
getValue={getValue}
|
|
269
|
+
positiveBars={positiveBars}
|
|
270
|
+
negativeBars={negativeBars}
|
|
271
|
+
lines={lines}
|
|
272
|
+
barWidthRatio={barWidthRatio}
|
|
273
|
+
barLayout={barLayout}
|
|
274
|
+
{...(barOpacity !== undefined && { barOpacity })}
|
|
275
|
+
ctx={ctx}
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
278
|
+
</CartesianChart>
|
|
398
279
|
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
type BarRectDatum = {
|
|
283
|
+
key: string
|
|
284
|
+
x: number
|
|
285
|
+
y: number
|
|
286
|
+
width: number
|
|
287
|
+
height: number
|
|
288
|
+
fill: string
|
|
289
|
+
fillOpacity: number
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type LinePt<T> = { __d: T; __y: number }
|
|
293
|
+
|
|
294
|
+
type BarsMarksProps<T> = {
|
|
295
|
+
getX: (d: T) => string
|
|
296
|
+
getValue: (d: T, key: string) => number | null
|
|
297
|
+
positiveBars: BarsBar[]
|
|
298
|
+
negativeBars: BarsBar[]
|
|
299
|
+
lines: BarsLine[]
|
|
300
|
+
barWidthRatio: number
|
|
301
|
+
barLayout: 'stacked' | 'grouped'
|
|
302
|
+
barOpacity?: (d: T, key: string) => number
|
|
303
|
+
ctx: PlotContext<T>
|
|
304
|
+
}
|
|
399
305
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
306
|
+
/**
|
|
307
|
+
* The bars, line overlays, and zero baseline. A component rather than an inline render-prop body
|
|
308
|
+
* so the per-row rect geometry can be memoized: the cursor is a shared store, so every pointer
|
|
309
|
+
* frame re-renders every mounted chart, and re-walking rows × bar-series (building fresh rect data
|
|
310
|
+
* + allocating a `<rect>`/`<g>` per bar) each frame is pure waste (parity with StackedAreaMarks /
|
|
311
|
+
* MultiLineMarks).
|
|
312
|
+
*/
|
|
313
|
+
function BarsMarks<T>({
|
|
314
|
+
getX,
|
|
315
|
+
getValue,
|
|
316
|
+
positiveBars,
|
|
317
|
+
negativeBars,
|
|
318
|
+
lines,
|
|
319
|
+
barWidthRatio,
|
|
320
|
+
barLayout,
|
|
321
|
+
barOpacity,
|
|
322
|
+
ctx,
|
|
323
|
+
}: BarsMarksProps<T>) {
|
|
324
|
+
const { data: rows, xScale, yScale, y2Scale, xMax, hidden, highlighted } = ctx
|
|
325
|
+
|
|
326
|
+
// Per-row bar rects — memoized on every input that drives the geometry (not on `hidden`/
|
|
327
|
+
// `highlighted` alone: a legend toggle or hover must still recompute, a hover-frame re-render of
|
|
328
|
+
// an unrelated chart must not). `scaleForSide`/`dim` are declared INSIDE the factory (rather than
|
|
329
|
+
// shared with the JSX below) so exhaustive-deps traces their `yScale`/`y2Scale`/`highlighted`
|
|
330
|
+
// reads directly instead of demanding the closures themselves as deps — those are recreated every
|
|
331
|
+
// render and would bust the memo every render if listed.
|
|
332
|
+
const barGroups = useMemo(() => {
|
|
333
|
+
const scaleForSide = (side: 'left' | 'right' | undefined) =>
|
|
334
|
+
side === 'right' && y2Scale ? y2Scale : yScale
|
|
335
|
+
const dim = (key: string) => (highlighted === null || highlighted === key ? 1 : 0.15)
|
|
336
|
+
|
|
337
|
+
const groupWidth = Math.max((xMax / Math.max(rows.length, 1)) * barWidthRatio, 2)
|
|
338
|
+
const totalWeight =
|
|
339
|
+
barLayout === 'grouped' ? positiveBars.reduce((s, b) => s + (b.weight ?? 1), 0) || 1 : 1
|
|
340
|
+
const groupedBarWidths =
|
|
341
|
+
barLayout === 'grouped'
|
|
342
|
+
? positiveBars.map((b) => Math.max(groupWidth * ((b.weight ?? 1) / totalWeight), 1))
|
|
343
|
+
: []
|
|
344
|
+
const groupedBarOffsets: number[] = []
|
|
409
345
|
let cursor = 0
|
|
410
346
|
for (const w of groupedBarWidths) {
|
|
411
|
-
|
|
347
|
+
groupedBarOffsets.push(cursor)
|
|
412
348
|
cursor += w
|
|
413
349
|
}
|
|
414
|
-
return out
|
|
415
|
-
}, [groupedBarWidths])
|
|
416
350
|
|
|
417
|
-
|
|
418
|
-
|
|
351
|
+
return rows.map((d) => {
|
|
352
|
+
const cx = xScale(getX(d)) ?? 0
|
|
353
|
+
const groupLeft = cx - groupWidth / 2
|
|
354
|
+
const rects: BarRectDatum[] = []
|
|
355
|
+
|
|
356
|
+
if (barLayout === 'stacked') {
|
|
357
|
+
let posOffset = 0
|
|
358
|
+
for (const b of positiveBars) {
|
|
359
|
+
if (hidden.has(b.key)) continue
|
|
360
|
+
const v = getValue(d, b.key)
|
|
361
|
+
if (v === null || Number.isNaN(v) || v <= 0) continue
|
|
362
|
+
const top = posOffset + v
|
|
363
|
+
const yTop = yScale(top)
|
|
364
|
+
const yBottom = yScale(posOffset)
|
|
365
|
+
rects.push({
|
|
366
|
+
key: `${getX(d)}-${b.key}`,
|
|
367
|
+
x: groupLeft,
|
|
368
|
+
y: yTop,
|
|
369
|
+
width: groupWidth,
|
|
370
|
+
height: yBottom - yTop,
|
|
371
|
+
fill: b.color,
|
|
372
|
+
fillOpacity: (barOpacity?.(d, b.key) ?? 0.85) * dim(b.key),
|
|
373
|
+
})
|
|
374
|
+
posOffset = top
|
|
375
|
+
}
|
|
376
|
+
let negOffset = 0
|
|
377
|
+
for (const b of negativeBars) {
|
|
378
|
+
if (hidden.has(b.key)) continue
|
|
379
|
+
const v = getValue(d, b.key)
|
|
380
|
+
if (v === null || Number.isNaN(v) || v <= 0) continue
|
|
381
|
+
const top = negOffset + v
|
|
382
|
+
const yTop = yScale(-negOffset)
|
|
383
|
+
const yBottom = yScale(-top)
|
|
384
|
+
rects.push({
|
|
385
|
+
key: `${getX(d)}-${b.key}-neg`,
|
|
386
|
+
x: groupLeft,
|
|
387
|
+
y: yTop,
|
|
388
|
+
width: groupWidth,
|
|
389
|
+
height: yBottom - yTop,
|
|
390
|
+
fill: b.color,
|
|
391
|
+
fillOpacity: (barOpacity?.(d, b.key) ?? 0.85) * dim(b.key),
|
|
392
|
+
})
|
|
393
|
+
negOffset = top
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
positiveBars.forEach((b, i) => {
|
|
397
|
+
if (hidden.has(b.key)) return
|
|
398
|
+
const v = getValue(d, b.key)
|
|
399
|
+
if (v === null || Number.isNaN(v) || v <= 0) return
|
|
400
|
+
const scale = scaleForSide(b.axisSide)
|
|
401
|
+
const yTop = scale(v)
|
|
402
|
+
const yBottom = scale(0)
|
|
403
|
+
rects.push({
|
|
404
|
+
key: `${getX(d)}-${b.key}`,
|
|
405
|
+
x: groupLeft + (groupedBarOffsets[i] ?? 0),
|
|
406
|
+
y: yTop,
|
|
407
|
+
width: groupedBarWidths[i] ?? 0,
|
|
408
|
+
height: yBottom - yTop,
|
|
409
|
+
fill: b.color,
|
|
410
|
+
fillOpacity: (barOpacity?.(d, b.key) ?? 0.85) * dim(b.key),
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { rowKey: getX(d), rects }
|
|
416
|
+
})
|
|
417
|
+
}, [
|
|
418
|
+
rows,
|
|
419
|
+
xScale,
|
|
420
|
+
yScale,
|
|
421
|
+
y2Scale,
|
|
422
|
+
xMax,
|
|
423
|
+
hidden,
|
|
424
|
+
highlighted,
|
|
425
|
+
positiveBars,
|
|
426
|
+
negativeBars,
|
|
427
|
+
barLayout,
|
|
428
|
+
barWidthRatio,
|
|
429
|
+
barOpacity,
|
|
430
|
+
getX,
|
|
431
|
+
getValue,
|
|
432
|
+
])
|
|
433
|
+
|
|
434
|
+
// Per-line valid points — walked once per (lines, rows), not re-walked inside the render map
|
|
435
|
+
// every paint (parity with DualPanel's lineValid / MultiLine's pointsBySeries).
|
|
436
|
+
const lineValid = useMemo(() => {
|
|
437
|
+
const out = new Map<string, LinePt<T>[]>()
|
|
438
|
+
for (const ln of lines) {
|
|
439
|
+
const valid: LinePt<T>[] = []
|
|
440
|
+
for (const d of rows) {
|
|
441
|
+
const v = getValue(d, ln.key)
|
|
442
|
+
if (v !== null && !Number.isNaN(v)) valid.push({ __d: d, __y: v })
|
|
443
|
+
}
|
|
444
|
+
out.set(ln.key, valid)
|
|
445
|
+
}
|
|
446
|
+
return out
|
|
447
|
+
}, [lines, rows, getValue])
|
|
419
448
|
|
|
420
|
-
|
|
449
|
+
// Cheap per-render closures for the JSX below — unlike `barGroups`' O(rows × series) walk,
|
|
450
|
+
// calling these a handful of times per paint (once per line/baseline) isn't worth memoizing.
|
|
451
|
+
const scaleForSide = (side: 'left' | 'right' | undefined) =>
|
|
452
|
+
side === 'right' && y2Scale ? y2Scale : yScale
|
|
453
|
+
const dim = (key: string) => (highlighted === null || highlighted === key ? 1 : 0.15)
|
|
421
454
|
|
|
422
455
|
return (
|
|
423
456
|
<>
|
|
424
|
-
|
|
425
|
-
<
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const scale = scaleFor(r.axisSide)
|
|
437
|
-
return (
|
|
438
|
-
<line
|
|
439
|
-
key={`ref-${i}`}
|
|
440
|
-
x1={0}
|
|
441
|
-
x2={xMax}
|
|
442
|
-
y1={scale(r.value)}
|
|
443
|
-
y2={scale(r.value)}
|
|
444
|
-
stroke={r.color}
|
|
445
|
-
strokeDasharray={r.dashed ? VX.dashArray : undefined}
|
|
446
|
-
/>
|
|
447
|
-
)
|
|
448
|
-
})}
|
|
449
|
-
|
|
450
|
-
{data.map((d) => {
|
|
451
|
-
const cx = xScale(getX(d)) ?? 0
|
|
452
|
-
const groupLeft = cx - groupWidth / 2
|
|
453
|
-
|
|
454
|
-
const els: ReactNode[] = []
|
|
455
|
-
|
|
456
|
-
if (barLayout === 'stacked') {
|
|
457
|
-
let posOffset = 0
|
|
458
|
-
for (const b of positiveBars) {
|
|
459
|
-
const v = getValue(d, b.key)
|
|
460
|
-
if (v === null || Number.isNaN(v) || v <= 0) continue
|
|
461
|
-
const top = posOffset + v
|
|
462
|
-
const yTop = leftYScale(top)
|
|
463
|
-
const yBottom = leftYScale(posOffset)
|
|
464
|
-
els.push(
|
|
465
|
-
<rect
|
|
466
|
-
key={`${getX(d)}-${b.key}`}
|
|
467
|
-
x={groupLeft}
|
|
468
|
-
y={yTop}
|
|
469
|
-
width={groupWidth}
|
|
470
|
-
height={yBottom - yTop}
|
|
471
|
-
rx={1.4}
|
|
472
|
-
fill={b.color}
|
|
473
|
-
fillOpacity={(barOpacity?.(d, b.key) ?? 0.85) * dimOpacity(b.key)}
|
|
474
|
-
/>,
|
|
475
|
-
)
|
|
476
|
-
posOffset = top
|
|
477
|
-
}
|
|
478
|
-
let negOffset = 0
|
|
479
|
-
for (const b of negativeBars) {
|
|
480
|
-
const v = getValue(d, b.key)
|
|
481
|
-
if (v === null || Number.isNaN(v) || v <= 0) continue
|
|
482
|
-
const top = negOffset + v
|
|
483
|
-
const yTop = leftYScale(-negOffset)
|
|
484
|
-
const yBottom = leftYScale(-top)
|
|
485
|
-
els.push(
|
|
486
|
-
<rect
|
|
487
|
-
key={`${getX(d)}-${b.key}-neg`}
|
|
488
|
-
x={groupLeft}
|
|
489
|
-
y={yTop}
|
|
490
|
-
width={groupWidth}
|
|
491
|
-
height={yBottom - yTop}
|
|
492
|
-
rx={1.4}
|
|
493
|
-
fill={b.color}
|
|
494
|
-
fillOpacity={(barOpacity?.(d, b.key) ?? 0.85) * dimOpacity(b.key)}
|
|
495
|
-
/>,
|
|
496
|
-
)
|
|
497
|
-
negOffset = top
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
positiveBars.forEach((b, i) => {
|
|
501
|
-
const v = getValue(d, b.key)
|
|
502
|
-
if (v === null || Number.isNaN(v) || v <= 0) return
|
|
503
|
-
const scale = scaleFor(b.axisSide)
|
|
504
|
-
const yTop = scale(v)
|
|
505
|
-
const yBottom = scale(0)
|
|
506
|
-
els.push(
|
|
507
|
-
<rect
|
|
508
|
-
key={`${getX(d)}-${b.key}`}
|
|
509
|
-
x={groupLeft + (groupedBarOffsets[i] ?? 0)}
|
|
510
|
-
y={yTop}
|
|
511
|
-
width={groupedBarWidths[i] ?? 0}
|
|
512
|
-
height={yBottom - yTop}
|
|
513
|
-
rx={1.4}
|
|
514
|
-
fill={b.color}
|
|
515
|
-
fillOpacity={(barOpacity?.(d, b.key) ?? 0.85) * dimOpacity(b.key)}
|
|
516
|
-
/>,
|
|
517
|
-
)
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return <g key={`bars-${getX(d)}`}>{els}</g>
|
|
522
|
-
})}
|
|
523
|
-
|
|
524
|
-
{lines.map((ln) => {
|
|
525
|
-
const scale = scaleFor(ln.axisSide)
|
|
526
|
-
type LinePt = { __d: T; __y: number }
|
|
527
|
-
const valid: LinePt[] = []
|
|
528
|
-
for (const d of data) {
|
|
529
|
-
const v = getValue(d, ln.key)
|
|
530
|
-
if (v !== null && !Number.isNaN(v)) valid.push({ __d: d, __y: v })
|
|
531
|
-
}
|
|
532
|
-
if (valid.length === 0) return null
|
|
533
|
-
return (
|
|
534
|
-
<LinePath<LinePt>
|
|
535
|
-
key={`line-${ln.key}`}
|
|
536
|
-
data={valid}
|
|
537
|
-
x={(p) => xScale(getX(p.__d)) ?? 0}
|
|
538
|
-
y={(p) => scale(p.__y)}
|
|
539
|
-
stroke={ln.color}
|
|
540
|
-
strokeWidth={ln.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH}
|
|
541
|
-
strokeDasharray={ln.dashed ? VX.dashArray : undefined}
|
|
542
|
-
strokeOpacity={dimOpacity(ln.key)}
|
|
543
|
-
curve={curveMonotoneX}
|
|
544
|
-
/>
|
|
545
|
-
)
|
|
546
|
-
})}
|
|
547
|
-
|
|
548
|
-
{negativeBars.length > 0 && (
|
|
549
|
-
<line
|
|
550
|
-
x1={0}
|
|
551
|
-
x2={xMax}
|
|
552
|
-
y1={leftYScale(0)}
|
|
553
|
-
y2={leftYScale(0)}
|
|
554
|
-
stroke={VX.grid}
|
|
555
|
-
strokeWidth={1}
|
|
457
|
+
{barGroups.map((group) => (
|
|
458
|
+
<g key={`bars-${group.rowKey}`}>
|
|
459
|
+
{group.rects.map((r) => (
|
|
460
|
+
<rect
|
|
461
|
+
key={r.key}
|
|
462
|
+
x={r.x}
|
|
463
|
+
y={r.y}
|
|
464
|
+
width={r.width}
|
|
465
|
+
height={r.height}
|
|
466
|
+
rx={1.4}
|
|
467
|
+
fill={r.fill}
|
|
468
|
+
fillOpacity={r.fillOpacity}
|
|
556
469
|
/>
|
|
557
|
-
)}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
<AxisLeftNumeric
|
|
578
|
-
scale={leftYScale}
|
|
579
|
-
numTicks={leftAxis.numTicks ?? 5}
|
|
580
|
-
{...(leftAxis.formatTick !== undefined && { tickFormat: leftAxis.formatTick })}
|
|
470
|
+
))}
|
|
471
|
+
</g>
|
|
472
|
+
))}
|
|
473
|
+
|
|
474
|
+
{lines.map((ln) => {
|
|
475
|
+
if (hidden.has(ln.key)) return null
|
|
476
|
+
const scale = scaleForSide(ln.axisSide)
|
|
477
|
+
const valid = lineValid.get(ln.key) ?? []
|
|
478
|
+
if (valid.length === 0) return null
|
|
479
|
+
return (
|
|
480
|
+
<LinePath<LinePt<T>>
|
|
481
|
+
key={`line-${ln.key}`}
|
|
482
|
+
data={valid}
|
|
483
|
+
x={(p) => xScale(getX(p.__d)) ?? 0}
|
|
484
|
+
y={(p) => scale(p.__y)}
|
|
485
|
+
stroke={ln.color}
|
|
486
|
+
strokeWidth={ln.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH}
|
|
487
|
+
strokeDasharray={ln.dashed ? VX.dashArray : undefined}
|
|
488
|
+
strokeOpacity={dim(ln.key)}
|
|
489
|
+
curve={curveMonotoneX}
|
|
581
490
|
/>
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
/>
|
|
589
|
-
)}
|
|
590
|
-
<AxisBottomDate top={yMax} scale={xScale} tickValues={tickValues} />
|
|
591
|
-
|
|
592
|
-
<HoverOverlay width={xMax} height={yMax} onMove={handleMouse} onLeave={handleLeave} />
|
|
593
|
-
</Group>
|
|
594
|
-
</svg>
|
|
595
|
-
<ChartTooltip tip={isDirectHover ? tip : null} tooltipRef={tooltipRef} styles={tooltipStyles}>
|
|
596
|
-
{tip && isDirectHover && (
|
|
597
|
-
<>
|
|
598
|
-
<TooltipHeader
|
|
599
|
-
date={getX(tip.data)}
|
|
600
|
-
{...(tooltipLbl !== null && { label: tooltipLbl.text, labelColor: tooltipLbl.color })}
|
|
601
|
-
/>
|
|
602
|
-
<TooltipBody>
|
|
603
|
-
{renderPrefixTooltipRows?.(tip.data)}
|
|
604
|
-
{!hideBarTooltipRows &&
|
|
605
|
-
deriveTooltipRows(barSeries, tip.data, formatValue).map((row) => (
|
|
606
|
-
<TooltipRow
|
|
607
|
-
key={row.key}
|
|
608
|
-
color={row.color}
|
|
609
|
-
label={row.label}
|
|
610
|
-
value={row.value}
|
|
611
|
-
shape={row.shape}
|
|
612
|
-
/>
|
|
613
|
-
))}
|
|
614
|
-
{deriveTooltipRows(lineSeries, tip.data, formatValue).map((row) => (
|
|
615
|
-
<TooltipRow
|
|
616
|
-
key={row.key}
|
|
617
|
-
color={row.color}
|
|
618
|
-
label={row.label}
|
|
619
|
-
value={row.value}
|
|
620
|
-
shape={row.shape}
|
|
621
|
-
dashed={row.dashed}
|
|
622
|
-
{...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
|
|
623
|
-
/>
|
|
624
|
-
))}
|
|
625
|
-
{renderExtraTooltipRows?.(tip.data)}
|
|
626
|
-
</TooltipBody>
|
|
627
|
-
</>
|
|
628
|
-
)}
|
|
629
|
-
</ChartTooltip>
|
|
491
|
+
)
|
|
492
|
+
})}
|
|
493
|
+
|
|
494
|
+
{negativeBars.length > 0 && (
|
|
495
|
+
<line x1={0} x2={xMax} y1={yScale(0)} y2={yScale(0)} stroke={VX.grid} strokeWidth={1} />
|
|
496
|
+
)}
|
|
630
497
|
</>
|
|
631
498
|
)
|
|
632
499
|
}
|