basalt-ui 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +38 -2
  4. package/agent/rules/basalt-charts.md +279 -108
  5. package/agent/rules/basalt-mantine.md +4 -2
  6. package/agent/skills/basalt-charts/SKILL.md +114 -64
  7. package/agent/skills/basalt-design/SKILL.md +4 -3
  8. package/agent/templates/CLAUDE-block.md.tpl +9 -6
  9. package/configs/oxlint-plugin.js +152 -0
  10. package/configs/oxlint-plugin.test.ts +107 -0
  11. package/configs/oxlint.json +2 -0
  12. package/dist/charts/cursor/resolve.d.ts +33 -0
  13. package/dist/charts/cursor/resolve.d.ts.map +1 -0
  14. package/dist/charts/cursor/resolve.js +61 -0
  15. package/dist/charts/cursor/resolve.test.d.ts +2 -0
  16. package/dist/charts/cursor/resolve.test.d.ts.map +1 -0
  17. package/dist/charts/cursor/scope.d.ts +17 -0
  18. package/dist/charts/cursor/scope.d.ts.map +1 -0
  19. package/dist/charts/cursor/scope.js +20 -0
  20. package/dist/charts/cursor/store.d.ts +31 -0
  21. package/dist/charts/cursor/store.d.ts.map +1 -0
  22. package/dist/charts/cursor/store.js +25 -0
  23. package/dist/charts/cursor/store.test.d.ts +2 -0
  24. package/dist/charts/cursor/store.test.d.ts.map +1 -0
  25. package/dist/charts/hooks/useChartCursor.d.ts +39 -0
  26. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -0
  27. package/dist/charts/hooks/useChartCursor.js +104 -0
  28. package/dist/charts/hooks/useChartSize.d.ts +8 -4
  29. package/dist/charts/hooks/useChartSize.d.ts.map +1 -1
  30. package/dist/charts/index.d.ts +16 -13
  31. package/dist/charts/index.d.ts.map +1 -1
  32. package/dist/charts/index.js +38 -19
  33. package/dist/charts/kinds/Bars.d.ts +45 -54
  34. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  35. package/dist/charts/kinds/Bars.js +235 -392
  36. package/dist/charts/kinds/Bars.test.d.ts +2 -0
  37. package/dist/charts/kinds/Bars.test.d.ts.map +1 -0
  38. package/dist/charts/kinds/Donut.d.ts +5 -4
  39. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  40. package/dist/charts/kinds/Donut.js +17 -14
  41. package/dist/charts/kinds/DualPanel.d.ts +13 -0
  42. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  43. package/dist/charts/kinds/DualPanel.js +185 -92
  44. package/dist/charts/kinds/DualPanel.test.d.ts +2 -0
  45. package/dist/charts/kinds/DualPanel.test.d.ts.map +1 -0
  46. package/dist/charts/kinds/Heatmap.d.ts +15 -11
  47. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  48. package/dist/charts/kinds/Heatmap.js +38 -22
  49. package/dist/charts/kinds/MultiLine.d.ts +27 -35
  50. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  51. package/dist/charts/kinds/MultiLine.js +122 -235
  52. package/dist/charts/kinds/MultiLine.test.d.ts +2 -0
  53. package/dist/charts/kinds/MultiLine.test.d.ts.map +1 -0
  54. package/dist/charts/kinds/StackedArea.d.ts +21 -10
  55. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  56. package/dist/charts/kinds/StackedArea.js +74 -132
  57. package/dist/charts/kinds/ZonedLine.d.ts +24 -40
  58. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  59. package/dist/charts/kinds/ZonedLine.js +104 -184
  60. package/dist/charts/kinds/ZonedLine.test.d.ts +2 -0
  61. package/dist/charts/kinds/ZonedLine.test.d.ts.map +1 -0
  62. package/dist/charts/layout/auto-margin.d.ts +60 -0
  63. package/dist/charts/layout/auto-margin.d.ts.map +1 -0
  64. package/dist/charts/layout/auto-margin.js +37 -0
  65. package/dist/charts/layout/auto-margin.test.d.ts +2 -0
  66. package/dist/charts/layout/auto-margin.test.d.ts.map +1 -0
  67. package/dist/charts/primitives/CartesianChart.d.ts +161 -0
  68. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
  69. package/dist/charts/primitives/CartesianChart.js +359 -0
  70. package/dist/charts/primitives/CartesianChart.test.d.ts +2 -0
  71. package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -0
  72. package/dist/charts/primitives/ChartFrame.d.ts +16 -5
  73. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  74. package/dist/charts/primitives/ChartFrame.js +17 -2
  75. package/dist/charts/primitives/ChartLegend.d.ts +15 -1
  76. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  77. package/dist/charts/primitives/ChartLegend.js +25 -3
  78. package/dist/charts/primitives/ChartLegend.test.d.ts +2 -0
  79. package/dist/charts/primitives/ChartLegend.test.d.ts.map +1 -0
  80. package/dist/charts/primitives/ChartTooltip.d.ts +16 -16
  81. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  82. package/dist/charts/primitives/ChartTooltip.js +45 -19
  83. package/dist/charts/primitives/HoverOverlay.d.ts +19 -4
  84. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  85. package/dist/charts/primitives/HoverOverlay.js +22 -1
  86. package/dist/charts/primitives/XZoneRects.d.ts +54 -0
  87. package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
  88. package/dist/charts/primitives/XZoneRects.js +65 -0
  89. package/dist/charts/primitives/XZoneRects.test.d.ts +2 -0
  90. package/dist/charts/primitives/XZoneRects.test.d.ts.map +1 -0
  91. package/dist/charts/series.d.ts +28 -3
  92. package/dist/charts/series.d.ts.map +1 -1
  93. package/dist/charts/series.js +5 -2
  94. package/dist/charts/series.test.d.ts +2 -0
  95. package/dist/charts/series.test.d.ts.map +1 -0
  96. package/dist/charts/utils/measure-text.d.ts +25 -0
  97. package/dist/charts/utils/measure-text.d.ts.map +1 -0
  98. package/dist/charts/utils/measure-text.js +64 -0
  99. package/dist/charts/utils/measure-text.test.d.ts +2 -0
  100. package/dist/charts/utils/measure-text.test.d.ts.map +1 -0
  101. package/dist/content/fence-block.d.ts.map +1 -1
  102. package/dist/surfaces.d.ts +1 -1
  103. package/dist/surfaces.d.ts.map +1 -1
  104. package/dist/surfaces.js +1 -1
  105. package/dist/tokens/chart-margin.test.d.ts +2 -0
  106. package/dist/tokens/chart-margin.test.d.ts.map +1 -0
  107. package/dist/tokens/index.d.ts +29 -0
  108. package/dist/tokens/index.d.ts.map +1 -1
  109. package/dist/tokens/index.js +10 -0
  110. package/dist/tokens/palette.d.ts +4 -0
  111. package/dist/tokens/palette.d.ts.map +1 -1
  112. package/dist/tokens/palette.js +3 -0
  113. package/llms.txt +3 -3
  114. package/package.json +1 -1
  115. package/src/charts/cursor/resolve.test.ts +61 -0
  116. package/src/charts/cursor/resolve.ts +102 -0
  117. package/src/charts/cursor/scope.tsx +32 -0
  118. package/src/charts/cursor/store.test.ts +46 -0
  119. package/src/charts/cursor/store.ts +53 -0
  120. package/src/charts/hooks/useChartCursor.ts +163 -0
  121. package/src/charts/hooks/useChartSize.ts +8 -4
  122. package/src/charts/index.ts +35 -14
  123. package/src/charts/kinds/Bars.test.tsx +98 -0
  124. package/src/charts/kinds/Bars.tsx +350 -467
  125. package/src/charts/kinds/Donut.tsx +31 -23
  126. package/src/charts/kinds/DualPanel.test.tsx +131 -0
  127. package/src/charts/kinds/DualPanel.tsx +301 -135
  128. package/src/charts/kinds/Heatmap.tsx +72 -47
  129. package/src/charts/kinds/MultiLine.test.tsx +37 -0
  130. package/src/charts/kinds/MultiLine.tsx +171 -290
  131. package/src/charts/kinds/StackedArea.tsx +116 -175
  132. package/src/charts/kinds/ZonedLine.test.tsx +37 -0
  133. package/src/charts/kinds/ZonedLine.tsx +149 -255
  134. package/src/charts/layout/auto-margin.test.ts +42 -0
  135. package/src/charts/layout/auto-margin.ts +110 -0
  136. package/src/charts/primitives/CartesianChart.test.tsx +160 -0
  137. package/src/charts/primitives/CartesianChart.tsx +598 -0
  138. package/src/charts/primitives/ChartFrame.test.tsx +33 -0
  139. package/src/charts/primitives/ChartFrame.tsx +40 -4
  140. package/src/charts/primitives/ChartLegend.test.tsx +78 -0
  141. package/src/charts/primitives/ChartLegend.tsx +52 -2
  142. package/src/charts/primitives/ChartTooltip.tsx +67 -37
  143. package/src/charts/primitives/HoverOverlay.tsx +39 -3
  144. package/src/charts/primitives/XZoneRects.test.tsx +123 -0
  145. package/src/charts/primitives/XZoneRects.tsx +132 -0
  146. package/src/charts/series.test.ts +112 -0
  147. package/src/charts/series.ts +33 -5
  148. package/src/charts/utils/measure-text.test.ts +36 -0
  149. package/src/charts/utils/measure-text.ts +109 -0
  150. package/src/content/fence-block.tsx +2 -3
  151. package/src/surfaces.ts +2 -1
  152. package/src/theme/spacing.test.ts +2 -0
  153. package/src/tokens/chart-margin.test.ts +39 -0
  154. package/src/tokens/density.test.ts +1 -0
  155. package/src/tokens/index.ts +36 -0
  156. package/src/tokens/palette.ts +3 -0
  157. package/dist/charts/hooks/useChartTooltip.d.ts +0 -13
  158. package/dist/charts/hooks/useChartTooltip.d.ts.map +0 -1
  159. package/dist/charts/hooks/useChartTooltip.js +0 -26
  160. package/dist/charts/hooks/useHoverSync.d.ts +0 -46
  161. package/dist/charts/hooks/useHoverSync.d.ts.map +0 -1
  162. package/dist/charts/hooks/useHoverSync.js +0 -75
  163. package/dist/charts/hover-context.d.ts +0 -21
  164. package/dist/charts/hover-context.d.ts.map +0 -1
  165. package/dist/charts/hover-context.js +0 -12
  166. package/dist/charts/hover-sync.d.ts +0 -14
  167. package/dist/charts/hover-sync.d.ts.map +0 -1
  168. package/dist/charts/hover-sync.js +0 -17
  169. package/dist/charts/primitives/ResponsiveChart.d.ts +0 -45
  170. package/dist/charts/primitives/ResponsiveChart.d.ts.map +0 -1
  171. package/dist/charts/primitives/ResponsiveChart.js +0 -24
  172. package/src/charts/hooks/useChartTooltip.ts +0 -31
  173. package/src/charts/hooks/useHoverSync.ts +0 -152
  174. package/src/charts/hover-context.ts +0 -28
  175. package/src/charts/hover-sync.tsx +0 -37
  176. package/src/charts/primitives/ResponsiveChart.tsx +0 -64
