@cdc/chart 4.26.3 → 4.26.5

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 (192) hide show
  1. package/CONFIG.md +489 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39024 -36587
  4. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  5. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  6. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  7. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  8. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  9. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  10. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  11. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  12. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  13. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  14. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  15. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  16. package/examples/metadata-variables.json +1 -1
  17. package/examples/minimal-example.json +24 -0
  18. package/examples/private/bump-chart.json +1116 -0
  19. package/examples/private/test-123.json +624 -0
  20. package/examples/private/test2.json +35012 -0
  21. package/package.json +5 -4
  22. package/src/CdcChartComponent.tsx +144 -60
  23. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  24. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  25. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  26. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  27. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  28. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
  29. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  30. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  31. package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
  32. package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
  33. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  34. package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
  35. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  36. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  37. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  38. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  39. package/src/_stories/_mock/heatmap.json +156 -0
  40. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  41. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  42. package/src/_stories/_mock/paired-bar.json +1 -1
  43. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  44. package/src/_stories/_mock/sankey_mock.json +78 -0
  45. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  46. package/src/components/Annotations/components/AnnotationDraggable.tsx +2 -2
  47. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  48. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  49. package/src/components/Axis/BottomAxis.tsx +1 -1
  50. package/src/components/Axis/LeftAxis.tsx +15 -14
  51. package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
  52. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  53. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  54. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  55. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  56. package/src/components/BarChart/components/BarChart.tsx +2 -6
  57. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  58. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  59. package/src/components/Brush/BrushSelector.tsx +88 -187
  60. package/src/components/Brush/brushInteraction.ts +22 -0
  61. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  62. package/src/components/EditorPanel/EditorPanel.tsx +946 -960
  63. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  64. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
  65. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  66. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
  67. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
  68. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  69. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
  70. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  71. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  72. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  73. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  74. package/src/components/EditorPanel/useEditorPermissions.ts +32 -21
  75. package/src/components/Forecasting/Forecasting.tsx +2 -2
  76. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  77. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  78. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  79. package/src/components/HeatMap/heatmap.css +71 -0
  80. package/src/components/HeatMap/helpers/index.ts +261 -0
  81. package/src/components/HeatMap/index.tsx +5 -0
  82. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  83. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  84. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  85. package/src/components/HoverLine/HoverLine.tsx +6 -8
  86. package/src/components/Legend/Legend.Component.tsx +4 -4
  87. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  88. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  89. package/src/components/Legend/helpers/index.ts +0 -2
  90. package/src/components/LineChart/LineChartProps.ts +1 -0
  91. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
  92. package/src/components/LineChart/helpers.test.ts +345 -0
  93. package/src/components/LineChart/helpers.ts +15 -5
  94. package/src/components/LineChart/index.tsx +18 -5
  95. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  96. package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
  97. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  98. package/src/components/LinearChart.tsx +155 -77
  99. package/src/components/PairedBarChart.jsx +2 -2
  100. package/src/components/PieChart/PieChart.tsx +15 -10
  101. package/src/components/RadarChart/RadarChart.tsx +3 -1
  102. package/src/components/Regions/components/Regions.tsx +199 -35
  103. package/src/components/Sankey/components/Sankey.tsx +7 -3
  104. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  105. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  106. package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -3
  107. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  108. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  109. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  110. package/src/components/Sparkline/components/SparkLine.tsx +5 -10
  111. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  112. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  113. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  114. package/src/data/initial-state.js +12 -0
  115. package/src/helpers/axisLabelFontSize.ts +8 -0
  116. package/src/helpers/bumpChartTooltip.ts +54 -0
  117. package/src/helpers/calculateLeftYAxisWidth.ts +197 -0
  118. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  119. package/src/helpers/getMinMax.ts +2 -3
  120. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  121. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  122. package/src/helpers/handleChartAriaLabels.ts +23 -10
  123. package/src/helpers/seriesHighlight.ts +26 -0
  124. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  125. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  126. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  127. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  128. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  129. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  130. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  131. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  132. package/src/helpers/tooltipHelpers.ts +176 -0
  133. package/src/hooks/tests/useScales.test.tsx +105 -0
  134. package/src/hooks/useHighlightedBars.ts +9 -16
  135. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  136. package/src/hooks/useScales.ts +19 -9
  137. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  138. package/src/hooks/useTooltip.tsx +167 -38
  139. package/src/index.jsx +5 -2
  140. package/src/scss/main.scss +32 -49
  141. package/src/test/CdcChart.test.jsx +24 -0
  142. package/src/types/ChartConfig.ts +19 -0
  143. package/src/types/ChartContext.ts +2 -0
  144. package/examples/chart-regression-2.json +0 -2360
  145. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  146. package/examples/feature/__data__/city-temperature.json +0 -2198
  147. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  148. package/examples/feature/boxplot/box-plot-data.json +0 -71
  149. package/examples/feature/boxplot/boxplot-data.json +0 -156
  150. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  151. package/examples/feature/forest-plot/log.json +0 -26
  152. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  153. package/examples/region-issue.json +0 -2065
  154. package/examples/xaxis.json +0 -493
  155. package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
  156. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  157. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  158. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  159. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  160. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  161. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  162. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  163. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  164. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  165. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  166. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  167. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  168. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  169. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  170. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  171. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  172. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  173. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  174. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  175. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  176. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  177. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  178. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  179. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  180. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  181. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  182. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  183. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  184. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  185. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  186. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  187. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  188. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  189. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  190. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  191. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  192. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -35,6 +35,9 @@ import { useTickFormatters } from './LinearChart/utils/tickFormatting'
