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,31 +1,21 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* `T = unknown` is load-bearing, not a shortcut: it keeps a consumer's existing
|
|
15
|
+
* `const bars: BarsBar[] = [...]` compiling (and still assignable to `BarsBar<Row>[]`,
|
|
16
|
+
* contravariantly) even though the type is now generic over the datum.
|
|
17
|
+
*/
|
|
18
|
+
export type BarsBar<T = unknown> = {
|
|
29
19
|
/** Field key — `getValue(d, key)` extracts the number (null = skip this slot, not domain hole). */
|
|
30
20
|
key: string
|
|
31
21
|
/** Tooltip + legend label. */
|
|
@@ -33,22 +23,30 @@ export type BarsBar = {
|
|
|
33
23
|
/** Fill color (a resolved CSS color / token ref). Per `docs/DESIGN-SPEC.md` §5, the primary bar
|
|
34
24
|
* series should typically resolve to `VX.accent`, a secondary companion to `VX.faint`. */
|
|
35
25
|
color: string
|
|
36
|
-
/** Per-series tooltip value formatter — overrides
|
|
37
|
-
|
|
26
|
+
/** Per-series tooltip value formatter — overrides the left/right axis's own format. Receives the
|
|
27
|
+
* hovered datum alongside the value. */
|
|
28
|
+
formatValue?: (v: number, d: T) => string
|
|
38
29
|
/** Y axis to plot against. Honored only in grouped layout. Default 'left'. */
|
|
39
30
|
axisSide?: 'left' | 'right'
|
|
40
31
|
/** Relative width within the group (only honored in grouped layout). Default 1 — all bars equal. */
|
|
41
32
|
weight?: number
|
|
33
|
+
/** Default true; false = drawn and legended, but never listed as a tooltip row — threads
|
|
34
|
+
* `SeriesStyle.tooltip` through `Bars`, replacing the removed per-kind `hideBarTooltipRows`. */
|
|
35
|
+
tooltip?: boolean
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
export type BarsLine = {
|
|
38
|
+
export type BarsLine<T = unknown> = {
|
|
45
39
|
key: string
|
|
46
40
|
label: string
|
|
47
41
|
color: string
|
|
48
42
|
axisSide?: 'left' | 'right'
|
|
49
43
|
strokeWidth?: number
|
|
50
44
|
dashed?: boolean
|
|
51
|
-
formatValue?: (v: number) => string
|
|
45
|
+
formatValue?: (v: number, d: T) => string
|
|
46
|
+
/** Default true; false = drawn and legended, but never listed as a tooltip row. */
|
|
47
|
+
tooltip?: boolean
|
|
48
|
+
/** Dims the plotted stroke AND the legend swatch, never the tooltip row (`SeriesStyle.strokeOpacity`). */
|
|
49
|
+
strokeOpacity?: number
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
/** @deprecated Use ZoneSpec from primitives/ZoneRects. Kept as an alias. */
|
|
@@ -62,25 +60,9 @@ export type BarsRefLine = {
|
|
|
62
60
|
axisSide?: 'left' | 'right'
|
|
63
61
|
}
|
|
64
62
|
|
|
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
63
|
export type BarsProps<T> = {
|
|
82
64
|
data: T[]
|
|
83
|
-
/** Fixed height in pixels, forwarded to
|
|
65
|
+
/** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
|
|
84
66
|
height?: number
|
|
85
67
|
chartId: string
|
|
86
68
|
getX: (d: T) => string
|
|
@@ -88,22 +70,27 @@ export type BarsProps<T> = {
|
|
|
88
70
|
getValue: (d: T, key: string) => number | null
|
|
89
71
|
|
|
90
72
|
/** 1+ bar series, stacked when ≥2, plotted above baseline (y >= 0). */
|
|
91
|
-
positiveBars: BarsBar[]
|
|
73
|
+
positiveBars: BarsBar<T>[]
|
|
92
74
|
/** Optional bar series stacked below baseline (rendered as flipped negatives). */
|
|
93
|
-
negativeBars?: BarsBar[]
|
|
75
|
+
negativeBars?: BarsBar<T>[]
|
|
94
76
|
|
|
95
77
|
/** 0–2 line overlays, each on left or right axis. */
|
|
96
|
-
lines?: BarsLine[]
|
|
78
|
+
lines?: BarsLine<T>[]
|
|
97
79
|
|
|
98
80
|
/** Horizontal value-range overlays (target zones, optimal bands). */
|
|
99
81
|
zones?: BarsZone[]
|
|
100
82
|
/** Dashed/solid horizontal reference lines. */
|
|
101
83
|
refLines?: BarsRefLine[]
|
|
102
84
|
|
|
103
|
-
/** Left axis config
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
/** Left axis config — bars always live here. In `barLayout: 'stacked'` (the default), `domain`
|
|
86
|
+
* defaults to the summed per-point stack total (bars + any left-axis lines), not a per-series
|
|
87
|
+
* max — the same function-domain seam `StackedArea` uses — unless overridden with a fixed tuple
|
|
88
|
+
* or a custom function. Optional, like every other kind's — omit it for a plain auto domain. */
|
|
89
|
+
y?: AxisConfig<T>
|
|
90
|
+
/** Right axis config. Passing it is what makes the chart dual-axis: it draws the right axis and
|
|
91
|
+
* widens the right margin by measurement. Line overlays (and grouped-layout bars) opt in with
|
|
92
|
+
* `axisSide: 'right'`. */
|
|
93
|
+
y2?: AxisConfig<T>
|
|
107
94
|
|
|
108
95
|
/** Bar width as fraction of slot width. Default 0.6. */
|
|
109
96
|
barWidthRatio?: number
|
|
@@ -119,27 +106,19 @@ export type BarsProps<T> = {
|
|
|
119
106
|
/** Per-bar opacity override. Receives the data point + bar key. Default 0.85. */
|
|
120
107
|
barOpacity?: (d: T, key: string) => number
|
|
121
108
|
|
|
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).
|
|
109
|
+
/** X tick count override. Default: as many as fit. */
|
|
110
|
+
xTicks?: number
|
|
111
|
+
|
|
112
|
+
/** `false` disables the tooltip entirely (and with it the crosshair dots). */
|
|
113
|
+
tooltip?: CartesianTooltipConfig<T> | false
|
|
114
|
+
/** Per-side overrides of the measured margins — the escape hatch, applied last. */
|
|
115
|
+
margin?: Partial<ChartMargin>
|
|
116
|
+
/** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
|
|
138
117
|
* Default `{ placement: 'bottom' }`. */
|
|
139
118
|
legend?: ChartLegendConfig | false
|
|
140
|
-
/** Accessible text alternative, forwarded
|
|
119
|
+
/** Accessible text alternative, forwarded as `aria-label` (+ `role="img"`). */
|
|
141
120
|
ariaLabel?: string
|
|
142
|
-
/** Forwarded to `
|
|
121
|
+
/** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
|
|
143
122
|
isPending?: boolean
|
|
144
123
|
}
|
|
145
124
|
|
|
@@ -148,27 +127,37 @@ export type BarsProps<T> = {
|
|
|
148
127
|
* overlays on left/right axes, horizontal zones, and reference lines. Covers
|
|
149
128
|
* diverging-stack + bar+line + any future stacked-bar preset.
|
|
150
129
|
*
|
|
151
|
-
* Composes `
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `highlightedKey` prop).
|
|
130
|
+
* Composes `CartesianChart` (`docs/CHARTS-SPEC.md` §2) — margin, scales, grid, axes, the shared
|
|
131
|
+
* cursor, the crosshair + per-series dots, and the tooltip are ALL the primitive's job. This file
|
|
132
|
+
* draws only the bar rects, the line overlays, and (for a diverging stack) the zero baseline.
|
|
155
133
|
*
|
|
156
134
|
* X-axis is built from the full `data` array so the calendar is preserved even
|
|
157
135
|
* with per-bar nulls (nulls become visual gaps, not domain holes).
|
|
158
136
|
*/
|
|
159
137
|
function BarsInner<T>(props: BarsProps<T>) {
|
|
160
138
|
const {
|
|
139
|
+
data,
|
|
140
|
+
height,
|
|
141
|
+
chartId,
|
|
142
|
+
getX,
|
|
143
|
+
getValue,
|
|
161
144
|
positiveBars,
|
|
162
145
|
negativeBars = [],
|
|
163
146
|
lines = [],
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
zones,
|
|
148
|
+
refLines,
|
|
149
|
+
y,
|
|
150
|
+
y2,
|
|
151
|
+
barWidthRatio = 0.6,
|
|
152
|
+
barLayout = 'stacked',
|
|
153
|
+
barOpacity,
|
|
154
|
+
xTicks,
|
|
155
|
+
tooltip,
|
|
156
|
+
margin,
|
|
167
157
|
legend,
|
|
168
158
|
ariaLabel,
|
|
169
159
|
isPending,
|
|
170
160
|
} = props
|
|
171
|
-
const [highlightedKey, setHighlightedKey] = useState<string | null>(null)
|
|
172
161
|
|
|
173
162
|
const barSeries = useMemo<ChartSeries<T>[]>(
|
|
174
163
|
() =>
|
|
@@ -178,8 +167,10 @@ function BarsInner<T>(props: BarsProps<T>) {
|
|
|
178
167
|
color: b.color,
|
|
179
168
|
mark: 'bar' as const,
|
|
180
169
|
fillOpacity: 0.85,
|
|
170
|
+
...(b.axisSide !== undefined && { axis: b.axisSide }),
|
|
181
171
|
getValue: (d: T) => getValue(d, b.key),
|
|
182
172
|
...(b.formatValue !== undefined && { formatValue: b.formatValue }),
|
|
173
|
+
...(b.tooltip !== undefined && { tooltip: b.tooltip }),
|
|
183
174
|
})),
|
|
184
175
|
[positiveBars, negativeBars, getValue],
|
|
185
176
|
)
|
|
@@ -193,440 +184,332 @@ function BarsInner<T>(props: BarsProps<T>) {
|
|
|
193
184
|
mark: 'line' as const,
|
|
194
185
|
dash: ln.dashed ? ('dashed' as const) : ('solid' as const),
|
|
195
186
|
strokeWidth: ln.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH,
|
|
187
|
+
...(ln.axisSide !== undefined && { axis: ln.axisSide }),
|
|
196
188
|
getValue: (d: T) => getValue(d, ln.key),
|
|
197
189
|
...(ln.formatValue !== undefined && { formatValue: ln.formatValue }),
|
|
190
|
+
...(ln.tooltip !== undefined && { tooltip: ln.tooltip }),
|
|
191
|
+
...(ln.strokeOpacity !== undefined && { strokeOpacity: ln.strokeOpacity }),
|
|
198
192
|
})),
|
|
199
193
|
[lines, getValue],
|
|
200
194
|
)
|
|
201
195
|
|
|
202
|
-
const
|
|
196
|
+
const series = useMemo<ChartSeries<T>[]>(
|
|
203
197
|
() => [...barSeries, ...lineSeries],
|
|
204
198
|
[barSeries, lineSeries],
|
|
205
199
|
)
|
|
206
200
|
|
|
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) {
|
|
201
|
+
// `stacked` needs the SUMMED per-point total (bars stack on top of each other), not a per-series
|
|
202
|
+
// max — the built-in 'auto' domain (per-series max) would clip a multi-bar stack. Left-axis line
|
|
203
|
+
// overlays fold into the same min/max comparison, matching the pre-CartesianChart auto-domain
|
|
204
|
+
// math. Skipped when the caller already supplies a fixed tuple or their own domain function.
|
|
205
|
+
const yConfig = useMemo<AxisConfig<T>>(() => {
|
|
206
|
+
// `y ?? {}` INSIDE the memo, never a `y = {}` destructuring default: the default builds a
|
|
207
|
+
// fresh object every render, which busts this memo and cascades into `CartesianChart`'s own
|
|
208
|
+
// margin/scale memos. Same pattern as `StackedArea`.
|
|
209
|
+
const base = y ?? {}
|
|
210
|
+
if (barLayout !== 'stacked' || (base.domain !== undefined && base.domain !== 'auto'))
|
|
211
|
+
return base
|
|
212
|
+
const pad = base.autoPad ?? 1.1
|
|
213
|
+
return {
|
|
214
|
+
...base,
|
|
215
|
+
// Visible-only, so a legend toggle shrinks the axis rather than leaving a gap.
|
|
216
|
+
domain: (rows: readonly T[], visible: readonly ChartSeries<T>[]) => {
|
|
217
|
+
const shown = new Set(visible.map((s) => s.key))
|
|
218
|
+
let maxSum = 0
|
|
219
|
+
let minSum = 0
|
|
220
|
+
for (const d of rows) {
|
|
297
221
|
let pos = 0
|
|
298
222
|
for (const b of positiveBars) {
|
|
223
|
+
if (!shown.has(b.key)) continue
|
|
299
224
|
const v = getValue(d, b.key)
|
|
300
225
|
if (v !== null && !Number.isNaN(v) && v > 0) pos += v
|
|
301
226
|
}
|
|
302
227
|
let neg = 0
|
|
303
228
|
for (const b of negativeBars) {
|
|
229
|
+
if (!shown.has(b.key)) continue
|
|
304
230
|
const v = getValue(d, b.key)
|
|
305
231
|
if (v !== null && !Number.isNaN(v) && v > 0) neg -= v
|
|
306
232
|
}
|
|
307
233
|
if (pos > maxSum) maxSum = pos
|
|
308
234
|
if (neg < minSum) minSum = neg
|
|
309
235
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for (const
|
|
313
|
-
|
|
314
|
-
const v = getValue(d, b.key)
|
|
236
|
+
for (const ln of lines) {
|
|
237
|
+
if ((ln.axisSide ?? 'left') !== 'left' || !shown.has(ln.key)) continue
|
|
238
|
+
for (const d of rows) {
|
|
239
|
+
const v = getValue(d, ln.key)
|
|
315
240
|
if (v === null || Number.isNaN(v)) continue
|
|
316
241
|
if (v > maxSum) maxSum = v
|
|
242
|
+
if (v < minSum) minSum = v
|
|
317
243
|
}
|
|
318
244
|
}
|
|
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 })
|
|
245
|
+
const upper = Math.max(maxSum, base.autoMaxFloor ?? maxSum) * pad
|
|
246
|
+
const ceil = base.autoMinCeil ?? 0
|
|
247
|
+
return [padAutoLower(Math.min(minSum, ceil), pad), upper]
|
|
248
|
+
},
|
|
333
249
|
}
|
|
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
|
-
)
|
|
250
|
+
}, [y, barLayout, positiveBars, negativeBars, lines, getValue])
|
|
393
251
|
|
|
394
|
-
const
|
|
252
|
+
const mappedRefLines = useMemo(
|
|
395
253
|
() =>
|
|
396
|
-
|
|
397
|
-
|
|
254
|
+
refLines?.map((r) => ({
|
|
255
|
+
value: r.value,
|
|
256
|
+
color: r.color,
|
|
257
|
+
...(r.dashed !== undefined && { dashed: r.dashed }),
|
|
258
|
+
...(r.axisSide !== undefined && { axis: r.axisSide }),
|
|
259
|
+
})),
|
|
260
|
+
[refLines],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<CartesianChart
|
|
265
|
+
data={data}
|
|
266
|
+
chartId={chartId}
|
|
267
|
+
getX={getX}
|
|
268
|
+
series={series}
|
|
269
|
+
y={yConfig}
|
|
270
|
+
{...(y2 !== undefined && { y2 })}
|
|
271
|
+
{...(xTicks !== undefined && { xTicks })}
|
|
272
|
+
{...(zones !== undefined && { zones })}
|
|
273
|
+
{...(mappedRefLines !== undefined && { refLines: mappedRefLines })}
|
|
274
|
+
{...(height !== undefined && { height })}
|
|
275
|
+
{...(legend !== undefined && { legend })}
|
|
276
|
+
{...(tooltip !== undefined && { tooltip })}
|
|
277
|
+
{...(margin !== undefined && { margin })}
|
|
278
|
+
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
279
|
+
{...(isPending !== undefined && { isPending })}
|
|
280
|
+
>
|
|
281
|
+
{(ctx) => (
|
|
282
|
+
<BarsMarks
|
|
283
|
+
getX={getX}
|
|
284
|
+
getValue={getValue}
|
|
285
|
+
positiveBars={positiveBars}
|
|
286
|
+
negativeBars={negativeBars}
|
|
287
|
+
lines={lines}
|
|
288
|
+
barWidthRatio={barWidthRatio}
|
|
289
|
+
barLayout={barLayout}
|
|
290
|
+
{...(barOpacity !== undefined && { barOpacity })}
|
|
291
|
+
ctx={ctx}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
</CartesianChart>
|
|
398
295
|
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
type BarRectDatum = {
|
|
299
|
+
key: string
|
|
300
|
+
x: number
|
|
301
|
+
y: number
|
|
302
|
+
width: number
|
|
303
|
+
height: number
|
|
304
|
+
fill: string
|
|
305
|
+
fillOpacity: number
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
type LinePt<T> = { __d: T; __y: number }
|
|
309
|
+
|
|
310
|
+
type BarsMarksProps<T> = {
|
|
311
|
+
getX: (d: T) => string
|
|
312
|
+
getValue: (d: T, key: string) => number | null
|
|
313
|
+
positiveBars: BarsBar<T>[]
|
|
314
|
+
negativeBars: BarsBar<T>[]
|
|
315
|
+
lines: BarsLine<T>[]
|
|
316
|
+
barWidthRatio: number
|
|
317
|
+
barLayout: 'stacked' | 'grouped'
|
|
318
|
+
barOpacity?: (d: T, key: string) => number
|
|
319
|
+
ctx: PlotContext<T>
|
|
320
|
+
}
|
|
399
321
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
322
|
+
/**
|
|
323
|
+
* The bars, line overlays, and zero baseline. A component rather than an inline render-prop body
|
|
324
|
+
* so the per-row rect geometry can be memoized: the cursor is a shared store, so every pointer
|
|
325
|
+
* frame re-renders every mounted chart, and re-walking rows × bar-series (building fresh rect data
|
|
326
|
+
* + allocating a `<rect>`/`<g>` per bar) each frame is pure waste (parity with StackedAreaMarks /
|
|
327
|
+
* MultiLineMarks).
|
|
328
|
+
*/
|
|
329
|
+
function BarsMarks<T>({
|
|
330
|
+
getX,
|
|
331
|
+
getValue,
|
|
332
|
+
positiveBars,
|
|
333
|
+
negativeBars,
|
|
334
|
+
lines,
|
|
335
|
+
barWidthRatio,
|
|
336
|
+
barLayout,
|
|
337
|
+
barOpacity,
|
|
338
|
+
ctx,
|
|
339
|
+
}: BarsMarksProps<T>) {
|
|
340
|
+
const { data: rows, xScale, yScale, y2Scale, xMax, hidden, highlighted } = ctx
|
|
341
|
+
|
|
342
|
+
// Per-row bar rects — memoized on every input that drives the geometry (not on `hidden`/
|
|
343
|
+
// `highlighted` alone: a legend toggle or hover must still recompute, a hover-frame re-render of
|
|
344
|
+
// an unrelated chart must not). `scaleForSide`/`dim` are declared INSIDE the factory (rather than
|
|
345
|
+
// shared with the JSX below) so exhaustive-deps traces their `yScale`/`y2Scale`/`highlighted`
|
|
346
|
+
// reads directly instead of demanding the closures themselves as deps — those are recreated every
|
|
347
|
+
// render and would bust the memo every render if listed.
|
|
348
|
+
const barGroups = useMemo(() => {
|
|
349
|
+
const scaleForSide = (side: 'left' | 'right' | undefined) =>
|
|
350
|
+
side === 'right' && y2Scale ? y2Scale : yScale
|
|
351
|
+
const dim = (key: string) => (highlighted === null || highlighted === key ? 1 : 0.15)
|
|
352
|
+
|
|
353
|
+
const groupWidth = Math.max((xMax / Math.max(rows.length, 1)) * barWidthRatio, 2)
|
|
354
|
+
const totalWeight =
|
|
355
|
+
barLayout === 'grouped' ? positiveBars.reduce((s, b) => s + (b.weight ?? 1), 0) || 1 : 1
|
|
356
|
+
const groupedBarWidths =
|
|
357
|
+
barLayout === 'grouped'
|
|
358
|
+
? positiveBars.map((b) => Math.max(groupWidth * ((b.weight ?? 1) / totalWeight), 1))
|
|
359
|
+
: []
|
|
360
|
+
const groupedBarOffsets: number[] = []
|
|
409
361
|
let cursor = 0
|
|
410
362
|
for (const w of groupedBarWidths) {
|
|
411
|
-
|
|
363
|
+
groupedBarOffsets.push(cursor)
|
|
412
364
|
cursor += w
|
|
413
365
|
}
|
|
414
|
-
return out
|
|
415
|
-
}, [groupedBarWidths])
|
|
416
366
|
|
|
417
|
-
|
|
418
|
-
|
|
367
|
+
return rows.map((d) => {
|
|
368
|
+
const cx = xScale(getX(d)) ?? 0
|
|
369
|
+
const groupLeft = cx - groupWidth / 2
|
|
370
|
+
const rects: BarRectDatum[] = []
|
|
371
|
+
|
|
372
|
+
if (barLayout === 'stacked') {
|
|
373
|
+
let posOffset = 0
|
|
374
|
+
for (const b of positiveBars) {
|
|
375
|
+
if (hidden.has(b.key)) continue
|
|
376
|
+
const v = getValue(d, b.key)
|
|
377
|
+
if (v === null || Number.isNaN(v) || v <= 0) continue
|
|
378
|
+
const top = posOffset + v
|
|
379
|
+
const yTop = yScale(top)
|
|
380
|
+
const yBottom = yScale(posOffset)
|
|
381
|
+
rects.push({
|
|
382
|
+
key: `${getX(d)}-${b.key}`,
|
|
383
|
+
x: groupLeft,
|
|
384
|
+
y: yTop,
|
|
385
|
+
width: groupWidth,
|
|
386
|
+
height: yBottom - yTop,
|
|
387
|
+
fill: b.color,
|
|
388
|
+
fillOpacity: (barOpacity?.(d, b.key) ?? 0.85) * dim(b.key),
|
|
389
|
+
})
|
|
390
|
+
posOffset = top
|
|
391
|
+
}
|
|
392
|
+
let negOffset = 0
|
|
393
|
+
for (const b of negativeBars) {
|
|
394
|
+
if (hidden.has(b.key)) continue
|
|
395
|
+
const v = getValue(d, b.key)
|
|
396
|
+
if (v === null || Number.isNaN(v) || v <= 0) continue
|
|
397
|
+
const top = negOffset + v
|
|
398
|
+
const yTop = yScale(-negOffset)
|
|
399
|
+
const yBottom = yScale(-top)
|
|
400
|
+
rects.push({
|
|
401
|
+
key: `${getX(d)}-${b.key}-neg`,
|
|
402
|
+
x: groupLeft,
|
|
403
|
+
y: yTop,
|
|
404
|
+
width: groupWidth,
|
|
405
|
+
height: yBottom - yTop,
|
|
406
|
+
fill: b.color,
|
|
407
|
+
fillOpacity: (barOpacity?.(d, b.key) ?? 0.85) * dim(b.key),
|
|
408
|
+
})
|
|
409
|
+
negOffset = top
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
positiveBars.forEach((b, i) => {
|
|
413
|
+
if (hidden.has(b.key)) return
|
|
414
|
+
const v = getValue(d, b.key)
|
|
415
|
+
if (v === null || Number.isNaN(v) || v <= 0) return
|
|
416
|
+
const scale = scaleForSide(b.axisSide)
|
|
417
|
+
const yTop = scale(v)
|
|
418
|
+
const yBottom = scale(0)
|
|
419
|
+
rects.push({
|
|
420
|
+
key: `${getX(d)}-${b.key}`,
|
|
421
|
+
x: groupLeft + (groupedBarOffsets[i] ?? 0),
|
|
422
|
+
y: yTop,
|
|
423
|
+
width: groupedBarWidths[i] ?? 0,
|
|
424
|
+
height: yBottom - yTop,
|
|
425
|
+
fill: b.color,
|
|
426
|
+
fillOpacity: (barOpacity?.(d, b.key) ?? 0.85) * dim(b.key),
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { rowKey: getX(d), rects }
|
|
432
|
+
})
|
|
433
|
+
}, [
|
|
434
|
+
rows,
|
|
435
|
+
xScale,
|
|
436
|
+
yScale,
|
|
437
|
+
y2Scale,
|
|
438
|
+
xMax,
|
|
439
|
+
hidden,
|
|
440
|
+
highlighted,
|
|
441
|
+
positiveBars,
|
|
442
|
+
negativeBars,
|
|
443
|
+
barLayout,
|
|
444
|
+
barWidthRatio,
|
|
445
|
+
barOpacity,
|
|
446
|
+
getX,
|
|
447
|
+
getValue,
|
|
448
|
+
])
|
|
449
|
+
|
|
450
|
+
// Per-line valid points — walked once per (lines, rows), not re-walked inside the render map
|
|
451
|
+
// every paint (parity with DualPanel's lineValid / MultiLine's pointsBySeries).
|
|
452
|
+
const lineValid = useMemo(() => {
|
|
453
|
+
const out = new Map<string, LinePt<T>[]>()
|
|
454
|
+
for (const ln of lines) {
|
|
455
|
+
const valid: LinePt<T>[] = []
|
|
456
|
+
for (const d of rows) {
|
|
457
|
+
const v = getValue(d, ln.key)
|
|
458
|
+
if (v !== null && !Number.isNaN(v)) valid.push({ __d: d, __y: v })
|
|
459
|
+
}
|
|
460
|
+
out.set(ln.key, valid)
|
|
461
|
+
}
|
|
462
|
+
return out
|
|
463
|
+
}, [lines, rows, getValue])
|
|
419
464
|
|
|
420
|
-
|
|
465
|
+
// Cheap per-render closures for the JSX below — unlike `barGroups`' O(rows × series) walk,
|
|
466
|
+
// calling these a handful of times per paint (once per line/baseline) isn't worth memoizing.
|
|
467
|
+
const scaleForSide = (side: 'left' | 'right' | undefined) =>
|
|
468
|
+
side === 'right' && y2Scale ? y2Scale : yScale
|
|
469
|
+
const dim = (key: string) => (highlighted === null || highlighted === key ? 1 : 0.15)
|
|
421
470
|
|
|
422
471
|
return (
|
|
423
472
|
<>
|
|
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}
|
|
473
|
+
{barGroups.map((group) => (
|
|
474
|
+
<g key={`bars-${group.rowKey}`}>
|
|
475
|
+
{group.rects.map((r) => (
|
|
476
|
+
<rect
|
|
477
|
+
key={r.key}
|
|
478
|
+
x={r.x}
|
|
479
|
+
y={r.y}
|
|
480
|
+
width={r.width}
|
|
481
|
+
height={r.height}
|
|
482
|
+
rx={1.4}
|
|
483
|
+
fill={r.fill}
|
|
484
|
+
fillOpacity={r.fillOpacity}
|
|
556
485
|
/>
|
|
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 })}
|
|
486
|
+
))}
|
|
487
|
+
</g>
|
|
488
|
+
))}
|
|
489
|
+
|
|
490
|
+
{lines.map((ln) => {
|
|
491
|
+
if (hidden.has(ln.key)) return null
|
|
492
|
+
const scale = scaleForSide(ln.axisSide)
|
|
493
|
+
const valid = lineValid.get(ln.key) ?? []
|
|
494
|
+
if (valid.length === 0) return null
|
|
495
|
+
return (
|
|
496
|
+
<LinePath<LinePt<T>>
|
|
497
|
+
key={`line-${ln.key}`}
|
|
498
|
+
data={valid}
|
|
499
|
+
x={(p) => xScale(getX(p.__d)) ?? 0}
|
|
500
|
+
y={(p) => scale(p.__y)}
|
|
501
|
+
stroke={ln.color}
|
|
502
|
+
strokeWidth={ln.strokeWidth ?? LINE_OVERLAY_STROKE_WIDTH}
|
|
503
|
+
strokeDasharray={ln.dashed ? VX.dashArray : undefined}
|
|
504
|
+
strokeOpacity={dim(ln.key) * (ln.strokeOpacity ?? 1)}
|
|
505
|
+
curve={curveMonotoneX}
|
|
581
506
|
/>
|
|
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>
|
|
507
|
+
)
|
|
508
|
+
})}
|
|
509
|
+
|
|
510
|
+
{negativeBars.length > 0 && (
|
|
511
|
+
<line x1={0} x2={xMax} y1={yScale(0)} y2={yScale(0)} stroke={VX.grid} strokeWidth={1} />
|
|
512
|
+
)}
|
|
630
513
|
</>
|
|
631
514
|
)
|
|
632
515
|
}
|