@cdc/chart 4.26.1 → 4.26.3

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 (173) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  4. package/dist/cdcchart.js +54742 -49796
  5. package/examples/data/data-with-metadata.json +10 -0
  6. package/examples/default.json +378 -0
  7. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  8. package/examples/feature/annotations/index.json +3 -6
  9. package/examples/feature/horizon/horizon-chart.json +395 -0
  10. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  11. package/examples/line-chart-states.json +1085 -0
  12. package/examples/metadata-variables.json +58 -0
  13. package/examples/private/123.json +694 -0
  14. package/examples/private/anchor-issue.json +4094 -0
  15. package/examples/private/backwards-slider.json +10430 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/timeline-data.json +1 -0
  18. package/examples/private/timeline.json +389 -0
  19. package/examples/radar-chart-simple.json +133 -0
  20. package/examples/radar-chart.json +148 -0
  21. package/index.html +1 -31
  22. package/package.json +57 -59
  23. package/src/CdcChart.tsx +8 -4
  24. package/src/CdcChartComponent.tsx +398 -284
  25. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  26. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  27. package/src/_stories/Chart.CI.stories.tsx +13 -0
  28. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  29. package/src/_stories/Chart.CustomColors.stories.tsx +78 -0
  30. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  31. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  32. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  33. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  34. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  35. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  36. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  37. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  38. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  39. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  40. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  41. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  42. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  43. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  44. package/src/_stories/Chart.stories.tsx +72 -1
  45. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  46. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  47. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  48. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  49. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  50. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  51. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  52. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  53. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  54. package/src/_stories/ChartBrush.stories.tsx +7 -0
  55. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  56. package/src/_stories/ChartEditor.stories.tsx +7 -0
  57. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  58. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  59. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  60. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  61. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  62. package/src/_stories/_mock/brush_continuous.json +86 -0
  63. package/src/_stories/_mock/brush_date_large.json +176 -0
  64. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  65. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  66. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  67. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  68. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  69. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  70. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  71. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  72. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  73. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  74. package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
  75. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  76. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  77. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  78. package/src/components/Annotations/components/AnnotationList.styles.css +12 -18
  79. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  80. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  81. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  82. package/src/components/Axis/BottomAxis.tsx +277 -0
  83. package/src/components/Axis/LeftAxis.tsx +404 -0
  84. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  85. package/src/components/Axis/PairedBarAxis.tsx +192 -0
  86. package/src/components/Axis/README.md +94 -0
  87. package/src/components/Axis/RightAxis.tsx +108 -0
  88. package/src/components/Axis/axis.constants.ts +21 -0
  89. package/src/components/Axis/index.ts +7 -0
  90. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  91. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  92. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  93. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  94. package/src/components/BarChart/components/BarChart.tsx +7 -1
  95. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  96. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  97. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  98. package/src/components/Brush/BrushSelector.tsx +155 -22
  99. package/src/components/Brush/MiniChartPreview.tsx +133 -21
  100. package/src/components/EditorPanel/EditorPanel.tsx +81 -54
  101. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
  102. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  103. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
  104. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
  105. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  106. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
  107. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
  108. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  109. package/src/components/EditorPanel/editor-panel.scss +1 -1
  110. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  111. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  112. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  113. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  114. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  115. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  116. package/src/components/HorizonChart/index.tsx +3 -0
  117. package/src/components/Legend/Legend.Component.tsx +52 -4
  118. package/src/components/Legend/Legend.tsx +1 -1
  119. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  120. package/src/components/Legend/LegendValueRange.tsx +77 -0
  121. package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
  122. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  123. package/src/components/LineChart/helpers/README.md +292 -0
  124. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  125. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  126. package/src/components/LineChart/index.tsx +44 -8
  127. package/src/components/LinearChart/README.md +109 -0
  128. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  129. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  130. package/src/components/LinearChart/tests/LinearChart.test.tsx +278 -0
  131. package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
  132. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  133. package/src/components/LinearChart.tsx +268 -1057
  134. package/src/components/PieChart/PieChart.tsx +20 -5
  135. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  136. package/src/components/RadarChart/RadarChart.tsx +298 -0
  137. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  138. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  139. package/src/components/RadarChart/helpers.ts +83 -0
  140. package/src/components/RadarChart/index.tsx +3 -0
  141. package/src/components/Regions/components/Regions.tsx +6 -6
  142. package/src/components/Sankey/components/Sankey.tsx +3 -3
  143. package/src/components/Sankey/sankey.scss +1 -1
  144. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  145. package/src/components/Sparkline/index.scss +4 -2
  146. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  147. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  148. package/src/data/initial-state.js +37 -15
  149. package/src/data/legacy-defaults.ts +18 -0
  150. package/src/helpers/abbreviateNumber.ts +24 -17
  151. package/src/helpers/getChartPatternId.ts +17 -0
  152. package/src/helpers/getExcludedData.ts +4 -0
  153. package/src/helpers/getMinMax.ts +16 -2
  154. package/src/helpers/handleChartAriaLabels.ts +19 -19
  155. package/src/helpers/handleLineType.ts +22 -18
  156. package/src/helpers/seriesColumnSettings.ts +114 -0
  157. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  158. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  159. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  160. package/src/hooks/useRightAxis.ts +14 -0
  161. package/src/hooks/useScales.ts +99 -56
  162. package/src/hooks/useTooltip.tsx +23 -3
  163. package/src/scss/main.scss +157 -79
  164. package/src/selectors/README.md +68 -0
  165. package/src/store/chart.reducer.ts +2 -0
  166. package/src/test/CdcChart.test.jsx +2 -2
  167. package/src/types/ChartConfig.ts +22 -0
  168. package/src/types/ChartContext.ts +1 -0
  169. package/src/types/Horizon.ts +64 -0
  170. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  171. package/tests/fixtures/data-with-metadata.json +10 -0
  172. package/preview.html +0 -1616
  173. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -0,0 +1,134 @@