35
35
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
36
36
  import { calcInitialHeight } from '../helpers/sizeHelpers'
37
37
  import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
38
+ import { calculateLeftYAxisWidth } from '../helpers/calculateLeftYAxisWidth'
39
+ import { getAxisLabelFontSize } from '../helpers/axisLabelFontSize'
40
+ import { getYAxisAutoPaddingMode } from '../helpers/getYAxisAutoPaddingMode'
38
41
 
39
42
  // Hooks
40
43
  import useReduceData from '../hooks/useReduceData'
@@ -51,6 +54,7 @@ import Annotation from './Annotations'
51
54
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
52
55
  import HoverLine from './HoverLine/HoverLine'
53
56
  import { SmallMultiples } from './SmallMultiples'
57
+ import { type TooltipDisplayData } from '../helpers/tooltipHelpers'
54
58
 
55
59
  type LinearChartProps = {
56
60
  parentWidth: number
@@ -62,13 +66,11 @@ const BOTTOM_LABEL_PADDING = 9
62
66
  const X_TICK_LABEL_PADDING = 4.5
63
67
  const DEFAULT_TICK_LENGTH = 8
64
68
  const DEFAULT_MAX_TICK_ROTATION = 90
69
+ const DEFAULT_LEFT_Y_AXIS_WIDTH = 50
65
70
 
66
71
  // Font sizes
67
72
  const TICK_LABEL_FONT_SIZE = 16
68
73
  const TICK_LABEL_FONT_SIZE_SMALL = 13
69
- const AXIS_LABEL_FONT_SIZE = 18
70
- const AXIS_LABEL_FONT_SIZE_SMALL = 14
71
-
72
74
  // Label positioning constants
73
75
  const BELOW_BAR_TEXT_OFFSET = -6.5
74
76
  const LABEL_PADDING_OFFSET = 8
@@ -88,10 +90,7 @@ const WARMING_STRIPES_HEIGHT = 78
88
90
  // Time constants
89
91
  const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
90
92
 
91
- type TooltipData = {
92
- dataXPosition?: number
93
- dataYPosition?: number
94
- }
93
+ type TooltipData = Partial<TooltipDisplayData>
95
94
 
96
95
  type UseTooltipReturn<T = TooltipData> = {
97
96
  tooltipData: T | null
@@ -122,6 +121,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
122
121
  transformedData: data,
123
122
  } = useContext(ConfigContext)
124
123
 
124
+ // SVG accessibility: title/desc pattern
125
+ const a11y = handleChartAriaLabels(config)
126
+
125
127
  // CONFIG
126
128
  // todo: start destructuring this file for conciseness
127
129
  const { visualizationType, orientation, xAxis, yAxis, runtime, legend, forestPlot, debugSvg } = config
@@ -129,9 +131,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
129
131
  const { inlineLabel } = config.yAxis
130
132
 
131
133
  // HOOKS % STATES
132
- // When brush is active, use tableData (full dataset) for min/max calculation
133
- // so the y-axis shows the full range, but still use filtered data for rendering
134
- const dataForMinMax = config.xAxis.brushActive && tableData && tableData.length > 0 ? tableData : data
134
+ const useBrushFullRange = config.xAxis.brushActive && !config.xAxis.brushDynamicYAxis
135
+ const dataForMinMax = useBrushFullRange && tableData && tableData.length > 0 ? tableData : data
135
136
  const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, dataForMinMax)
