basalt-ui 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) 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 +260 -108
  5. package/agent/rules/basalt-mantine.md +4 -2
  6. package/agent/skills/basalt-charts/SKILL.md +114 -64
  7. package/agent/skills/basalt-design/SKILL.md +4 -3
  8. package/configs/oxlint-plugin.js +152 -0
  9. package/configs/oxlint-plugin.test.ts +107 -0
  10. package/configs/oxlint.json +2 -0
  11. package/dist/charts/cursor/resolve.d.ts +33 -0
  12. package/dist/charts/cursor/resolve.d.ts.map +1 -0
  13. package/dist/charts/cursor/resolve.js +61 -0
  14. package/dist/charts/cursor/resolve.test.d.ts +2 -0
  15. package/dist/charts/cursor/resolve.test.d.ts.map +1 -0
  16. package/dist/charts/cursor/scope.d.ts +17 -0
  17. package/dist/charts/cursor/scope.d.ts.map +1 -0
  18. package/dist/charts/cursor/scope.js +20 -0
  19. package/dist/charts/cursor/store.d.ts +31 -0
  20. package/dist/charts/cursor/store.d.ts.map +1 -0
  21. package/dist/charts/cursor/store.js +25 -0
  22. package/dist/charts/cursor/store.test.d.ts +2 -0
  23. package/dist/charts/cursor/store.test.d.ts.map +1 -0
  24. package/dist/charts/hooks/useChartCursor.d.ts +39 -0
  25. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -0
  26. package/dist/charts/hooks/useChartCursor.js +104 -0
  27. package/dist/charts/hooks/useChartSize.d.ts +8 -4
  28. package/dist/charts/hooks/useChartSize.d.ts.map +1 -1
  29. package/dist/charts/index.d.ts +16 -13
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/index.js +38 -19
  32. package/dist/charts/kinds/Bars.d.ts +25 -47
  33. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  34. package/dist/charts/kinds/Bars.js +230 -390
  35. package/dist/charts/kinds/Donut.d.ts +5 -4
  36. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  37. package/dist/charts/kinds/Donut.js +17 -14
  38. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  39. package/dist/charts/kinds/DualPanel.js +136 -90
  40. package/dist/charts/kinds/Heatmap.d.ts +15 -11
  41. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  42. package/dist/charts/kinds/Heatmap.js +38 -22
  43. package/dist/charts/kinds/MultiLine.d.ts +27 -35
  44. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  45. package/dist/charts/kinds/MultiLine.js +114 -237
  46. package/dist/charts/kinds/StackedArea.d.ts +21 -10
  47. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  48. package/dist/charts/kinds/StackedArea.js +74 -132
  49. package/dist/charts/kinds/ZonedLine.d.ts +24 -40
  50. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  51. package/dist/charts/kinds/ZonedLine.js +103 -184
  52. package/dist/charts/layout/auto-margin.d.ts +60 -0
  53. package/dist/charts/layout/auto-margin.d.ts.map +1 -0
  54. package/dist/charts/layout/auto-margin.js +37 -0
  55. package/dist/charts/layout/auto-margin.test.d.ts +2 -0
  56. package/dist/charts/layout/auto-margin.test.d.ts.map +1 -0
  57. package/dist/charts/primitives/CartesianChart.d.ts +143 -0
  58. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
  59. package/dist/charts/primitives/CartesianChart.js +349 -0
  60. package/dist/charts/primitives/CartesianChart.test.d.ts +2 -0
  61. package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -0
  62. package/dist/charts/primitives/ChartFrame.d.ts +16 -5
  63. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  64. package/dist/charts/primitives/ChartFrame.js +17 -2
  65. package/dist/charts/primitives/ChartLegend.d.ts +12 -1
  66. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartLegend.js +21 -3
  68. package/dist/charts/primitives/ChartLegend.test.d.ts +2 -0
  69. package/dist/charts/primitives/ChartLegend.test.d.ts.map +1 -0
  70. package/dist/charts/primitives/ChartTooltip.d.ts +16 -16
  71. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  72. package/dist/charts/primitives/ChartTooltip.js +45 -19
  73. package/dist/charts/primitives/HoverOverlay.d.ts +19 -4
  74. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  75. package/dist/charts/primitives/HoverOverlay.js +22 -1
  76. package/dist/charts/primitives/XZoneRects.d.ts +40 -0
  77. package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
  78. package/dist/charts/primitives/XZoneRects.js +28 -0
  79. package/dist/charts/primitives/XZoneRects.test.d.ts +2 -0
  80. package/dist/charts/primitives/XZoneRects.test.d.ts.map +1 -0
  81. package/dist/charts/series.d.ts +16 -1
  82. package/dist/charts/series.d.ts.map +1 -1
  83. package/dist/charts/series.js +3 -1
  84. package/dist/charts/series.test.d.ts +2 -0
  85. package/dist/charts/series.test.d.ts.map +1 -0
  86. package/dist/charts/utils/measure-text.d.ts +25 -0
  87. package/dist/charts/utils/measure-text.d.ts.map +1 -0
  88. package/dist/charts/utils/measure-text.js +64 -0
  89. package/dist/charts/utils/measure-text.test.d.ts +2 -0
  90. package/dist/charts/utils/measure-text.test.d.ts.map +1 -0
  91. package/dist/content/fence-block.d.ts.map +1 -1
  92. package/dist/shell/app-sidebar.d.ts +19 -1
  93. package/dist/shell/app-sidebar.d.ts.map +1 -1
  94. package/dist/shell/app-sidebar.js +30 -26
  95. package/dist/shell/app-sidebar.module.css +11 -0
  96. package/dist/shell/app-sidebar.module.css.d.ts +1 -0
  97. package/dist/shell/app-sidebar.test.d.ts +2 -0
  98. package/dist/shell/app-sidebar.test.d.ts.map +1 -0
  99. package/dist/shell/index.d.ts +10 -1
  100. package/dist/shell/index.d.ts.map +1 -1
  101. package/dist/shell/index.js +2 -0
  102. package/dist/shell/index.test.d.ts +2 -0
  103. package/dist/shell/index.test.d.ts.map +1 -0
  104. package/dist/surfaces.d.ts +1 -1
  105. package/dist/surfaces.d.ts.map +1 -1
  106. package/dist/surfaces.js +1 -1
  107. package/dist/tokens/chart-margin.test.d.ts +2 -0
  108. package/dist/tokens/chart-margin.test.d.ts.map +1 -0
  109. package/dist/tokens/index.d.ts +29 -0
  110. package/dist/tokens/index.d.ts.map +1 -1
  111. package/dist/tokens/index.js +10 -0
  112. package/dist/tokens/palette.d.ts +4 -0
  113. package/dist/tokens/palette.d.ts.map +1 -1
  114. package/dist/tokens/palette.js +3 -0
  115. package/llms.txt +3 -3
  116. package/package.json +1 -1
  117. package/src/charts/cursor/resolve.test.ts +61 -0
  118. package/src/charts/cursor/resolve.ts +102 -0
  119. package/src/charts/cursor/scope.tsx +32 -0
  120. package/src/charts/cursor/store.test.ts +46 -0
  121. package/src/charts/cursor/store.ts +53 -0
  122. package/src/charts/hooks/useChartCursor.ts +163 -0
  123. package/src/charts/hooks/useChartSize.ts +8 -4
  124. package/src/charts/index.ts +34 -14
  125. package/src/charts/kinds/Bars.tsx +327 -460
  126. package/src/charts/kinds/Donut.tsx +31 -23
  127. package/src/charts/kinds/DualPanel.tsx +228 -134
  128. package/src/charts/kinds/Heatmap.tsx +72 -47
  129. package/src/charts/kinds/MultiLine.tsx +158 -294
  130. package/src/charts/kinds/StackedArea.tsx +116 -175
  131. package/src/charts/kinds/ZonedLine.tsx +148 -255
  132. package/src/charts/layout/auto-margin.test.ts +42 -0
  133. package/src/charts/layout/auto-margin.ts +110 -0
  134. package/src/charts/primitives/CartesianChart.test.ts +99 -0
  135. package/src/charts/primitives/CartesianChart.tsx +566 -0
  136. package/src/charts/primitives/ChartFrame.test.tsx +33 -0
  137. package/src/charts/primitives/ChartFrame.tsx +40 -4
  138. package/src/charts/primitives/ChartLegend.test.tsx +48 -0
  139. package/src/charts/primitives/ChartLegend.tsx +45 -2
  140. package/src/charts/primitives/ChartTooltip.tsx +67 -37
  141. package/src/charts/primitives/HoverOverlay.tsx +39 -3
  142. package/src/charts/primitives/XZoneRects.test.tsx +63 -0
  143. package/src/charts/primitives/XZoneRects.tsx +67 -0
  144. package/src/charts/series.test.ts +61 -0
  145. package/src/charts/series.ts +18 -1
  146. package/src/charts/utils/measure-text.test.ts +36 -0
  147. package/src/charts/utils/measure-text.ts +109 -0
  148. package/src/content/fence-block.tsx +2 -3
  149. package/src/shell/app-sidebar.module.css +11 -0
  150. package/src/shell/app-sidebar.module.css.d.ts +1 -0
  151. package/src/shell/app-sidebar.test.tsx +98 -0
  152. package/src/shell/app-sidebar.tsx +22 -0
  153. package/src/shell/index.test.tsx +35 -0
  154. package/src/shell/index.tsx +11 -0
  155. package/src/surfaces.ts +2 -1
  156. package/src/theme/spacing.test.ts +2 -0
  157. package/src/tokens/chart-margin.test.ts +39 -0
  158. package/src/tokens/density.test.ts +1 -0
  159. package/src/tokens/index.ts +36 -0
  160. package/src/tokens/palette.ts +3 -0
  161. package/dist/charts/hooks/useChartTooltip.d.ts +0 -13
  162. package/dist/charts/hooks/useChartTooltip.d.ts.map +0 -1
  163. package/dist/charts/hooks/useChartTooltip.js +0 -26
  164. package/dist/charts/hooks/useHoverSync.d.ts +0 -46
  165. package/dist/charts/hooks/useHoverSync.d.ts.map +0 -1
  166. package/dist/charts/hooks/useHoverSync.js +0 -75
  167. package/dist/charts/hover-context.d.ts +0 -21
  168. package/dist/charts/hover-context.d.ts.map +0 -1
  169. package/dist/charts/hover-context.js +0 -12
  170. package/dist/charts/hover-sync.d.ts +0 -14
  171. package/dist/charts/hover-sync.d.ts.map +0 -1
  172. package/dist/charts/hover-sync.js +0 -17
  173. package/dist/charts/primitives/ResponsiveChart.d.ts +0 -45
  174. package/dist/charts/primitives/ResponsiveChart.d.ts.map +0 -1
  175. package/dist/charts/primitives/ResponsiveChart.js +0 -24
  176. package/src/charts/hooks/useChartTooltip.ts +0 -31
  177. package/src/charts/hooks/useHoverSync.ts +0 -152
  178. package/src/charts/hover-context.ts +0 -28
  179. package/src/charts/hover-sync.tsx +0 -37
  180. 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, 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
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 top-level formatValue. */
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 the internal `ChartFrame`. Default 240. */
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 (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
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
- /** 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).
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 to `ChartFrame` as `aria-label` (+ `role="img"`). */
106
+ /** Accessible text alternative, forwarded as `aria-label` (+ `role="img"`). */
141
107
  ariaLabel?: string
142
- /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
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 `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).
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
- getValue,
165
- chartId,
166
- height,
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 legendSeries = useMemo<ChartSeries<T>[]>(
180
+ const series = useMemo<ChartSeries<T>[]>(
203
181
  () => [...barSeries, ...lineSeries],
204
182
  [barSeries, lineSeries],
205
183
  )
206
184
 
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) {
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
- } 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)
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
- 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 })
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
- 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
- )
234
+ }, [y, barLayout, positiveBars, negativeBars, lines, getValue])
393
235
 
394
- const tickValues = useMemo(
236
+ const mappedRefLines = useMemo(
395
237
  () =>
396
- numTicksX ? smartTicksEvery(data.map(getX), numTicksX) : smartTicks(data.map(getX), xMax),
397
- [data, xMax, getX, numTicksX],
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
- 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[] = []
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
- out.push(cursor)
347
+ groupedBarOffsets.push(cursor)
412
348
  cursor += w
413
349
  }
414
- return out
415
- }, [groupedBarWidths])
416
350
 
417
- const scaleFor = (side: 'left' | 'right' | undefined) =>
418
- side === 'right' && rightYScale ? rightYScale : leftYScale
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
- const tooltipLbl = tip ? (tooltipLabel?.(tip.data) ?? null) : null
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
- <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}
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
- {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 })}
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
- {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>
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
  }