1
+ import { getPatternUrl } from '../getPatternUrl'
2
+ import { getChartPatternId } from '../../../../helpers/getChartPatternId'
3
+
4
+ describe('getPatternUrl', () => {
5
+ const pattern1Url = `url(#${getChartPatternId('Pattern1')})`
6
+ const pattern2Url = `url(#${getChartPatternId('Pattern2')})`
7
+
8
+ it('matches specific series patterns by series key and value', () => {
9
+ const patternUrl = getPatternUrl({
10
+ patterns: {
11
+ Pattern1: { dataKey: 'y1', dataValue: '19000' }
12
+ },
13
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
14
+ seriesKey: 'y1',
15
+ seriesValue: 19000,
16
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
17
+ })
18
+
19
+ expect(patternUrl).toBe(pattern1Url)
20
+ })
21
+
22
+ it('matches specific non-series field patterns', () => {
23
+ const patternUrl = getPatternUrl({
24
+ patterns: {
25
+ Pattern1: { dataKey: 'category', dataValue: 'Q1' }
26
+ },
27
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
28
+ seriesKey: 'y2',
29
+ seriesValue: 47000,
30
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
31
+ })
32
+
33
+ expect(patternUrl).toBe(pattern1Url)
34
+ })
35
+
36
+ it('does not match non-series field patterns when non-series matching is disabled', () => {
37
+ const patternUrl = getPatternUrl({
38
+ patterns: {
39
+ Pattern1: { dataKey: 'category', dataValue: 'Q1' }
40
+ },
41
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
42
+ seriesKey: 'y2',
43
+ seriesValue: 47000,
44
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' },
45
+ allowNonSeriesFieldMatch: false
46
+ })
47
+
48
+ expect(patternUrl).toBeNull()
49
+ })
50
+
51
+ it('matches blank dataKey pattern by value across series', () => {
52
+ const patternUrl = getPatternUrl({
53
+ patterns: {
54
+ Pattern1: { dataKey: '', dataValue: '47000' }
55
+ },
56
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
57
+ seriesKey: 'y2',
58
+ seriesValue: 47000,
59
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
60
+ })
61
+
62
+ expect(patternUrl).toBe(pattern1Url)
63
+ })
64
+
65
+ it('does not match blank dataKey pattern with empty dataValue', () => {
66
+ const patternUrl = getPatternUrl({
67
+ patterns: {
68
+ Pattern1: { dataKey: '', dataValue: '' }
69
+ },
70
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
71
+ seriesKey: 'y2',
72
+ seriesValue: 47000,
73
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
74
+ })
75
+
76
+ expect(patternUrl).toBeNull()
77
+ })
78
+
79
+ it('prioritizes specific match over broad match', () => {
80
+ const patternUrl = getPatternUrl({
81
+ patterns: {
82
+ Pattern1: { dataKey: '', dataValue: '19000' },
83
+ Pattern2: { dataKey: 'y1', dataValue: '19000' }
84
+ },
85
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
86
+ seriesKey: 'y1',
87
+ seriesValue: 19000,
88
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
89
+ })
90
+
91
+ expect(patternUrl).toBe(pattern2Url)
92
+ })
93
+
94
+ it('uses seriesKeys as fallback to identify series keys when seriesLabels are missing', () => {
95
+ const patternUrl = getPatternUrl({
96
+ patterns: {
97
+ Pattern1: { dataKey: 'y1', dataValue: '19000' }
98
+ },
99
+ datum: { category: 'Q1', y1: 19000, y2: 19000 },
100
+ seriesKey: 'y2',
101
+ seriesValue: 19000,
102
+ seriesKeys: ['y1', 'y2']
103
+ })
104
+
105
+ expect(patternUrl).toBeNull()
106
+ })
107
+
108
+ it('sanitizes special-character pattern keys in returned url fragments', () => {
109
+ const patternKey = 'Pattern 1 / @ value'
110
+ const patternUrl = getPatternUrl({
111
+ patterns: {
112
+ [patternKey]: { dataKey: 'y1', dataValue: '19000' }
113
+ },
114
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
115
+ seriesKey: 'y1',
116
+ seriesValue: 19000,
117
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
118
+ })
119
+
120
+ expect(patternUrl).toBe(`url(#${getChartPatternId(patternKey)})`)
121
+ })
122
+
123
+ it('creates distinct ids for keys that sanitize to the same base id', () => {
124
+ const keyA = 'A B'
125
+ const keyB = 'A@B'
126
+
127
+ const idA = getChartPatternId(keyA)
128
+ const idB = getChartPatternId(keyB)
129
+
130
+ expect(idA).not.toBe(idB)
131
+ expect(idA.startsWith('chart-pattern-A-B-')).toBe(true)
132
+ expect(idB.startsWith('chart-pattern-A-B-')).toBe(true)
133
+ })
134
+ })
@@ -6,6 +6,7 @@ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
6
6
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
7
7
  import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