136
137
 
137
138
  const { visSupportsSmallMultiples } = useEditorPermissions()
@@ -163,6 +164,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
163
164
  const xAxisLabelRefs = useRef([])
164
165
  const xAxisTitleRef = useRef(null)
165
166
  const tooltipRef = useRef(null)
167
+ const brushLayoutRef = useRef<{ yAxisWidth: number; xMax: number } | null>(null)
168
+ const brushLayoutDepsRef = useRef<{ parentWidth: number; tableData: any } | null>(null)
166
169
 
167
170
  const dataRef = useIntersectionObserver(triggerRef, {
168
171
  freezeOnceVisible: false
@@ -174,12 +177,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
174
177
  const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
175
178
  const isForestPlot = visualizationType === 'Forest Plot'
176
179
  const isDateTime = config.xAxis.type === 'date-time'
177
- const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
178
- const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
179
- const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
180
- const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
180
+ const yAxisAutoPaddingMode = getYAxisAutoPaddingMode(config)
181
181
  const tickLabelFontSize = isMobileFontViewport(vizViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
182
- const axisLabelFontSize = isMobileFontViewport(vizViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
182
+ const axisLabelFontSize = getAxisLabelFontSize(vizViewport)
183
183
  const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
184
184
 
185
185
  // zero if not forest plot
@@ -187,11 +187,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
187
187
 
188
188
  // height before bottom axis
189
189
  const initialHeight = useMemo(
190
- () =>
191
- visualizationType === 'Warming Stripes' ? WARMING_STRIPES_HEIGHT : calcInitialHeight(config, currentViewport),
190
+ () => (visualizationType === 'Warming Stripes' ? WARMING_STRIPES_HEIGHT : calcInitialHeight(config, vizViewport)),
192
191
  [
193
192
  visualizationType,
194
- currentViewport,
193
+ vizViewport,
195
194
  config.heights?.vertical,
196
195
  config.heights?.horizontal,
197
196
  config.heights?.mobileVertical,
@@ -258,15 +257,28 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
258
257
 
259
258
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
260
259
 
261
- // State for computed y-axis width - allows re-render when horizontal bar label space is calculated
262
- const [computedYAxisWidth, setComputedYAxisWidth] = useState<number | null>(null)
263
-
264
- // Use computed width if available, otherwise fall back to config value
265
- const yAxisWidth = computedYAxisWidth ?? Number(runtime.yAxis.size)
260
+ // State for computed left-axis width - shared across all linear-chart types.
261
+ const [currentYAxisWidth, setCurrentYAxisWidth] = useState<number>(DEFAULT_LEFT_Y_AXIS_WIDTH)
262
+ const yAxisWidth = currentYAxisWidth
266
263
 
267
264
  // Chart width calculation using the current y-axis width
268
265
  const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
269
266
 
267
+ // Stabilize brush container dimensions when brushDynamicYAxis is enabled.
268
+ // Without this, y-axis rescaling on each brush change creates a feedback loop:
269
+ // brush drag → y-domain change → tick label width change → xMax shift → brush jumps.
270
+ // We freeze the brush coordinate system and only update on real layout changes.
271
+ const isDynamicBrushYAxis = config.xAxis.brushDynamicYAxis
272
+ const isFirstBrushLayout = !brushLayoutDepsRef.current
273
+ const viewportResized = brushLayoutDepsRef.current?.parentWidth !== parentWidth
274
+ const dataReloaded = brushLayoutDepsRef.current?.tableData !== tableData
275
+ if (!isDynamicBrushYAxis || isFirstBrushLayout || viewportResized || dataReloaded) {
276
+ brushLayoutRef.current = { yAxisWidth, xMax }
277
+ brushLayoutDepsRef.current = { parentWidth, tableData }
278
+ }
279
+ const stableBrushYAxisWidth = isDynamicBrushYAxis ? brushLayoutRef.current!.yAxisWidth : yAxisWidth
280
+ const stableBrushXMax = isDynamicBrushYAxis ? brushLayoutRef.current!.xMax : xMax
281
+
270
282
  const {
271
283
  xScale,
272
284
  yScale,
@@ -289,8 +301,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
289
301
  xAxisDataMapped,
290
302
  yMax,
291
303
  xMax,
292
- needsYAxisAutoPadding,
293
- currentViewport
304
+ yAxisAutoPaddingMode,
305
+ currentViewport: vizViewport
294
306
  })
295
307
 
296
308
  // Consolidated tick formatters
@@ -303,39 +315,78 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
303
315
  shouldAbbreviate
304
316
  })
305
317
 
306
- // Calculate category label space for horizontal bar charts
318
+ // Horizontal charts swap axes at runtime, but the left-side category label visibility
319
+ // still follows config.yAxis.hideLabel to match the actual axis renderers.
307
320
  const categoryLabelSpace = useMemo(() => {
321
+ if (config.yAxis.hideLabel) return 0
322
+
308
323
  return calculateHorizontalBarCategoryLabelWidth({
309
324
  yScale,
310
325
  chartWidth: parentWidth,
311
326
  formatDate,
312
327
  parseDate,
313
328
  tickLabelFont: GET_TEXT_WIDTH_FONT,
314
- xAxisType: config.runtime.xAxis?.type,
329
+ xAxisType: runtime.xAxis?.type,
315
330
  labelPlacement: config.yAxis.labelPlacement
316
331
  })
317
- }, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
318
-
319
- const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
332
+ }, [
333
+ config.yAxis.hideLabel,
334
+ config.yAxis.labelPlacement,
335
+ yScale,
336
+ parentWidth,
337
+ formatDate,
338
+ parseDate,
339
+ GET_TEXT_WIDTH_FONT,
340
+ runtime.xAxis?.type
341
+ ])
320
342
 
321
- // Update y-axis width state when computed value changes (for horizontal bar charts)
322
- useEffect(() => {
323
- if (isHorizontal && config.visualizationType === 'Bar') {
324
- const newWidth = categoryLabelSpace + horizontalYAxisLabelSpace
325
- if (newWidth !== computedYAxisWidth) {
326
- setComputedYAxisWidth(newWidth)
327
- }
328
- } else if (computedYAxisWidth !== null) {
329
- // Reset to null for non-horizontal bar charts so we use config value
330
- setComputedYAxisWidth(null)
331
- }
332
- }, [isHorizontal, config.visualizationType, categoryLabelSpace, horizontalYAxisLabelSpace, computedYAxisWidth])
343
+ const horizontalYAxisLabelSpace =
344
+ runtime.yAxis.label && !config.hideYAxisLabel && config.yAxis.titlePlacement !== 'top' ? 30 : 0
333
345
 
334
346
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
335
- countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
347
+ countNumOfTicks({ axis, max, runtime, currentViewport: vizViewport, isHorizontal, data, config, min })
336
348
  )
337
349
  const handleNumTicks = isForestPlot ? config.data.length : yTickCount
338
350
 
351
+ const calculatedYAxisWidth = useMemo(() => {
352
+ if (isHorizontal && config.visualizationType === 'Bar') {
353
+ return categoryLabelSpace + horizontalYAxisLabelSpace
354
+ }
355
+
356
+ return calculateLeftYAxisWidth({
357
+ config,
358
+ data,
359
+ yScale,
360
+ numTicks: handleNumTicks,
361
+ parentWidth,
362
+ tickLabelFont: GET_TEXT_WIDTH_FONT,
363
+ axisLabelFontSize,
364
+ handleLeftTickFormatting,
365
+ categoryLabelSpace,
366
+ horizontalYAxisLabelSpace
367
+ })
368
+ }, [
369
+ config,
370
+ data,
371
+ yScale,
372
+ handleNumTicks,
373
+ parentWidth,
374
+ isHorizontal,
375
+ GET_TEXT_WIDTH_FONT,
376
+ axisLabelFontSize,
377
+ handleLeftTickFormatting,
378
+ categoryLabelSpace,
379
+ horizontalYAxisLabelSpace
380
+ ])
381
+
382
+ useLayoutEffect(() => {
383
+ if (parentWidth <= 0) return
384
+
385
+ if (calculatedYAxisWidth !== currentYAxisWidth) {
386
+ setCurrentYAxisWidth(calculatedYAxisWidth)
387
+ }
388
+ }, [calculatedYAxisWidth, currentYAxisWidth, parentWidth])
389
+
339
390
  // Tooltip Helpers
340
391
 
341
392
  const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } =
@@ -353,6 +404,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
353
404
  xScale,
354
405
  yScale,
355
406
  seriesScale,
407
+ yAxisWidth,
356
408
  showTooltip,
357
409
  hideTooltip,
358
410
  interactionLabel
@@ -381,13 +433,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
381
433
 
382
434
  const getManualStep = useCallback(() => {
383
435
  let manualStep = config.xAxis.manualStep
384
- if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
385
- manualStep = config.xAxis.viewportStepCount[currentViewport]
436
+ if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[vizViewport]) {
437
+ manualStep = config.xAxis.viewportStepCount[vizViewport]
386
438
  }
387
439
  return manualStep
388
- }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
440
+ }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, vizViewport])
389
441
 
390
- const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
442
+ const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, yAxisWidth, getXValueFromCoordinate)
391
443
 
392
444
  const onMouseMove = event => {
393
445
  const svgRect = event.currentTarget.getBoundingClientRect()
@@ -416,7 +468,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
416
468
  const internalSvgRef = useProgrammaticTooltip({
417
469
  svgRef,
418
470
  getCoordinateFromXValue,
471
+ getXValueFromCoordinate,
419
472
  config,
473
+ yAxisWidth,
420
474
  setPoint,
421
475
  setShowHoverLine,
422
476
  handleTooltipMouseOver,
@@ -471,7 +525,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
471
525
 
472
526
  const isForestPlot = visualizationType === 'Forest Plot'
473
527
  const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
474
-
475
528
  // Heights to add
476
529
  const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
477
530
  const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
@@ -568,7 +621,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
568
621
  debugSvg && 'debug'
569
622
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
570
623
  role='img'
571
- aria-label={handleChartAriaLabels(config)}
624
+ aria-label={a11y}
572
625
  style={{ overflow: 'visible' }}
573
626
  onMouseLeave={() => {
574
627
  setShowHoverLine(false)
@@ -589,7 +642,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
589
642
  xMax={xMax}
590
643
  yAxisWidth={yAxisWidth}
591
644
  numTicks={handleNumTicks}
592
- yLabelOffset={yLabelOffset}
593
645
  axisLabelFontSize={axisLabelFontSize}
594
646
  />
595
647
  )}
@@ -673,8 +725,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
673
725
  strokeDasharray={handleLineType(anchor.lineStyle)}
674
726
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
675
727
  className='anchor-y'
676
- from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
677
- to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }}
728
+ from={{ x: Number(yAxisWidth), y: position - middleOffset }}
729
+ to={{ x: Number(yAxisWidth) + Number(xMax), y: position - middleOffset }}
678
730
  />
679
731
  )
