@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,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'
@@ -24,6 +24,8 @@ import { ChartContext } from '../../../types/ChartContext'
24
24
  import _ from 'lodash'
25
25
  import { getBarData } from '../helpers/getBarData'
26
26
  import { getHorizontalBarHeights } from '../helpers/getBarHeights'
27
+ import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
28
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
27
29
 
28
30
  const BarChartHorizontal = () => {
29
31
  const { xScale, yScale, yMax, seriesScale, barChart } = useContext<BarChartContextValues>(BarChartContext)
@@ -72,7 +74,7 @@ const BarChartHorizontal = () => {
72
74
  return (
73
75
  <defs>
74
76
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
75
- const patternId = `chart-pattern-${key}`
77
+ const patternId = getChartPatternId(key)
76
78
  const size = pattern.patternSize || 8
77
79
 
78
80
  switch (pattern.shape) {
@@ -324,33 +326,15 @@ const BarChartHorizontal = () => {
324
326
  })
325
327
 
326
328
  // Check if this bar should use a pattern
327
- const getPatternUrl = (): string | null => {
328
- if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
329
- return null
330
- }
331
-
332
- // Find a pattern that matches this specific bar
333
- for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
334
- if (pattern.dataKey && pattern.dataValue) {
335
- // For grouped bar charts, check if the pattern's dataKey matches the current bar's series key
336
- // and if the pattern's dataValue matches the current bar's value
337
- if (pattern.dataKey === bar.key && String(bar.value) === String(pattern.dataValue)) {
338
- return `url(#chart-pattern-${patternKey})`
339
- }
340
- // Fallback for non-grouped charts: check datum field value
341
- else if (!config.series || config.series.length <= 1) {
342
- const dataFieldValue = datum[pattern.dataKey]
343
- if (String(dataFieldValue) === String(pattern.dataValue)) {
344
- return `url(#chart-pattern-${patternKey})`
345
- }
346
- }
347
- }
348
- }
349
-
350
- return null
351
- }
352
-
353
- const patternUrl = getPatternUrl()
329
+ const patternUrl = getPatternUrlForBar({
330
+ patterns: config.legend?.patterns,
331
+ datum,
332
+ seriesKey: bar.key,
333
+ seriesValue: bar.value,
334
+ seriesLabels: config.runtime?.seriesLabels,
335
+ seriesKeys: config.series?.map(series => series.dataKey),
336
+ allowNonSeriesFieldMatch: !config.series || config.series.length <= 1
337
+ })
354
338
  const baseBackground = getBarBackgroundColor()
355
339
 
356
340
  return (
@@ -14,6 +14,8 @@ import { type ChartContext } from '../../../types/ChartContext'
14
14
 
15
15
  import createBarElement from '@cdc/core/components/createBarElement'
16
16
  import { getHorizontalBarHeights } from '../helpers/getBarHeights'
17
+ import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
18
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
17
19
 
18
20
  const BarChartStackedHorizontal = () => {
19
21
  const { yMax, yScale, xScale, barChart } = useContext<BarChartContextValues>(BarChartContext)
@@ -55,7 +57,7 @@ const BarChartStackedHorizontal = () => {
55
57
  return (
56
58
  <defs>
57
59
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
58
- const patternId = `chart-pattern-${key}`
60
+ const patternId = getChartPatternId(key)
59
61
  const size = pattern.patternSize || 8
60
62
 
61
63
  switch (pattern.shape) {
@@ -166,35 +168,15 @@ const BarChartStackedHorizontal = () => {
166
168
  </li></ul>`
167
169
 
168
170
  // Check if this bar should use a pattern
169
- const getPatternUrl = (): string | null => {
170
- if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
171
- return null
172
- }
173
-
174
- // Find a pattern that matches this specific bar
175
- for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
176
- if (pattern.dataKey && pattern.dataValue) {
177
- // For stacked bar charts, check if the pattern's dataKey matches the current bar's series key
178
- // and if the pattern's dataValue matches the current bar's value
179
- const barValue = data[bar.index][bar.key]
180
- if (pattern.dataKey === bar.key && String(barValue) === String(pattern.dataValue)) {
181
- return `url(#chart-pattern-${patternKey})`
182
- }
183
- // Fallback for non-series pattern matching (like the original stacked pattern test)
184
- // Only check this if the pattern dataKey is NOT a series key
185
- else if (!config.runtime.seriesLabels || !config.runtime.seriesLabels[pattern.dataKey]) {
186
- const dataFieldValue = data[bar.index][pattern.dataKey]
187
- if (String(dataFieldValue) === String(pattern.dataValue)) {
188
- return `url(#chart-pattern-${patternKey})`
189
- }
190
- }
191
- }
192
- }
193
-
194
- return null
195
- }
196
-
197
- const patternUrl = getPatternUrl()
171
+ const patternUrl = getPatternUrlForBar({
172
+ patterns: config.legend?.patterns,
173
+ datum: data[bar.index],
174
+ seriesKey: bar.key,
175
+ seriesValue: data[bar.index][bar.key],
176
+ seriesLabels: config.runtime?.seriesLabels,
177
+ seriesKeys: config.series?.map(series => series.dataKey),
178
+ allowNonSeriesFieldMatch: true
179
+ })
198
180
 
199
181
  return (
200
182
  <React.Fragment key={`stack-${stackIndex}-bar-${index}-${barStack.index}`}>
@@ -8,6 +8,8 @@ import Regions from '../../Regions'
8
8
  import { addMinimumBarHeights } from '../helpers'
9
9
 
10
10
  import createBarElement from '@cdc/core/components/createBarElement'
11
+ import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
12
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
11
13
 
12
14
  const BarChartStackedVertical = () => {
13
15
  const [barWidth, setBarWidth] = useState(0)
@@ -40,7 +42,7 @@ const BarChartStackedVertical = () => {
40
42
  return (
41
43
  <defs>
42
44
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
43
- const patternId = `chart-pattern-${key}`
45
+ const patternId = getChartPatternId(key)
44
46
  const size = pattern.patternSize || 8
45
47
 
46
48
  switch (pattern.shape) {
@@ -150,36 +152,15 @@ const BarChartStackedVertical = () => {
150
152
  setBarWidth(barThickness)
151
153
 
152
154
  // Check if this bar should use a pattern
153
- const getPatternUrl = (): string | null => {
154
- if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
155
- return null
156
- }
157
-
158
- // Find a pattern that matches this specific bar
159
- for (const [patternKey, patternObj] of Object.entries(config.legend.patterns)) {
160
- const pattern = patternObj as any
161
- if (pattern?.dataKey && pattern?.dataValue) {
162
- // For stacked bar charts, check if the pattern's dataKey matches the current bar's series key
163
- // and if the pattern's dataValue matches the current bar's value
164
- const barValue = bar.bar.data[bar.key]
165
- if (pattern.dataKey === bar.key && String(barValue) === String(pattern.dataValue)) {
166
- return `url(#chart-pattern-${patternKey})`
167
- }
168
- // Fallback for non-series pattern matching (like the original stacked pattern test)
169
- // Only check this if the pattern dataKey is NOT a series key
170
- else if (!config.runtime.seriesLabels || !config.runtime.seriesLabels[pattern.dataKey]) {
171
- const dataFieldValue = bar.bar.data[pattern.dataKey]
172
- if (String(dataFieldValue) === String(pattern.dataValue)) {
173
- return `url(#chart-pattern-${patternKey})`
174
- }
175
- }
176
- }
177
- }
178
-
179
- return null
180
- }
181
-
182
- const patternUrl = getPatternUrl()
155
+ const patternUrl = getPatternUrlForBar({
156
+ patterns: config.legend?.patterns,
157
+ datum: bar.bar.data,
158
+ seriesKey: bar.key,
159
+ seriesValue: bar.bar.data[bar.key],
160
+ seriesLabels: config.runtime?.seriesLabels,
161
+ seriesKeys: config.series?.map(series => series.dataKey),
162
+ allowNonSeriesFieldMatch: true
163
+ })
183
164
 
184
165
  return (
185
166
  <Group key={`${barStack.index}--${bar.index}--${orientation}`}>
@@ -21,6 +21,8 @@ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
21
21
  import { type ChartContext } from '../../../types/ChartContext'
22
22
  import _ from 'lodash'
23
23
  import { getBarData } from '../helpers/getBarData'
24
+ import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
25
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
24
26
 
25
27
  const BarChartVertical = () => {
26
28
  const { xScale, yScale, xMax, yMax, seriesScale, convertLineToBarGraph, barChart } =
@@ -86,7 +88,7 @@ const BarChartVertical = () => {
86
88
  return (
87
89
  <defs>
88
90
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
89
- const patternId = `chart-pattern-${key}`
91
+ const patternId = getChartPatternId(key)
90
92
  const size = pattern.patternSize || 8
91
93
 
92
94
  switch (pattern.shape) {
@@ -325,33 +327,15 @@ const BarChartVertical = () => {
325
327
  }
326
328
 
327
329
  // Check if this bar should use a pattern
328
- const getPatternUrl = (): string | null => {
329
- if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
330
- return null
331
- }
332
-
333
- // Find a pattern that matches this specific bar
334
- for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
335
- if (pattern.dataKey && pattern.dataValue) {
336
- // For grouped bar charts, check if the pattern's dataKey matches the current bar's series key
337
- // and if the pattern's dataValue matches the current bar's value
338
- if (pattern.dataKey === bar.key && String(bar.value) === String(pattern.dataValue)) {
339
- return `url(#chart-pattern-${patternKey})`
340
- }
341
- // Fallback for non-grouped charts: check datum field value
342
- else if (!config.series || config.series.length <= 1) {
343
- const dataFieldValue = datum[pattern.dataKey]
344
- if (String(dataFieldValue) === String(pattern.dataValue)) {
345
- return `url(#chart-pattern-${patternKey})`
346
- }
347
- }
348
- }
349
- }
350
-
351
- return null
352
- }
353
-
354
- const patternUrl = getPatternUrl()
330
+ const patternUrl = getPatternUrlForBar({
331
+ patterns: config.legend?.patterns,
332
+ datum,
333
+ seriesKey: bar.key,
334
+ seriesValue: bar.value,
335
+ seriesLabels: config.runtime?.seriesLabels,
336
+ seriesKeys: config.series?.map(series => series.dataKey),
337
+ allowNonSeriesFieldMatch: !config.series || config.series.length <= 1
338
+ })
355
339
  const baseBackground = getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))
356
340
 
357
341
  // Confidence Interval Variables
@@ -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 />
@@ -0,0 +1,94 @@
1
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
2
+
3
+ type LegendPattern = {
4
+ dataKey?: string
5
+ dataValue?: string | number
6
+ }
7
+
8
+ type SeriesLabels = Record<string, string> | undefined
9
+
10
+ type GetPatternUrlArgs = {
11
+ patterns?: Record<string, LegendPattern>
12
+ datum: Record<string, any>
13
+ seriesKey: string
14
+ seriesValue: string | number
15
+ seriesLabels?: SeriesLabels
16
+ seriesKeys?: string[]
17
+ allowNonSeriesFieldMatch?: boolean
18
+ }
19
+
20
+ const normalizeString = (value: unknown): string => String(value ?? '').trim()
21
+
22
+ const hasPatternValue = (value: unknown): boolean => normalizeString(value) !== ''
23
+
24
+ const isNumericLike = (value: string): boolean => value !== '' && !Number.isNaN(Number(value))
25
+
26
+ const valuesMatch = (left: unknown, right: unknown): boolean => {
27
+ const normalizedLeft = normalizeString(left)
28
+ const normalizedRight = normalizeString(right)
29
+
30
+ if (normalizedLeft === '' || normalizedRight === '') {
31
+ return false
32
+ }
33
+
34
+ if (isNumericLike(normalizedLeft) && isNumericLike(normalizedRight)) {
35
+ return Number(normalizedLeft) === Number(normalizedRight)
36
+ }
37
+
38
+ return normalizedLeft === normalizedRight
39
+ }
40
+
41
+ const isSeriesDataKey = (dataKey: string, seriesLabels?: SeriesLabels, seriesKeys?: string[]): boolean => {
42
+ if (Array.isArray(seriesKeys) && seriesKeys.length > 0) {
43
+ return seriesKeys.includes(dataKey)
44
+ }
45
+ if (!seriesLabels) return false
46
+ return Object.prototype.hasOwnProperty.call(seriesLabels, dataKey)
47
+ }
48
+
49
+ export const getPatternUrl = ({
50
+ patterns,
51
+ datum,
52
+ seriesKey,
53
+ seriesValue,
54
+ seriesLabels,
55
+ seriesKeys,
56
+ allowNonSeriesFieldMatch = true
57
+ }: GetPatternUrlArgs): string | null => {
58
+ if (!patterns) {
59
+ return null
60
+ }
61
+
62
+ let broadMatchUrl: string | null = null
63
+
64
+ for (const patternKey in patterns) {
65
+ if (!Object.prototype.hasOwnProperty.call(patterns, patternKey)) continue
66
+ const pattern = patterns[patternKey]
67
+ const dataKey = normalizeString(pattern.dataKey)
68
+
69
+ if (!hasPatternValue(pattern.dataValue)) {
70
+ continue
71
+ }
72
+
73
+ if (dataKey === '') {
74
+ if (!broadMatchUrl && valuesMatch(seriesValue, pattern.dataValue)) {
75
+ broadMatchUrl = `url(#${getChartPatternId(patternKey)})`
76
+ }
77
+ continue
78
+ }
79
+
80
+ if (dataKey === seriesKey && valuesMatch(seriesValue, pattern.dataValue)) {
81
+ return `url(#${getChartPatternId(patternKey)})`
82
+ }
83
+
84
+ if (
85
+ allowNonSeriesFieldMatch &&
86
+ !isSeriesDataKey(dataKey, seriesLabels, seriesKeys) &&
87
+ valuesMatch(datum?.[dataKey], pattern.dataValue)
88
+ ) {
89
+ return `url(#${getChartPatternId(patternKey)})`
90
+ }
91
+ }
92
+
93
+ return broadMatchUrl
94
+ }