8
8
  import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
9
+ import { getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
9
10
 
10
11
  export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, configContext) => {
11
12
  const {
@@ -51,6 +52,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
51
52
  isBarAndLegendIsolate && seriesHighlight?.length
52
53
  ? seriesHighlight
53
54
  : config.runtime.barSeriesKeys || config.runtime.seriesKeys
55
+ const seriesOwnedColumnNames = getSeriesOwnedColumnNames(config.series)
54
56
 
55
57
  useEffect(() => {
56
58
  if (orientation === 'horizontal' && !config.yAxis.labelPlacement) {
@@ -179,6 +181,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
179
181
  }) || {}
180
182
  Object.keys(columns).forEach(colKeys => {
181
183
  const colConfig = config.columns[colKeys]
184
+ if (seriesOwnedColumnNames.includes(colConfig.name || colKeys)) return
182
185
  if (series && colConfig.series && colConfig.series !== series && !colConfig.tooltips) return
183
186
  const formattingParams = {
184
187
  addColPrefix: config.columns[colKeys].prefix,
@@ -2,10 +2,13 @@ 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'
11
+ import { getChartPatternId } from '../../helpers/getChartPatternId'
9
12
  import MiniChartPreview from './MiniChartPreview'
10
13
 
11
14
  interface BrushSelectorProps {
@@ -110,11 +113,77 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
110
113
  const selectionRef = useRef<HTMLButtonElement>(null)
111
114
  const rightHandleRef = useRef<HTMLButtonElement>(null)
112
115
 
113
- const { tableData, config, colorScale } = useContext(ConfigContext)
116
+ const { tableData, config, colorScale, parseDate } = useContext(ConfigContext)
114
117
  const dispatch = useContext(ChartDispatchContext)
115
118
  const dataKey = config.xAxis.dataKey
116
119
  const series = config.series || []
117
120
 
121
+ const renderPatternDefs = () => {
122
+ if (!config.legend?.patterns || Object.keys(config.legend.patterns).length === 0) {
123
+ return null
124
+ }
125
+
126
+ return (
127
+ <>
128
+ {Object.entries(config.legend.patterns).map(([key, pattern]) => {
129
+ const patternId = getChartPatternId(key)
130
+ const size = pattern.patternSize || 8
131
+
132
+ switch (pattern.shape) {
133
+ case 'circles':
134
+ return (
135
+ <PatternCircles
136
+ key={patternId}
137
+ id={patternId}
138
+ height={size}
139
+ width={size}
140
+ fill={pattern.color}
141
+ radius={1.25}
142
+ />
143
+ )
144
+ case 'lines':
145
+ return (
146
+ <PatternLines
147
+ key={patternId}
148
+ id={patternId}
149
+ height={size}
150
+ width={size}
151
+ stroke={pattern.color}
152
+ strokeWidth={0.75}
153
+ orientation={['horizontal']}
154
+ />
155
+ )
156
+ case 'diagonalLines':
157
+ return (
158
+ <PatternLines
159
+ key={patternId}
160
+ id={patternId}
161
+ height={size}
162
+ width={size}
163
+ stroke={pattern.color}
164
+ strokeWidth={0.75}
165
+ orientation={['diagonalRightToLeft']}
166
+ />
167
+ )
168
+ case 'waves':
169
+ return (
170
+ <PatternWaves
171
+ key={patternId}
172
+ id={patternId}
173
+ height={size}
174
+ width={size}
175
+ fill={pattern.color}
176
+ strokeWidth={0.25}
177
+ />
178
+ )
179
+ default:
180
+ return null
181
+ }
182
+ })}
183
+ </>
184
+ )
185
+ }
186
+
118
187
  // Capture initial brush extent after mount and sync accessible extent
119
188
  useEffect(() => {
120
189
  if (brushRef.current && brushRef.current.state.extent.x0 !== -1) {
@@ -133,8 +202,22 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
133
202
  return scaleBand<string>({ domain: [], range: [0, Math.max(xMax, 0)] })
134
203
  }
135
204
 
136
- const mappedDates = tableData.map(row => row[dataKey])
137
- const domain = config?.xAxis?.sortByRecentDate ? [...mappedDates].reverse() : mappedDates
205
+ const mappedValues = tableData.map(row => row[dataKey])
206
+
207
+ // Sort domain chronologically for date types, matching the main chart's sort behavior.
208
+ // Without this, data arriving in reverse chronological order renders the brush backwards.
209
+ const xAxisType = config?.xAxis?.type
210
+ let domain: string[]
211
+ if (xAxisType === 'date' || xAxisType === 'date-time') {
212
+ const sorted = [...mappedValues].sort((a, b) => {
213
+ const dateA = parseDate ? parseDate(a) : new Date(a)
214
+ const dateB = parseDate ? parseDate(b) : new Date(b)
215
+ return dateA - dateB
216
+ })
217
+ domain = config?.xAxis?.sortByRecentDate ? sorted.reverse() : sorted
218
+ } else {
219
+ domain = config?.xAxis?.sortByRecentDate ? [...mappedValues].reverse() : mappedValues
220
+ }
138
221
 
139
222
  return scaleBand<string>({
140
223
  domain,
@@ -142,30 +225,82 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
142
225
  paddingInner: 0.1,
143
226
  paddingOuter: 0.1
144
227
  })
145
- }, [tableData, dataKey, config?.xAxis?.sortByRecentDate, xMax])
228
+ }, [tableData, dataKey, config?.xAxis?.sortByRecentDate, config?.xAxis?.type, parseDate, xMax])
146
229
 
147
230
  // Simple Y scale for brush (identity mapping)
148
231
  const yScale = useMemo(() => scaleLinear<number>({ domain: [0, BRUSH_HEIGHT], range: [BRUSH_HEIGHT, 0] }), [])
149
232
 
150
- // Mini chart Y scale
233
+ // Helper to build a mini Y scale from a subset of series
234
+ const buildMiniYScale = useCallback(
235
+ (seriesSubset: typeof series, includeZero: boolean) => {
236
+ const defaultScale = scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
237
+ if (!seriesSubset.length || !tableData.length) return defaultScale
238
+
239
+ let minValue = Number.POSITIVE_INFINITY
240
+ let maxValue = Number.NEGATIVE_INFINITY
241
+ let hasValidValues = false
242
+
243
+ for (const s of seriesSubset) {
244
+ if (!s.dataKey) continue
245
+ for (const row of tableData) {
246
+ const value = parseFloat(row[s.dataKey])
247
+ if (!isNaN(value) && isFinite(value)) {
248
+ hasValidValues = true
249
+ minValue = Math.min(minValue, value)
250
+ maxValue = Math.max(maxValue, value)
251
+ }
252
+ }
253
+ }
254
+
255
+ if (!hasValidValues) return defaultScale
256
+
257
+ if (includeZero) minValue = Math.min(0, minValue)
258
+
259
+ if (minValue === maxValue) {
260
+ const padding = Math.abs(minValue) * 0.1 || 10
261
+ minValue = minValue - padding
262
+ maxValue = maxValue + padding
263
+ if (minValue > 0) minValue = 0
264
+ }
265
+
266
+ const domain = [minValue, maxValue]
267
+ return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
268
+ },
269
+ [tableData]
270
+ )
271
+
272
+ // Determine if we have a right-axis series (dual-axis combo)
273
+ const hasRightAxis = useMemo(
274
+ () => config.visualizationType === 'Combo' && series.some(s => s.axis === 'Right'),
275
+ [series, config.visualizationType]
276
+ )
277
+
278
+ // Mini chart Y scale — left-axis (or all series when there's no right axis)
151
279
  const miniYScale = useMemo(() => {
152
280
  if (!series.length || !tableData.length) {
153
281
  return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
154
282
  }
155
283
 
284
+ const barSeriesTypes = new Set(['Bar', 'Paired Bar', 'Deviation Bar', 'Combo'])
285
+ const hasBarSeries =
286
+ config.visualizationType === 'Bar' ||
287
+ (config.visualizationType === 'Combo' && series.some(s => barSeriesTypes.has(s.type)))
156
288
  const isStacked =
157
289
  config.visualizationSubType === 'stacked' &&
158
290
  (config.visualizationType === 'Bar' || config.visualizationType === 'Area Chart')
291
+
292
+ // When dual-axis, only use left-axis series for this scale
293
+ const leftSeries = hasRightAxis ? series.filter(s => s.axis !== 'Right') : series
294
+
159
295
  let minValue = Number.POSITIVE_INFINITY
160
296
  let maxValue = Number.NEGATIVE_INFINITY
161
297
  let hasValidValues = false
162
298
 
163
299
  if (isStacked) {
164
- // For stacked bars, calculate the sum of all series for each row
165
300
  for (const row of tableData) {
166
301
  let rowSum = 0
167
302
  let hasRowValue = false
168
- for (const s of series) {
303
+ for (const s of leftSeries) {
169
304
  if (!s.dataKey) continue
170
305
  const value = parseFloat(row[s.dataKey])
171
306
  if (!isNaN(value) && isFinite(value)) {
@@ -179,11 +314,9 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
179
314
  maxValue = Math.max(maxValue, rowSum)
180
315
  }
181
316
  }
182
- // For stacked charts, ensure domain starts at 0
183
317
  minValue = Math.min(0, minValue)
184
318
  } else {
185
- // For non-stacked charts, use individual series values
186
- for (const s of series) {
319
+ for (const s of leftSeries) {
187
320
  if (!s.dataKey) continue
188
321
  for (const row of tableData) {
189
322
  const value = parseFloat(row[s.dataKey])
@@ -194,19 +327,13 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
194
327
  }
195
328
  }
196
329
  }
197
- // For bar charts, ensure domain includes 0
198
- if (config.visualizationType === 'Bar') {
199
- minValue = Math.min(0, minValue)
200
- }
330
+ if (hasBarSeries) minValue = Math.min(0, minValue)
201
331
  }
202
332
 
203
- // Handle edge case where all values are the same
204
333
  if (hasValidValues && minValue === maxValue) {
205
- // Create a domain with some padding around the single value
206
334
  const padding = Math.abs(minValue) * 0.1 || 10
207
335
  minValue = minValue - padding
208
336
  maxValue = maxValue + padding
209
- // Ensure 0 is included if we're near it
210
337
  if (minValue > 0) minValue = 0
211
338
  }
212
339
 
@@ -214,14 +341,18 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
214
341
  return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
215
342
  }
216
343
 
217
- // Ensure domain includes 0 for bar charts
218
- if (config.visualizationType === 'Bar') {
219
- minValue = Math.min(0, minValue)
220
- }
344
+ if (hasBarSeries) minValue = Math.min(0, minValue)
221
345
 
222
346
  const domain = minValue === maxValue ? [minValue - 1, maxValue + 1] : [minValue, maxValue]
223
347
  return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
224
- }, [series, tableData, config.visualizationSubType, config.visualizationType])
348
+ }, [series, tableData, config.visualizationSubType, config.visualizationType, hasRightAxis])
349
+
350
+ // Mini chart Y scale for right-axis series (dual-axis combo charts)
351
+ const miniRightYScale = useMemo(() => {
352
+ if (!hasRightAxis) return undefined
353
+ const rightSeries = series.filter(s => s.axis === 'Right')
354
+ return buildMiniYScale(rightSeries, false)
355
+ }, [hasRightAxis, series, buildMiniYScale])
225
356
 
226
357
  // Fallback: Window mouseup listener to prevent stuck drag states
227
358
  useEffect(() => {
@@ -1107,6 +1238,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
1107
1238
  shapeRendering='auto'
1108
1239
  />
1109
1240
  </pattern>
1241
+ {renderPatternDefs()}
1110
1242
  </defs>
1111
1243
 
1112
1244
  {/* Mini chart preview */}
@@ -1118,6 +1250,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
1118
1250
  dataKey={dataKey}
1119
1251
  xScale={xScale}
1120
1252
  miniYScale={miniYScale}
1253
+ miniRightYScale={miniRightYScale}
1121
1254
  colorScale={colorScale}
1122
1255
  config={config}
1123
1256
  xMax={safeXMax}