680
732
  })}
@@ -710,25 +762,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
710
762
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
711
763
  fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
712
764
  className='anchor-x'
713
- from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
714
- to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
765
+ from={{ x: Number(anchorPosition) + Number(yAxisWidth), y: 0 }}
766
+ to={{ x: Number(anchorPosition) + Number(yAxisWidth), y: yMax }}
715
767
  />
716
768
  )
717
769
  })}
718
770
  {/* we are handling regions in bar charts differently, so that we can calculate the bar group into the region space. */}
719
771
  {/* prettier-ignore */}
720
772
  {config.visualizationType !== 'Bar' && config.visualizationType !== 'Combo' && (
721
- <Regions
722
- xScale={xScale}
723
- handleTooltipClick={handleTooltipClick}
724
- handleTooltipMouseOff={handleTooltipMouseOff}
725
- handleTooltipMouseOver={handleTooltipMouseOver}
726
- showTooltip={showTooltip}
727
- hideTooltip={hideTooltip}
728
- tooltipData={tooltipData}
729
- yMax={yMax}
730
- xMax={xMax}
731
- />
773
+ <Group left={yAxisWidth}>
774
+ <Regions
775
+ xScale={xScale}
776
+ handleTooltipClick={handleTooltipClick}
777
+ handleTooltipMouseOff={handleTooltipMouseOff}
778
+ handleTooltipMouseOver={handleTooltipMouseOver}
779
+ showTooltip={showTooltip}
780
+ hideTooltip={hideTooltip}
781
+ tooltipData={tooltipData}
782
+ yMax={yMax}
783
+ xMax={xMax}
784
+ />
785
+ </Group>
732
786
  )}
