@cdc/chart 4.26.1 → 4.26.2

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 (132) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +45357 -43655
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/line-chart-states.json +1085 -0
  9. package/examples/private/123.json +694 -0
  10. package/examples/private/anchor-issue.json +4094 -0
  11. package/examples/private/backwards-slider.json +10430 -0
  12. package/examples/private/georgia.csv +160 -0
  13. package/examples/private/timeline-data.json +1 -0
  14. package/examples/private/timeline.json +389 -0
  15. package/examples/radar-chart-simple.json +133 -0
  16. package/examples/radar-chart.json +148 -0
  17. package/index.html +1 -31
  18. package/package.json +57 -59
  19. package/src/CdcChartComponent.tsx +99 -18
  20. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  21. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  22. package/src/_stories/Chart.CI.stories.tsx +13 -0
  23. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  24. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  25. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  26. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  27. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  28. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  30. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  31. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  32. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  33. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  34. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  37. package/src/_stories/Chart.stories.tsx +37 -0
  38. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  39. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  40. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  41. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  42. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  43. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  44. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  45. package/src/_stories/ChartBrush.stories.tsx +7 -0
  46. package/src/_stories/ChartEditor.stories.tsx +7 -0
  47. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  48. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  49. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  50. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  51. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  52. package/src/_stories/_mock/brush_continuous.json +86 -0
  53. package/src/_stories/_mock/brush_date_large.json +176 -0
  54. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  55. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  56. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  57. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  58. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  59. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  60. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  61. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  63. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  64. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  65. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  66. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  67. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  68. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  69. package/src/components/Axis/BottomAxis.tsx +270 -0
  70. package/src/components/Axis/LeftAxis.tsx +404 -0
  71. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  72. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  73. package/src/components/Axis/README.md +94 -0
  74. package/src/components/Axis/RightAxis.tsx +108 -0
  75. package/src/components/Axis/axis.constants.ts +21 -0
  76. package/src/components/Axis/index.ts +7 -0
  77. package/src/components/BarChart/components/BarChart.tsx +7 -1
  78. package/src/components/Brush/BrushSelector.tsx +154 -22
  79. package/src/components/Brush/MiniChartPreview.tsx +138 -21
  80. package/src/components/EditorPanel/EditorPanel.tsx +25 -11
  81. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  82. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +81 -1
  83. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -1
  84. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  86. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -1
  87. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  88. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  89. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  90. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  91. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  92. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  93. package/src/components/HorizonChart/index.tsx +3 -0
  94. package/src/components/Legend/Legend.Component.tsx +52 -4
  95. package/src/components/Legend/Legend.tsx +1 -1
  96. package/src/components/Legend/LegendValueRange.tsx +77 -0
  97. package/src/components/Legend/helpers/createFormatLabels.tsx +13 -0
  98. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  99. package/src/components/LineChart/helpers/README.md +292 -0
  100. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  101. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  102. package/src/components/LineChart/index.tsx +44 -8
  103. package/src/components/LinearChart/README.md +109 -0
  104. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  105. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  106. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  107. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  108. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  109. package/src/components/LinearChart.tsx +250 -1059
  110. package/src/components/PieChart/PieChart.tsx +1 -1
  111. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  112. package/src/components/RadarChart/RadarChart.tsx +298 -0
  113. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  114. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  115. package/src/components/RadarChart/helpers.ts +83 -0
  116. package/src/components/RadarChart/index.tsx +3 -0
  117. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  118. package/src/data/initial-state.js +14 -1
  119. package/src/helpers/getExcludedData.ts +4 -0
  120. package/src/helpers/handleChartAriaLabels.ts +19 -19
  121. package/src/helpers/handleLineType.ts +22 -18
  122. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  123. package/src/hooks/useScales.ts +7 -0
  124. package/src/hooks/useTooltip.tsx +3 -0
  125. package/src/scss/main.scss +5 -0
  126. package/src/selectors/README.md +68 -0
  127. package/src/store/chart.reducer.ts +2 -0
  128. package/src/types/ChartConfig.ts +18 -0
  129. package/src/types/ChartContext.ts +1 -0
  130. package/src/types/Horizon.ts +64 -0
  131. package/preview.html +0 -1616
  132. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -0,0 +1,94 @@