@@ -1,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, useState } from 'react'
7
- import type { ReactNode } from 'react'
8
- import { AxisBottomDate, AxisLeftNumeric, AxisRightNumeric } from '../primitives/Axes'
9
- import {
10
- ChartTooltip,
11
- TooltipBody,
12
- TooltipHeader,
13
- TooltipRow,
14
- useTooltipStyles,
15
- } from '../primitives/ChartTooltip'
16
- import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
17
- import { Crosshair, SeriesDot } from '../primitives/Crosshair'
18
- import { HoverOverlay } from '../primitives/HoverOverlay'
19
- import { ZoneRects } from '../primitives/ZoneRects'
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 { useHoverSync } from '../hooks/useHoverSync'
22
- import { deriveTooltipRows, LINE_OVERLAY_STROKE_WIDTH } from '../series'
9
+ import { LINE_OVERLAY_STROKE_WIDTH } from '../series'
23
10
  import type { ChartLegendConfig, ChartSeries } from '../series'
24
- import { VX } from '../../tokens'
25
11
  import { padAutoLower } from '../utils/domain'
26
- import { smartTicks, smartTicksEvery } from '../utils/ticks'
27
12
 
28
- export type BarsBar = {
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 top-level formatValue. */
37
- formatValue?: (v: number) => string
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 the internal `ChartFrame`. Default 240. */
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 (always present — bars live here). */
104
- leftAxis: BarsAxisConfig
105
- /** Right axis config — only required when at least one line/zone/refLine uses 'right'. */
106
- rightAxis?: BarsAxisConfig
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
- /** Tooltip badge — appears at the right of the tooltip header. */
123
- tooltipLabel?: (d: T) => { text: string; color: string } | null
124
- /** Tooltip rows rendered BEFORE the generated bar/line rows. */
125
- renderPrefixTooltipRows?: (d: T) => ReactNode
126
- /** Tooltip rows rendered AFTER the generated bar/line rows. */
127
- renderExtraTooltipRows?: (d: T) => ReactNode
128
- /** Skip the auto-generated positive/negative bar rows in the tooltip (chart still renders them). */
129
- hideBarTooltipRows?: boolean
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 to `ChartFrame` as `aria-label` (+ `role="img"`). */
119
+ /** Accessible text alternative, forwarded as `aria-label` (+ `role="img"`). */
141
120
  ariaLabel?: string
142
- /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
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 `ChartFrame` for measuring + the derived legend — the legend was previously pushed
152
- * entirely to the consumer; it is now derived from `positiveBars` + `negativeBars` + `lines` and
153
- * owns its own hover-dim state (the legend-hover wiring that used to require a consumer-supplied
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
- getValue,
165
- chartId,
166
- height,
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 legendSeries = useMemo<ChartSeries<T>[]>(
196
+ const series = useMemo<ChartSeries<T>[]>(
203
197
  () => [...barSeries, ...lineSeries],
204
198
  [barSeries, lineSeries],
205
199
  )
206
200
 
207
- return (
208
- <ChartFrame
209
- series={legendSeries}
210
- chartId={chartId}
211
- {...(height !== undefined && { height })}
212
- {...(ariaLabel !== undefined && { ariaLabel })}
213
- {...(isPending !== undefined && { isPending })}
214
- legend={resolveLegend(legend, {
215
- highlighted: highlightedKey,
216
- onHighlight: setHighlightedKey,
217
- })}
218
- >
219
- {(plot) => (
220
- <BarsPlot
221
- {...props}
222
- plot={plot}
223
- highlightedKey={highlightedKey}
224
- barSeries={barSeries}
225
- lineSeries={lineSeries}
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
- } else {
311
- for (const d of data) {
312
- for (const b of positiveBars) {
313
- if ((b.axisSide ?? 'left') !== 'left') continue
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
- for (const ln of lines) {
321
- if ((ln.axisSide ?? 'left') !== 'left') continue
322
- for (const d of data) {
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
- return scaleLinear<number>({ domain: leftAxis.domain, range: [yMax, 0] })
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 tickValues = useMemo(
252
+ const mappedRefLines = useMemo(
395
253
  () =>
396
- numTicksX ? smartTicksEvery(data.map(getX), numTicksX) : smartTicks(data.map(getX), xMax),
397
- [data, xMax, getX, numTicksX],
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
- const groupWidth = Math.max((xMax / Math.max(data.length, 1)) * barWidthRatio, 2)
401
- // Per-bar widths + offsets for grouped layout (weighted distribution).
402
- const groupedBarWidths = useMemo(() => {
403
- if (barLayout !== 'grouped') return [] as number[]
404
- const totalWeight = positiveBars.reduce((s, b) => s + (b.weight ?? 1), 0) || 1
405
- return positiveBars.map((b) => Math.max(groupWidth * ((b.weight ?? 1) / totalWeight), 1))
406
- }, [positiveBars, groupWidth, barLayout])
407
- const groupedBarOffsets = useMemo(() => {
408
- const out: number[] = []
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
- out.push(cursor)
363
+ groupedBarOffsets.push(cursor)
412
364
  cursor += w
413
365
  }
414
- return out
415
- }, [groupedBarWidths])
416
366
 
417
- const scaleFor = (side: 'left' | 'right' | undefined) =>
418
- side === 'right' && rightYScale ? rightYScale : leftYScale
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
- const tooltipLbl = tip ? (tooltipLabel?.(tip.data) ?? null) : null
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
- <svg width={plot.width} height={plot.height}>
425
- <Group left={MARGIN.left} top={MARGIN.top}>
426
- <GridRows
427
- scale={leftYScale}
428
- width={xMax}
429
- stroke={VX.grid}
430
- numTicks={leftAxis.numTicks ?? 5}
431
- />
432
-
433
- <ZoneRects zones={zones} width={xMax} leftScale={leftYScale} rightScale={rightYScale} />
434
-
435
- {refLines.map((r, i) => {
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
- {syncedPoint &&
560
- (() => {
561
- const sx = xScale(getX(syncedPoint)) ?? 0
562
- return (
563
- <>
564
- <Crosshair x={sx} top={0} bottom={yMax} />
565
- {lines.map((ln) => {
566
- const v = getValue(syncedPoint, ln.key)
567
- if (v === null || Number.isNaN(v)) return null
568
- const scale = scaleFor(ln.axisSide)
569
- return (
570
- <SeriesDot key={`dot-${ln.key}`} cx={sx} cy={scale(v)} color={ln.color} />
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
- {rightYScale && rightAxis && (
583
- <AxisRightNumeric
584
- scale={rightYScale}
585
- left={xMax}
586
- numTicks={rightAxis.numTicks ?? 5}
587
- {...(rightAxis.formatTick !== undefined && { tickFormat: rightAxis.formatTick })}
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
  }