733
787
  {isNoDataAvailable && (
734
788
  <Text
@@ -741,8 +795,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
741
795
  )}
742
796
  {showHoverLine && (
743
797
  <>
744
- <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='horizontal' />
745
- <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='vertical' />
798
+ <HoverLine
799
+ xMax={xMax}
800
+ yMax={yMax}
801
+ point={point}
802
+ tooltipData={tooltipData}
803
+ orientation='horizontal'
804
+ yAxisWidth={yAxisWidth}
805
+ />
806
+ <HoverLine
807
+ xMax={xMax}
808
+ yMax={yMax}
809
+ point={point}
810
+ tooltipData={tooltipData}
811
+ orientation='vertical'
812
+ yAxisWidth={yAxisWidth}
813
+ />
746
814
  </>
747
815
  )}
748
816
  <Group left={yAxisWidth}>
@@ -778,7 +846,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
778
846
  suffixWidth={suffixWidth}
779
847
  horizontalYAxisLabelSpace={horizontalYAxisLabelSpace}
780
848
  categoryLabelSpace={categoryLabelSpace}
781
- yLabelOffset={yLabelOffset}
782
849
  />
783
850
  )}
784
851
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
@@ -839,10 +906,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
839
906
  </svg>
840
907
  {!isDraggingAnnotation &&
841
908
  tooltipData &&
842
- Object.entries(tooltipData.data).length > 0 &&
909
+ tooltipData.data?.length > 0 &&
843
910
  tooltipOpen &&
844
911
  showTooltip &&
845
- !tooltipData?.data?.some(subArray => subArray.some(item => item === undefined)) &&
912
+ !tooltipData?.data?.some(row => row?.value === undefined) &&
846
913
  tooltipData.dataYPosition &&
847
914
  tooltipData.dataXPosition &&
848
915
  !config.tooltips.singleSeries && (
@@ -860,16 +927,23 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
860
927
  >
861
928
  <ul>
862
929
  {typeof tooltipData === 'object' &&
863
- Object.entries(tooltipData.data)
864
- .filter(([_, values]) => Array.isArray(values) && !values.includes(undefined))
865
- .map((item, index) => <TooltipListItem item={item} key={index} />)}
930
+ tooltipData.data
931
+ .filter(row => row && row.value !== undefined)
932
+ .map((row, index) => (
933
+ <TooltipListItem
934
+ row={row}
935
+ index={index}
936
+ key={index}
937
+ useMarkerColumn={tooltipData.useMarkerColumn}
938
+ />
939
+ ))}
866
940
  </ul>
867
941
  </TooltipWithBounds>
868
942
  </>
869
943
  )}
870
944
  {config.visualizationType === 'Bump Chart' && (
871
945
  <ReactTooltip
872
- id={`bump-chart`}
946
+ id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}-bump`}
873
947
  variant='light'
874
948
  arrowColor='rgba(0,0,0,0)'
875
949
  className='tooltip'
@@ -892,8 +966,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
892
966
  style={{
893
967
  position: 'relative',
894
968
  marginTop: `${BRUSH_MARGIN}px`,
895
- left: `${yAxisWidth}px`,
896
- width: `${Math.max(xMax, BRUSH_MIN_WIDTH)}px`,
969
+ left: `${stableBrushYAxisWidth}px`,
970
+ width: `${Math.max(stableBrushXMax, BRUSH_MIN_WIDTH)}px`,
897
971
  height: `${BRUSH_HEIGHT}px`,
898
972
  pointerEvents: 'auto',
899
973
  zIndex: 15,
@@ -904,7 +978,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
904
978
  }}
905
979
  className='brush-overlay'
906
980
  >
907
- <BrushSelector key={brushKeyRef.current} xMax={Math.max(xMax, BRUSH_MIN_WIDTH)} yMax={BRUSH_HEIGHT} />
981
+ <BrushSelector
982
+ key={brushKeyRef.current}
983
+ xMax={Math.max(stableBrushXMax, BRUSH_MIN_WIDTH)}
984
+ yMax={BRUSH_HEIGHT}
985
+ />
908
986
  </div>
909
987
  )}
910
988
  </div>
@@ -10,7 +10,7 @@ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
10
10
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
11
11
  import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
12
12
 
13
- const PairedBarChart = ({ width, height, originalWidth }) => {
13
+ const PairedBarChart = ({ width, height, originalWidth, yAxisWidth }) => {
14
14
  const {
15
15
  config,
16
16
  colorScale,
@@ -93,7 +93,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
93
93
  id='cdc-visualization__paired-bar-chart'
94
94
  width={originalWidth}
95
95
  height={height}
96
- viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`}
96
+ viewBox={`0 0 ${width + yAxisWidth} ${height}`}
97
97
  role='img'