1
+ # Axis Components
2
+
3
+ This folder contains extracted axis components from `LinearChart.tsx` to improve maintainability and reduce complexity.
4
+
5
+ ## Components
6
+
7
+ ### LeftAxis
8
+ **File:** `LeftAxis.tsx`
9
+
10
+ Renders the left (Y) axis for vertical charts. Handles:
11
+ - Tick formatting and positioning
12
+ - Labels above gridlines option
13
+ - Inline label (suffix) display
14
+ - Forest plot special rendering
15
+
16
+ **Props:**
17
+ - `yScale` - D3 scale for Y axis
18
+ - `xScale` - D3 scale for X axis
19
+ - `yMax`, `xMax` - Chart dimensions
20
+ - `yAxisWidth` - Width allocated for Y axis
21
+ - `numTicks` - Number of ticks to display
22
+ - `handleLeftTickFormatting` - Tick format function
23
+
24
+ ### LeftAxisGridlines
25
+ **File:** `LeftAxisGridlines.tsx`
26
+
27
+ Renders horizontal gridlines separately from the axis itself. This separation allows gridlines to be drawn behind the visualization while the axis is drawn on top.
28
+
29
+ ### BottomAxis
30
+ **File:** `BottomAxis.tsx`
31
+
32
+ Renders the bottom (X) axis. Handles:
33
+ - Date/time formatting
34
+ - Tick rotation for responsive layouts
35
+ - Manual step configuration
36
+ - Brush integration
37
+
38
+ **Props:**
39
+ - `xScale` - D3 scale for X axis
40
+ - `yMax` - Chart height
41
+ - `xTickCount` - Number of ticks
42
+ - `handleBottomTickFormatting` - Tick format function
43
+ - `useDateSpanMonths` - Date range calculation flag
44
+
45
+ ### PairedBarAxis
46
+ **File:** `PairedBarAxis.tsx`
47
+
48
+ Specialized axis for Paired Bar charts with two mirrored AxisBottom components. Handles:
49
+ - Dual scale rendering (g1xScale, g2xScale)
50
+ - Responsive tick rotation
51
+ - Tick overlap detection
52
+
53
+ ### RightAxis
54
+ **File:** `RightAxis.tsx`
55
+
56
+ Renders the right (Y) axis for dual-axis charts. Handles:
57
+ - Secondary Y scale
58
+ - Configurable tick/label colors
59
+ - Optional gridlines from right axis
60
+
61
+ ### CategoricalYAxis
62
+ **File:** `Categorical.Axis.tsx`
63
+
64
+ Specialized Y axis for categorical data types.
65
+
66
+ ## Constants
67
+ **File:** `axis.constants.ts`
68
+
69
+ Shared constants used across axis components.
70
+
71
+ ## Usage
72
+
73
+ All components are exported from `index.ts`:
74
+
75
+ ```tsx
76
+ import {
77
+ LeftAxis,
78
+ LeftAxisGridlines,
79
+ BottomAxis,
80
+ PairedBarAxis,
81
+ RightAxis,
82
+ CategoricalYAxis
83
+ } from './Axis'
84
+ ```
85
+
86
+ ## Architecture Notes
87
+
88
+ These components were extracted from `LinearChart.tsx` as part of a refactoring effort to:
89
+ 1. Reduce the main component from 1,704 to ~845 lines
90
+ 2. Improve testability through smaller, focused components
91
+ 3. Enable reuse across different chart types
92
+ 4. Simplify maintenance and debugging
93
+
94
+ Each component accesses the `ConfigContext` for chart configuration rather than receiving all config as props, keeping the prop interfaces focused on rendering-specific data.
@@ -0,0 +1,108 @@
1
+ import React, { useContext } from 'react'
2
+ import { AxisRight as VisxAxisRight } from '@visx/axis'
3
+ import { Group } from '@visx/group'
4
+ import { Line } from '@visx/shape'
5
+ import { Text } from '@visx/text'
6
+ import { ScaleLinear } from 'd3-scale'
7
+
8
+ import ConfigContext from '../../ConfigContext'
9
+
10
+ // Constants
11
+ const HORIZONTAL_TICK_OFFSET_ADJUSTMENT = 5
12
+
13
+ type RightAxisProps = {
14
+ yScaleRight: ScaleLinear<number, number>
15
+ yMax: number
16
+ xMax: number
17
+ yAxisWidth: number
18
+ tickLabelFontSize: number
19
+ axisLabelFontSize: number
20
+ }
21
+
22
+ /**
23
+ * Right Y-axis component for dual-axis charts.
24
+ * Renders a secondary y-axis on the right side with configurable styling.
25
+ * Extracted from LinearChart.tsx
26
+ */
27
+ export const RightAxis: React.FC<RightAxisProps> = ({
28
+ yScaleRight,
29
+ yMax,
30
+ xMax,
31
+ yAxisWidth,
32
+ tickLabelFontSize,
33
+ axisLabelFontSize
34
+ }) => {
35
+ const { config, formatNumber } = useContext(ConfigContext)
36
+ const { runtime } = config
37
+
38
+ const horizontalTickOffset = (ticks: any[]) =>
39
+ yMax / ticks.length / 2 - (yMax / ticks.length) * (1 - config.barThickness) + HORIZONTAL_TICK_OFFSET_ADJUSTMENT
40
+
41
+ return (
42
+ <VisxAxisRight
43
+ scale={yScaleRight}
44
+ left={yAxisWidth + xMax}
45
+ label={config.yAxis.rightLabel}
46
+ tickFormat={tick => formatNumber(tick, 'right')}
47
+ numTicks={runtime.yAxis.rightNumTicks || undefined}
48
+ labelOffset={45}
49
+ >
50
+ {props => {
51
+ const axisCenter =
52
+ config.orientation === 'horizontal'
53
+ ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
54
+ : (props.axisFromPoint.y - props.axisToPoint.y) / 2
55
+
56
+ return (
57
+ <Group className='right-axis'>
58
+ {props.ticks.map((tick, i) => (
59
+ <Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
60
+ {!runtime.yAxis.rightHideTicks && (
61
+ <Line
62
+ from={tick.from}
63
+ to={tick.to}
64
+ display={runtime.horizontal ? 'none' : 'block'}
65
+ stroke={config.yAxis.rightAxisTickColor}
66
+ />
67
+ )}
68
+
69
+ {runtime.yAxis.rightGridLines && (
70
+ <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='#d6d6d6' />
71
+ )}
72
+
73
+ {!config.yAxis.rightHideLabel && (
74
+ <Text
75
+ x={tick.to.x}
76
+ y={tick.to.y + (runtime.horizontal ? horizontalTickOffset(props.ticks) : 0)}
77
+ verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
78
+ textAnchor='start'
79
+ fill={config.yAxis.rightAxisTickLabelColor}
80
+ fontSize={tickLabelFontSize}
81
+ >
82
+ {tick.formattedValue}
83
+ </Text>
84
+ )}
85
+ </Group>
86
+ ))}
87
+
88
+ {!config.yAxis.rightHideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
89
+
90
+ <Text
91
+ className='y-label'
92
+ textAnchor='middle'
93
+ verticalAnchor='start'
94
+ transform={`translate(${config.yAxis.rightLabelOffsetSize || 0}, ${axisCenter}) rotate(-90)`}
95
+ fontWeight='bold'
96
+ fill={config.yAxis.rightAxisLabelColor}
97
+ fontSize={axisLabelFontSize}
98
+ >
99
+ {props.label}
100
+ </Text>
101
+ </Group>
102
+ )
103
+ }}
104
+ </VisxAxisRight>
105
+ )
106
+ }
107
+
108
+ export default RightAxis
@@ -0,0 +1,21 @@
1
+ // Tick length constants
2
+ export const DEFAULT_TICK_LENGTH = 8
3
+ export const LOGARITHMIC_TICK_LENGTH = 6
4
+ export const MAJOR_LOG_TICK_LENGTH = 7
5
+
6
+ // Tick styling constants
7
+ export const TICK_LABEL_MARGIN_RIGHT = 4.5
8
+ export const MAJOR_LOG_TICK_STROKE_WIDTH = 1.3
9
+
10
+ // Label positioning constants
11
+ export const VALUE_ON_LINE_PADDING_NO_AXIS = -8
12
+ export const VALUE_ON_LINE_PADDING_WITH_AXIS = -12
13
+ export const LABEL_Y_PADDING_ABOVE_GRIDLINES = 4
14
+ export const HORIZONTAL_TICK_OFFSET_ADJUSTMENT = 5
15
+
16
+ // Chart-specific constants
17
+ export const ZERO_LINE_STROKE_WIDTH = 2
18
+ export const BAR_MIN_HEIGHT = 15
19
+
20
+ // Lollipop chart sizes
21
+ export const LOLLIPOP_SIZES = { large: 7, medium: 6, small: 5 } as const
@@ -0,0 +1,7 @@
1
+ export { default as CategoricalYAxis } from './Categorical.Axis'
2
+ export { default as LeftAxis } from './LeftAxis'
3
+ export { default as LeftAxisGridlines } from './LeftAxisGridlines'
4
+ export { default as BottomAxis } from './BottomAxis'
5
+ export { default as PairedBarAxis } from './PairedBarAxis'
6
+ export { default as RightAxis } from './RightAxis'
7
+ export * from './axis.constants'
@@ -18,6 +18,7 @@ type BarChartProps = {
18
18
  seriesScale: PositionScale
19
19
  xMax: number
20
20
  yMax: number
21
+ yAxisWidth?: number
21
22
  handleTooltipMouseOver: MouseEventHandler<SVGRectElement>
22
23
  handleTooltipMouseOff: MouseEventHandler<SVGRectElement>
23
24
  handleTooltipClick: MouseEventHandler<SVGRectElement>
@@ -29,6 +30,7 @@ const BarChart: React.FC<BarChartProps> = ({
29
30
  seriesScale,
30
31
  xMax,
31
32
  yMax,
33
+ yAxisWidth,
32
34
  handleTooltipMouseOver,
33
35
  handleTooltipMouseOff,
34
36
  handleTooltipClick
@@ -47,10 +49,14 @@ const BarChart: React.FC<BarChartProps> = ({
47
49
  barChart
48
50
  }
49
51
 
52
+ // Use yAxisWidth prop if provided (for horizontal bar charts with dynamic labels)
53
+ // otherwise fall back to config value
54
+ const leftOffset = yAxisWidth ?? parseFloat(config.runtime.yAxis.size)
55
+
50
56
  return (
51
57
  <ErrorBoundary component='BarChart'>
52
58
  <BarChartContext.Provider value={contextValues}>
53
- <Group left={parseFloat(config.runtime.yAxis.size)}>
59
+ <Group left={leftOffset}>
54
60
  <BarChartType.StackedVertical />
55
61
  <BarChartType.StackedHorizontal />
56
62
  <BarChartType.Vertical />
@@ -2,9 +2,11 @@ import React, { FC, useContext, useMemo, memo, useRef, useEffect, useState, useC
2
2
  import { Brush } from '@visx/brush'
3
3
  import BaseBrush from '@visx/brush/lib/BaseBrush'
4
4
  import { Group } from '@visx/group'
5
+ import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
5
6
  import { scaleBand, scaleLinear } from '@visx/scale'
6
7
  import { Bounds } from '@visx/brush/lib/types'
7
8
  import type { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'
9
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
8
10
  import ConfigContext, { ChartDispatchContext } from '../../ConfigContext'
9
11
  import MiniChartPreview from './MiniChartPreview'
10
12
 
@@ -110,11 +112,77 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
110
112
  const selectionRef = useRef<HTMLButtonElement>(null)
111
113
  const rightHandleRef = useRef<HTMLButtonElement>(null)
112
114
 
113
- const { tableData, config, colorScale } = useContext(ConfigContext)
115
+ const { tableData, config, colorScale, parseDate } = useContext(ConfigContext)
114
116
  const dispatch = useContext(ChartDispatchContext)
115
117
  const dataKey = config.xAxis.dataKey
116
118
  const series = config.series || []
117
119
 
120
+ const renderPatternDefs = () => {
121
+ if (!config.legend?.patterns || Object.keys(config.legend.patterns).length === 0) {
122
+ return null
123
+ }
124
+
125
+ return (
126
+ <>
127
+ {Object.entries(config.legend.patterns).map(([key, pattern]) => {
128
+ const patternId = `chart-pattern-${key}`
129
+ const size = pattern.patternSize || 8
130
+
131
+ switch (pattern.shape) {
132
+ case 'circles':
133
+ return (
134
+ <PatternCircles
135
+ key={patternId}
136
+ id={patternId}
137
+ height={size}
138
+ width={size}
139
+ fill={pattern.color}
140
+ radius={1.25}
141
+ />
142
+ )
143
+ case 'lines':
144
+ return (
145
+ <PatternLines
146
+ key={patternId}
147
+ id={patternId}
148
+ height={size}
149
+ width={size}
150
+ stroke={pattern.color}
151
+ strokeWidth={0.75}
152
+ orientation={['horizontal']}
153
+ />
154
+ )
155
+ case 'diagonalLines':
156
+ return (
157
+ <PatternLines
158
+ key={patternId}
159
+ id={patternId}
160
+ height={size}
161
+ width={size}
162
+ stroke={pattern.color}
163
+ strokeWidth={0.75}
164
+ orientation={['diagonalRightToLeft']}
165
+ />
166
+ )
167
+ case 'waves':
168
+ return (
169
+ <PatternWaves
170
+ key={patternId}
171
+ id={patternId}
172
+ height={size}
173
+ width={size}
174
+ fill={pattern.color}
175
+ strokeWidth={0.25}
176
+ />
177
+ )
178
+ default:
179
+ return null
180
+ }
181
+ })}
182
+ </>
183
+ )
184
+ }
185
+
118
186
  // Capture initial brush extent after mount and sync accessible extent
119
187
  useEffect(() => {
120
188
  if (brushRef.current && brushRef.current.state.extent.x0 !== -1) {
@@ -133,8 +201,22 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
133
201
  return scaleBand<string>({ domain: [], range: [0, Math.max(xMax, 0)] })
134
202
  }
135
203
 
136
- const mappedDates = tableData.map(row => row[dataKey])
137
- const domain = config?.xAxis?.sortByRecentDate ? [...mappedDates].reverse() : mappedDates
204
+ const mappedValues = tableData.map(row => row[dataKey])
205
+
206
+ // Sort domain chronologically for date types, matching the main chart's sort behavior.
207
+ // Without this, data arriving in reverse chronological order renders the brush backwards.
208
+ const xAxisType = config?.xAxis?.type
209
+ let domain: string[]
210
+ if (xAxisType === 'date' || xAxisType === 'date-time') {
211
+ const sorted = [...mappedValues].sort((a, b) => {
212
+ const dateA = parseDate ? parseDate(a) : new Date(a)
213
+ const dateB = parseDate ? parseDate(b) : new Date(b)
214
+ return dateA - dateB
215
+ })
216
+ domain = config?.xAxis?.sortByRecentDate ? sorted.reverse() : sorted
217
+ } else {
218
+ domain = config?.xAxis?.sortByRecentDate ? [...mappedValues].reverse() : mappedValues
219
+ }
138
220
 
139
221
  return scaleBand<string>({
140
222
  domain,
@@ -142,30 +224,82 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
142
224
  paddingInner: 0.1,
143
225
  paddingOuter: 0.1
144
226
  })
145
- }, [tableData, dataKey, config?.xAxis?.sortByRecentDate, xMax])
227
+ }, [tableData, dataKey, config?.xAxis?.sortByRecentDate, config?.xAxis?.type, parseDate, xMax])
146
228
 
147
229
  // Simple Y scale for brush (identity mapping)
148
230
  const yScale = useMemo(() => scaleLinear<number>({ domain: [0, BRUSH_HEIGHT], range: [BRUSH_HEIGHT, 0] }), [])
149
231
 
150
- // Mini chart Y scale
232
+ // Helper to build a mini Y scale from a subset of series
233
+ const buildMiniYScale = useCallback(
234
+ (seriesSubset: typeof series, includeZero: boolean) => {
235
+ const defaultScale = scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
236
+ if (!seriesSubset.length || !tableData.length) return defaultScale
237
+
238
+ let minValue = Number.POSITIVE_INFINITY
239
+ let maxValue = Number.NEGATIVE_INFINITY
240
+ let hasValidValues = false
241
+
242
+ for (const s of seriesSubset) {
243
+ if (!s.dataKey) continue
244
+ for (const row of tableData) {
245
+ const value = parseFloat(row[s.dataKey])
246
+ if (!isNaN(value) && isFinite(value)) {
247
+ hasValidValues = true
248
+ minValue = Math.min(minValue, value)
249
+ maxValue = Math.max(maxValue, value)
250
+ }
251
+ }
252
+ }
253
+
254
+ if (!hasValidValues) return defaultScale
255
+
256
+ if (includeZero) minValue = Math.min(0, minValue)
257
+
258
+ if (minValue === maxValue) {
259
+ const padding = Math.abs(minValue) * 0.1 || 10
260
+ minValue = minValue - padding
261
+ maxValue = maxValue + padding
262
+ if (minValue > 0) minValue = 0
263
+ }
264
+
265
+ const domain = [minValue, maxValue]
266
+ return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
267
+ },
268
+ [tableData]
269
+ )
270
+
271
+ // Determine if we have a right-axis series (dual-axis combo)
272
+ const hasRightAxis = useMemo(
273
+ () => config.visualizationType === 'Combo' && series.some(s => s.axis === 'Right'),
274
+ [series, config.visualizationType]
275
+ )
276
+
277
+ // Mini chart Y scale — left-axis (or all series when there's no right axis)
151
278
  const miniYScale = useMemo(() => {
152
279
  if (!series.length || !tableData.length) {
153
280
  return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
154
281
  }
155
282
 
283
+ const barSeriesTypes = new Set(['Bar', 'Paired Bar', 'Deviation Bar', 'Combo'])
284
+ const hasBarSeries =
285
+ config.visualizationType === 'Bar' ||
286
+ (config.visualizationType === 'Combo' && series.some(s => barSeriesTypes.has(s.type)))
156
287
  const isStacked =
157
288
  config.visualizationSubType === 'stacked' &&
158
289
  (config.visualizationType === 'Bar' || config.visualizationType === 'Area Chart')
290
+
291
+ // When dual-axis, only use left-axis series for this scale
292
+ const leftSeries = hasRightAxis ? series.filter(s => s.axis !== 'Right') : series
293
+
159
294
  let minValue = Number.POSITIVE_INFINITY
160
295
  let maxValue = Number.NEGATIVE_INFINITY
161
296
  let hasValidValues = false
162
297
 
163
298
  if (isStacked) {
164
- // For stacked bars, calculate the sum of all series for each row
165
299
  for (const row of tableData) {
166
300
  let rowSum = 0
167
301
  let hasRowValue = false
168
- for (const s of series) {
302
+ for (const s of leftSeries) {
169
303
  if (!s.dataKey) continue
170
304
  const value = parseFloat(row[s.dataKey])
171
305
  if (!isNaN(value) && isFinite(value)) {
@@ -179,11 +313,9 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
179
313
  maxValue = Math.max(maxValue, rowSum)
180
314
  }
181
315
  }
182
- // For stacked charts, ensure domain starts at 0
183
316
  minValue = Math.min(0, minValue)
184
317
  } else {
185
- // For non-stacked charts, use individual series values
186
- for (const s of series) {
318
+ for (const s of leftSeries) {
187
319
  if (!s.dataKey) continue
188
320
  for (const row of tableData) {
189
321
  const value = parseFloat(row[s.dataKey])
@@ -194,19 +326,13 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
194
326
  }
195
327
  }
196
328
  }
197
- // For bar charts, ensure domain includes 0
198
- if (config.visualizationType === 'Bar') {
199
- minValue = Math.min(0, minValue)
200
- }
329
+ if (hasBarSeries) minValue = Math.min(0, minValue)
201
330
  }
202
331
 
203
- // Handle edge case where all values are the same
204
332
  if (hasValidValues && minValue === maxValue) {
205
- // Create a domain with some padding around the single value
206
333
  const padding = Math.abs(minValue) * 0.1 || 10
207
334
  minValue = minValue - padding
208
335
  maxValue = maxValue + padding
209
- // Ensure 0 is included if we're near it
210
336
  if (minValue > 0) minValue = 0
211
337
  }
212
338
 
@@ -214,14 +340,18 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
214
340
  return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
215
341
  }
216
342
 
217
- // Ensure domain includes 0 for bar charts
218
- if (config.visualizationType === 'Bar') {
219
- minValue = Math.min(0, minValue)
220
- }
343
+ if (hasBarSeries) minValue = Math.min(0, minValue)
221
344
 
222
345
  const domain = minValue === maxValue ? [minValue - 1, maxValue + 1] : [minValue, maxValue]
223
346
  return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
224
- }, [series, tableData, config.visualizationSubType, config.visualizationType])
347
+ }, [series, tableData, config.visualizationSubType, config.visualizationType, hasRightAxis])
348
+
349
+ // Mini chart Y scale for right-axis series (dual-axis combo charts)
350
+ const miniRightYScale = useMemo(() => {
351
+ if (!hasRightAxis) return undefined
352
+ const rightSeries = series.filter(s => s.axis === 'Right')
353
+ return buildMiniYScale(rightSeries, false)
354
+ }, [hasRightAxis, series, buildMiniYScale])
225
355
 
226
356
  // Fallback: Window mouseup listener to prevent stuck drag states
227
357
  useEffect(() => {
@@ -1107,6 +1237,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
1107
1237
  shapeRendering='auto'
1108
1238
  />
1109
1239
  </pattern>
1240
+ {renderPatternDefs()}
1110
1241
  </defs>
1111
1242
 
1112
1243
  {/* Mini chart preview */}
@@ -1118,6 +1249,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
1118
1249
  dataKey={dataKey}
1119
1250
  xScale={xScale}
1120
1251
  miniYScale={miniYScale}
1252
+ miniRightYScale={miniRightYScale}
1121
1253
  colorScale={colorScale}
1122
1254
  config={config}
1123
1255
  xMax={safeXMax}