98
98
  tabIndex={0}
99
99
  >
@@ -24,14 +24,9 @@ import { handleChartAriaLabels } from '../../helpers/handleChartAriaLabels'
24
24
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
25
25
  import { scaleOrdinal } from '@visx/scale'
26
26
  import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
27
+ import { type TooltipDisplayData } from '../../helpers/tooltipHelpers'
27
28
 
28
- type TooltipData = {
29
- data: {
30
- [key: string]: string | number
31
- }
32
- dataXPosition: number
33
- dataYPosition: number
34
- }
29
+ type TooltipData = TooltipDisplayData
35
30
 
36
31
  type PieChartProps = {
37
32
  parentWidth?: number
@@ -49,6 +44,9 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
49
44
  seriesHighlight,
50
45
  isDraggingAnnotation
51
46
  } = useContext(ConfigContext)
47
+
48
+ const a11y = handleChartAriaLabels(config)
49
+
52
50
  const dispatch = useContext(ChartDispatchContext)
53
51
  const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } = useTooltip<TooltipData>()
54
52
  const { handleTooltipMouseOver, handleTooltipMouseOff, TooltipListItem } = useCoveTooltip({
@@ -395,7 +393,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
395
393
  height={height}
396
394
  className={getSvgClasses()}
397
395
  role='img'
398
- aria-label={handleChartAriaLabels(config)}
396
+ aria-label={a11y}
399
397
  onMouseEnter={handleChartMouseEnter}
400
398
  onMouseLeave={() => {
401
399
  handleTooltipMouseOff()
@@ -426,7 +424,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
426
424
  <div ref={triggerRef} />
427
425
  {!isDraggingAnnotation &&
428
426
  tooltipData &&
429
- Object.entries(tooltipData.data).length > 0 &&
427
+ tooltipData.data?.length > 0 &&
430
428
  tooltipOpen &&
431
429
  showTooltip &&
432
430
  tooltipData.dataYPosition &&
@@ -442,7 +440,14 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
442
440
  >
443
441
  <ul>
444
442
  {typeof tooltipData === 'object' &&
445
- Object.entries(tooltipData.data).map((item, index) => <TooltipListItem item={item} key={index} />)}
443
+ tooltipData.data.map((row, index) => (
444
+ <TooltipListItem
445
+ row={row}
446
+ index={index}
447
+ key={index}
448
+ useMarkerColumn={tooltipData.useMarkerColumn}
449
+ />
450
+ ))}
446
451
  </ul>
447
452
  </TooltipWithBounds>
448
453
  </>
@@ -41,6 +41,8 @@ const RadarChart = React.forwardRef<SVGSVGElement, RadarChartProps>((props, ref)
41
41
  formatNumber
42
42
  } = useContext(ConfigContext)
43
43
 
44
+ const a11y = handleChartAriaLabels(config)
45
+
44
46
  const dispatch = useContext(ChartDispatchContext)
45
47
 
46
48
  const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } = useTooltip<TooltipData>()
@@ -232,7 +234,7 @@ const RadarChart = React.forwardRef<SVGSVGElement, RadarChartProps>((props, ref)
232
234
  height={height}
233
235
  className='radar-chart'
234
236
  role='img'
235
- aria-label={handleChartAriaLabels(config)}
237
+ aria-label={a11y}
236
238
  onMouseEnter={handleChartMouseEnter}
237
239
  onMouseLeave={() => {
238
240
  handleTooltipMouseOff()