@cdc/chart 4.25.8 → 4.25.11

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 (145) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  3. package/dist/cdcchart.js +44236 -40355
  4. package/examples/feature/__data__/planet-example-data.json +0 -30
  5. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  6. package/examples/grouped-bar-test.json +400 -0
  7. package/examples/private/DEV-11825.json +573 -0
  8. package/examples/private/d.json +382 -0
  9. package/examples/private/example-2.json +49784 -0
  10. package/examples/private/f2.json +1 -0
  11. package/examples/private/f4.json +1577 -0
  12. package/examples/private/forecast.json +1180 -0
  13. package/examples/private/lollipop.json +468 -0
  14. package/examples/private/na.json +913 -0
  15. package/examples/private/new.json +48756 -0
  16. package/examples/private/pie-chart-legend.json +904 -0
  17. package/examples/private/test-data.csv +28 -0
  18. package/examples/suppressed_tooltip.json +480 -0
  19. package/index.html +2 -133
  20. package/package.json +25 -7
  21. package/src/CdcChart.tsx +9 -13
  22. package/src/CdcChartComponent.tsx +403 -92
  23. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  24. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  25. package/src/_stories/Chart.CI.stories.tsx +1 -1
  26. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  27. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  28. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  29. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  30. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  31. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  32. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  33. package/src/_stories/Chart.Patterns.stories.tsx +20 -0
  34. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  37. package/src/_stories/Chart.stories.tsx +8 -5
  38. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  39. package/src/_stories/ChartAnnotation.stories.tsx +7 -4
  40. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  41. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  42. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  43. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  44. package/src/_stories/ChartEditor.stories.tsx +59 -60
  45. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  46. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  47. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  48. package/src/_stories/_mock/combo.json +451 -0
  49. package/src/_stories/_mock/editor-test-configs.json +376 -0
  50. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  51. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  52. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  53. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  54. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  55. package/src/_stories/_mock/pie_config.json +257 -62
  56. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  57. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  58. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  59. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  60. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  61. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  63. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  64. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  65. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  66. package/src/components/AreaChart/index.tsx +1 -2
  67. package/src/components/BarChart/components/BarChart.Horizontal.tsx +161 -22
  68. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
  69. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  70. package/src/components/BarChart/components/BarChart.Vertical.tsx +155 -22
  71. package/src/components/BarChart/helpers/index.ts +43 -4
  72. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  73. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  74. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  75. package/src/components/BoxPlot/helpers/index.ts +3 -3
  76. package/src/components/Brush/BrushChart.tsx +1 -1
  77. package/src/components/DeviationBar.jsx +9 -6
  78. package/src/components/EditorPanel/EditorPanel.tsx +563 -229
  79. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  80. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  81. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  82. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +461 -0
  83. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
  84. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
  86. package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
  87. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  88. package/src/components/EditorPanel/editor-panel.scss +0 -20
  89. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  90. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  91. package/src/components/Forecasting/Forecasting.tsx +175 -27
  92. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  93. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  94. package/src/components/Legend/Legend.Component.tsx +114 -14
  95. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  96. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  97. package/src/components/LegendWrapper.tsx +1 -1
  98. package/src/components/LineChart/LineChartProps.ts +0 -3
  99. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  100. package/src/components/LineChart/helpers.ts +1 -1
  101. package/src/components/LineChart/index.tsx +38 -15
  102. package/src/components/LinearChart.tsx +96 -84
  103. package/src/components/PairedBarChart.jsx +6 -4
  104. package/src/components/PieChart/PieChart.tsx +170 -54
  105. package/src/components/Regions/components/Regions.tsx +3 -24
  106. package/src/components/Sankey/components/Sankey.tsx +7 -1
  107. package/src/components/Sankey/types/index.ts +1 -1
  108. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  109. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  110. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  111. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  112. package/src/components/SmallMultiples/index.ts +2 -0
  113. package/src/data/initial-state.js +327 -293
  114. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  115. package/src/helpers/buildForecastPaletteOptions.ts +71 -0
  116. package/src/helpers/getColorScale.ts +82 -8
  117. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  118. package/src/helpers/getNewRuntime.ts +1 -1
  119. package/src/helpers/getTransformedData.ts +1 -1
  120. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  121. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  122. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  123. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  124. package/src/hooks/useReduceData.ts +105 -70
  125. package/src/hooks/useScales.ts +88 -34
  126. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  127. package/src/hooks/useTooltip.tsx +116 -29
  128. package/src/index.jsx +0 -2
  129. package/src/scss/main.scss +13 -80
  130. package/src/store/chart.actions.ts +2 -0
  131. package/src/store/chart.reducer.ts +5 -1
  132. package/src/test/CdcChart.test.jsx +8 -3
  133. package/src/types/ChartConfig.ts +53 -11
  134. package/src/types/ChartContext.ts +4 -0
  135. package/vite.config.js +1 -1
  136. package/vitest.config.ts +16 -0
  137. package/src/_stories/_mock/pie_data.json +0 -218
  138. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  139. package/src/coreStyles_chart.scss +0 -3
  140. package/src/helpers/configHelpers.ts +0 -28
  141. package/src/helpers/generateColorsArray.ts +0 -8
  142. package/src/helpers/sort.ts +0 -7
  143. package/src/hooks/useActiveElement.js +0 -19
  144. package/src/hooks/useChartClasses.js +0 -41
  145. package/src/hooks/useColorPalette.js +0 -76
@@ -2,10 +2,10 @@ import React, { useContext, useState } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import { BarStack } from '@visx/shape'
4
4
  import { Group } from '@visx/group'
5
- import { Text } from '@visx/text'
5
+ import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
6
6
  import BarChartContext from './context'
7
7
  import Regions from '../../Regions'
8
- import { isDateScale } from '@cdc/core/helpers/cove/date'
8
+ import { addMinimumBarHeights } from '../helpers'
9
9
 
10
10
  import createBarElement from '@cdc/core/components/createBarElement'
11
11
 
@@ -31,10 +31,78 @@ const BarChartStackedVertical = () => {
31
31
  const isDateAxisType = config.runtime.xAxis.type === 'date-time' || config.runtime.xAxis.type === 'date'
32
32
  const isDateTimeScaleAxisType = config.runtime.xAxis.type === 'date-time'
33
33
 
34
+ // Pattern helper function
35
+ const renderPatternDefs = () => {
36
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
37
+ return null
38
+ }
39
+
40
+ return (
41
+ <defs>
42
+ {Object.entries(config.legend.patterns).map(([key, pattern]) => {
43
+ const patternId = `chart-pattern-${key}`
44
+ const size = pattern.patternSize || 8
45
+
46
+ switch (pattern.shape) {
47
+ case 'circles':
48
+ return (
49
+ <PatternCircles
50
+ key={patternId}
51
+ id={patternId}
52
+ height={size}
53
+ width={size}
54
+ fill={pattern.color}
55
+ radius={1.25}
56
+ />
57
+ )
58
+ case 'lines':
59
+ return (
60
+ <PatternLines
61
+ key={patternId}
62
+ id={patternId}
63
+ height={size}
64
+ width={size}
65
+ stroke={pattern.color}
66
+ strokeWidth={0.75}
67
+ orientation={['horizontal']}
68
+ />
69
+ )
70
+ case 'diagonalLines':
71
+ return (
72
+ <PatternLines
73
+ key={patternId}
74
+ id={patternId}
75
+ height={size}
76
+ width={size}
77
+ stroke={pattern.color}
78
+ strokeWidth={0.75}
79
+ orientation={['diagonalRightToLeft']}
80
+ />
81
+ )
82
+ case 'waves':
83
+ return (
84
+ <PatternWaves
85
+ key={patternId}
86
+ id={patternId}
87
+ height={size}
88
+ width={size}
89
+ fill={pattern.color}
90
+ strokeWidth={0.25}
91
+ />
92
+ )
93
+ default:
94
+ return null
95
+ }
96
+ })}
97
+ </defs>
98
+ )
99
+ }
100
+
34
101
  return (
35
102
  config.visualizationSubType === 'stacked' &&
36
103
  !isHorizontal && (
37
104
  <>
105
+ {renderPatternDefs()}
38
106
  <BarStack
39
107
  data={data}
40
108
  keys={barStackedSeriesKeys}
@@ -43,87 +111,161 @@ const BarChartStackedVertical = () => {
43
111
  yScale={yScale}
44
112
  color={colorScale}
45
113
  >
46
- {barStacks =>
47
- barStacks.reverse().map(barStack =>
48
- barStack.bars.map(bar => {
49
- let transparentBar =
50
- config.legend.behavior === 'highlight' &&
51
- seriesHighlight.length > 0 &&
52
- seriesHighlight.indexOf(bar.key) === -1
53
- let displayBar =
54
- config.legend.behavior === 'highlight' ||
55
- seriesHighlight.length === 0 ||
56
- seriesHighlight.indexOf(bar.key) !== -1
57
- let barThickness = isDateAxisType
58
- ? seriesScale.range()[1] - seriesScale.range()[0]
59
- : xMax / barStack.bars.length
60
- if (config.runtime.xAxis.type !== 'date') barThickness = config.barThickness * barThickness
61
- // tooltips
62
- const rawXValue = bar.bar.data[config.runtime.xAxis.dataKey]
63
- const xAxisValue = isDateAxisType ? formatDate(parseDate(rawXValue)) : rawXValue
64
- const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0, 'left')
65
- if (!yAxisValue) return
66
- const barX =
67
- xScale(isDateAxisType ? parseDate(rawXValue) : rawXValue) -
68
- (isDateTimeScaleAxisType ? barThickness / 2 : 0)
69
- const xAxisTooltip = config.runtime.xAxis.label
70
- ? `${config.runtime.xAxis.label}: ${xAxisValue}`
71
- : xAxisValue
72
- const additionalColTooltip = getAdditionalColumn(bar.key, hoveredBar)
73
- const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${yAxisValue}`
74
- const tooltip = `<ul>
114
+ {barStacks => {
115
+ return addMinimumBarHeights(barStacks)
116
+ .reverse()
117
+ .map(barStack => {
118
+ return barStack.bars.map(bar => {
119
+ let transparentBar =
120
+ config.legend.behavior === 'highlight' &&
121
+ seriesHighlight.length > 0 &&
122
+ seriesHighlight.indexOf(bar.key) === -1
123
+ let displayBar =
124
+ config.legend.behavior === 'highlight' ||
125
+ seriesHighlight.length === 0 ||
126
+ seriesHighlight.indexOf(bar.key) !== -1
127
+ let barThickness = isDateAxisType
128
+ ? seriesScale.range()[1] - seriesScale.range()[0]
129
+ : xMax / barStack.bars.length
130
+ if (config.runtime.xAxis.type !== 'date') barThickness = config.barThickness * barThickness
131
+ // tooltips
132
+ const rawXValue = bar.bar.data[config.runtime.xAxis.dataKey]
133
+ const xAxisValue = isDateAxisType ? formatDate(parseDate(rawXValue)) : rawXValue
134
+ const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0, 'left')
135
+ if (!yAxisValue) return
136
+ const barX =
137
+ xScale(isDateAxisType ? parseDate(rawXValue) : rawXValue) -
138
+ (isDateTimeScaleAxisType ? barThickness / 2 : 0)
139
+ const xAxisTooltip = config.runtime.xAxis.label
140
+ ? `${config.runtime.xAxis.label}: ${xAxisValue}`
141
+ : xAxisValue
142
+ const additionalColTooltip = getAdditionalColumn(bar.key, hoveredBar)
143
+ const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${yAxisValue}`
144
+ const tooltip = `<ul>
75
145
  <li class="tooltip-heading"">${xAxisTooltip}</li>
76
146
  <li class="tooltip-body ">${tooltipBody}</li>
77
147
  <li class="tooltip-body ">${additionalColTooltip}</li>
78
148
  </li></ul>`
79
149
 
80
- setBarWidth(barThickness)
81
-
82
- return (
83
- <Group key={`${barStack.index}--${bar.index}--${orientation}`}>
84
- <Group
85
- key={`bar-stack-${barStack.index}-${bar.index}`}
86
- id={`barStack${barStack.index}-${bar.index}`}
87
- className='stack vertical'
88
- >
89
- {createBarElement({
90
- config: config,
91
- seriesHighlight,
92
- index: barStack.index,
93
- background: colorScale(config.runtime.seriesLabels[bar.key]),
94
- borderColor: '#333',
95
- borderStyle: 'solid',
96
- borderWidth: `${config.barHasBorder === 'true' ? barBorderWidth : 0}px`,
97
- width: barThickness,
98
- height: bar.height,
99
- x: barX,
100
- y: bar.y,
101
- onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data),
102
- onMouseLeave: onMouseLeaveBar,
103
- tooltipHtml: tooltip,
104
- tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`,
105
- onClick: e => {
106
- e.preventDefault()
107
- if (setSharedFilter) {
108
- bar[config.xAxis.dataKey] = xAxisValue
109
- setSharedFilter(config.uid, bar)
150
+ setBarWidth(barThickness)
151
+
152
+ // 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})`
110
174
  }
111
- },
112
- styleOverrides: {
113
- animationDelay: `${barStack.index * 0.5}s`,
114
- transformOrigin: `${barThickness / 2}px ${bar.y + bar.height}px`,
115
- opacity: transparentBar ? 0.2 : 1,
116
- display: displayBar ? 'block' : 'none'
117
175
  }
118
- })}
176
+ }
177
+ }
178
+
179
+ return null
180
+ }
181
+
182
+ const patternUrl = getPatternUrl()
183
+
184
+ return (
185
+ <Group key={`${barStack.index}--${bar.index}--${orientation}`}>
186
+ <Group
187
+ key={`bar-stack-${barStack.index}-${bar.index}`}
188
+ id={`barStack${barStack.index}-${bar.index}`}
189
+ className='stack vertical'
190
+ >
191
+ {/* Base colored bar */}
192
+ {createBarElement({
193
+ config: config,
194
+ seriesHighlight,
195
+ index: barStack.index,
196
+ background: colorScale(config.runtime.seriesLabels[bar.key]),
197
+ borderColor: '#333',
198
+ borderStyle: 'solid',
199
+ borderWidth: `${config.barHasBorder === 'true' ? barBorderWidth : 0}px`,
200
+ width: barThickness,
201
+ height: bar.height,
202
+ x: barX,
203
+ y: bar.y,
204
+ onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data, bar.bar.data[bar.key]),
205
+ onMouseLeave: onMouseLeaveBar,
206
+ tooltipHtml: tooltip,
207
+ tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`,
208
+ onClick: e => {
209
+ e.preventDefault()
210
+ if (setSharedFilter) {
211
+ bar[config.xAxis.dataKey] = xAxisValue
212
+ setSharedFilter(config.uid, bar)
213
+ }
214
+ },
215
+ styleOverrides: {
216
+ animationDelay: `${barStack.index * 0.5}s`,
217
+ transformOrigin: `${barThickness / 2}px ${bar.y + bar.height}px`,
218
+ opacity: transparentBar ? 0.2 : 1,
219
+ display: displayBar ? 'block' : 'none'
220
+ }
221
+ })}
222
+
223
+ {/* Pattern overlay if pattern exists */}
224
+ {patternUrl &&
225
+ createBarElement({
226
+ config: config,
227
+ seriesHighlight,
228
+ index: barStack.index,
229
+ background: patternUrl, // Use pattern as background
230
+ borderColor: 'transparent',
231
+ borderStyle: 'none',
232
+ borderWidth: '0px',
233
+ width: barThickness,
234
+ height: bar.height,
235
+ x: barX,
236
+ y: bar.y,
237
+ onMouseOver: () => {}, // No interaction
238
+ onMouseLeave: () => {}, // No interaction
239
+ tooltipHtml: '',
240
+ tooltipId: '',
241
+ onClick: () => {}, // No interaction
242
+ styleOverrides: {
243
+ animationDelay: `${barStack.index * 0.5}s`,
244
+ transformOrigin: `${barThickness / 2}px ${bar.y + bar.height}px`,
245
+ opacity: transparentBar ? 0.2 : 1,
246
+ display: displayBar ? 'block' : 'none',
247
+ pointerEvents: 'none' // Let clicks pass through to base bar
248
+ }
249
+ })}
250
+ </Group>
119
251
  </Group>
120
- </Group>
121
- )
252
+ )
253
+ })
122
254
  })
123
- )
124
- }
255
+ }}
125
256
  </BarStack>
126
- <Regions xScale={xScale} yMax={yMax} barWidth={barWidth} totalBarsInGroup={1} />
257
+ <Regions
258
+ xScale={xScale}
259
+ yMax={yMax}
260
+ barWidth={barWidth}
261
+ totalBarsInGroup={1}
262
+ handleTooltipMouseOff={() => {}}
263
+ handleTooltipMouseOver={() => {}}
264
+ handleTooltipClick={() => {}}
265
+ tooltipData={null}
266
+ showTooltip={() => {}}
267
+ hideTooltip={() => {}}
268
+ />
127
269
  </>
128
270
  )
129
271
  )
@@ -4,11 +4,12 @@ import ConfigContext from '../../../ConfigContext'
4
4
  import BarChartContext, { type BarChartContextValues } from './context'
5
5
  // Local hooks
6
6
  import { useHighlightedBars } from '../../../hooks/useHighlightedBars'
7
- import { getBarConfig, testZeroValue } from '../helpers'
7
+ import { getBarConfig, testZeroValue, getLollipopStemColor, getLollipopHeadColor } from '../helpers'
8
8
  // VisX library imports
9
9
  import { Group } from '@visx/group'
10
10
  import { Text } from '@visx/text'
11
11
  import { BarGroup } from '@visx/shape'
12
+ import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
12
13
  // Local components
13
14
  import Regions from '../../Regions'
14
15
  // CDC core components and helpers
@@ -17,14 +18,12 @@ import isNumber from '@cdc/core/helpers/isNumber'
17
18
  import createBarElement from '@cdc/core/components/createBarElement'
18
19
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
19
20
  import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
20
- // Third party libraries
21
- import chroma from 'chroma-js'
22
21
  // Types
23
22
  import { type ChartContext } from '../../../types/ChartContext'
24
23
  import _ from 'lodash'
25
24
  import { getBarData } from '../helpers/getBarData'
26
25
 
27
- export const BarChartVertical = () => {
26
+ const BarChartVertical = () => {
28
27
  const { xScale, yScale, xMax, yMax, seriesScale, convertLineToBarGraph, barChart } =
29
28
  useContext<BarChartContextValues>(BarChartContext)
30
29
  const {
@@ -47,6 +46,7 @@ export const BarChartVertical = () => {
47
46
  colorScale,
48
47
  config,
49
48
  currentViewport,
49
+ vizViewport,
50
50
  dashboardConfig,
51
51
  tableData,
52
52
  formatDate,
@@ -59,6 +59,8 @@ export const BarChartVertical = () => {
59
59
 
60
60
  const { HighLightedBarUtils } = useHighlightedBars(config)
61
61
 
62
+ const LABEL_FONT_SIZE = isMobileFontViewport(vizViewport) ? 13 : 16
63
+
62
64
  const root = document.documentElement
63
65
 
64
66
  let data = transformedData
@@ -76,11 +78,80 @@ export const BarChartVertical = () => {
76
78
  config.confidenceKeys.lower !== ''
77
79
 
78
80
  const _data = getBarData(config, data, hasConfidenceInterval)
81
+
82
+ // Pattern helper function
83
+ const renderPatternDefs = () => {
84
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
85
+ return null
86
+ }
87
+
88
+ return (
89
+ <defs>
90
+ {Object.entries(config.legend.patterns).map(([key, pattern]) => {
91
+ const patternId = `chart-pattern-${key}`
92
+ const size = pattern.patternSize || 8
93
+
94
+ switch (pattern.shape) {
95
+ case 'circles':
96
+ return (
97
+ <PatternCircles
98
+ key={patternId}
99
+ id={patternId}
100
+ height={size}
101
+ width={size}
102
+ fill={pattern.color}
103
+ radius={1.25}
104
+ />
105
+ )
106
+ case 'lines':
107
+ return (
108
+ <PatternLines
109
+ key={patternId}
110
+ id={patternId}
111
+ height={size}
112
+ width={size}
113
+ stroke={pattern.color}
114
+ strokeWidth={0.75}
115
+ orientation={['horizontal']}
116
+ />
117
+ )
118
+ case 'diagonalLines':
119
+ return (
120
+ <PatternLines
121
+ key={patternId}
122
+ id={patternId}
123
+ height={size}
124
+ width={size}
125
+ stroke={pattern.color}
126
+ strokeWidth={0.75}
127
+ orientation={['diagonalRightToLeft']}
128
+ />
129
+ )
130
+ case 'waves':
131
+ return (
132
+ <PatternWaves
133
+ key={patternId}
134
+ id={patternId}
135
+ height={size}
136
+ width={size}
137
+ fill={pattern.color}
138
+ strokeWidth={0.25}
139
+ />
140
+ )
141
+ default:
142
+ return null
143
+ }
144
+ })}
145
+ </defs>
146
+ )
147
+ }
148
+
79
149
  return (
80
150
  config.visualizationSubType !== 'stacked' &&
81
151
  (config.visualizationType === 'Bar' || config.visualizationType === 'Combo' || convertLineToBarGraph) &&
82
152
  config.orientation === 'vertical' && (
83
153
  <Group>
154
+ {renderPatternDefs()}
84
155
  <BarGroup
85
156
  data={_data}
86
157
  keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
@@ -170,7 +241,8 @@ export const BarChartVertical = () => {
170
241
  config,
171
242
  barWidth,
172
243
  isVertical: true,
173
- yAxisValue
244
+ yAxisValue,
245
+ labelFontSize: LABEL_FONT_SIZE
174
246
  })
175
247
  // configure colors
176
248
  let labelColor = APP_FONT_COLOR
@@ -236,12 +308,16 @@ export const BarChartVertical = () => {
236
308
  if (isHighlightedBar) _barColor = 'transparent'
237
309
  if (config.legend.colorCode)
238
310
  _barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
239
- if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
311
+ if (isTwoToneLollipopColor) {
312
+ _barColor = getLollipopStemColor(barColor)
313
+ }
240
314
  return _barColor
241
315
  }
242
316
 
243
- // if this is a two tone lollipop slightly lighten the bar.
244
- if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
317
+ // if this is a two tone lollipop, ensure stem has good contrast against white but is lighter than head
318
+ if (isTwoToneLollipopColor) {
319
+ _barColor = getLollipopStemColor(barColor)
320
+ }
245
321
  if (config.legend.colorCode)
246
322
  _barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
247
323
 
@@ -250,6 +326,36 @@ export const BarChartVertical = () => {
250
326
  return _barColor
251
327
  }
252
328
 
329
+ // Check if this bar should use a pattern
330
+ const getPatternUrl = (): string | null => {
331
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
332
+ return null
333
+ }
334
+
335
+ // Find a pattern that matches this specific bar
336
+ for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
337
+ if (pattern.dataKey && pattern.dataValue) {
338
+ // For grouped bar charts, check if the pattern's dataKey matches the current bar's series key
339
+ // and if the pattern's dataValue matches the current bar's value
340
+ if (pattern.dataKey === bar.key && String(bar.value) === String(pattern.dataValue)) {
341
+ return `url(#chart-pattern-${patternKey})`
342
+ }
343
+ // Fallback for non-grouped charts: check datum field value
344
+ else if (!config.series || config.series.length <= 1) {
345
+ const dataFieldValue = datum[pattern.dataKey]
346
+ if (String(dataFieldValue) === String(pattern.dataValue)) {
347
+ return `url(#chart-pattern-${patternKey})`
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ return null
354
+ }
355
+
356
+ const patternUrl = getPatternUrl()
357
+ const baseBackground = getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))
358
+
253
359
  // Confidence Interval Variables
254
360
  const tickWidth = 5
255
361
  const xPos = barX + (config.xAxis.type !== 'date-time' ? barWidth / 2 : 0)
@@ -268,16 +374,15 @@ export const BarChartVertical = () => {
268
374
 
269
375
  const BAR_LABEL_PADDING = 10
270
376
 
271
- const LABEL_FONT_SIZE = isMobileFontViewport(currentViewport) ? 13 : 16
272
-
273
377
  return (
274
378
  <Group display={hideGroup} key={`${barGroup.index}--${index}`}>
275
379
  <Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
380
+ {/* Base colored bar */}
276
381
  {createBarElement({
277
382
  config: config,
278
383
  index: newIndex,
279
384
  id: `barGroup${barGroup.index}`,
280
- background: getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key])),
385
+ background: baseBackground,
281
386
  borderColor,
282
387
  borderStyle: 'solid',
283
388
  borderWidth: `${borderWidth}px`,
@@ -285,7 +390,7 @@ export const BarChartVertical = () => {
285
390
  height: barHeight,
286
391
  x: barX,
287
392
  y: barY,
288
- onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data),
393
+ onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data, bar.value),
289
394
  onMouseLeave: onMouseLeaveBar,
290
395
  tooltipHtml: tooltip,
291
396
  tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`,
@@ -304,6 +409,32 @@ export const BarChartVertical = () => {
304
409
  }
305
410
  })}
306
411
 
412
+ {/* Pattern overlay if pattern exists */}
413
+ {patternUrl &&
414
+ createBarElement({
415
+ config: config,
416
+ index: newIndex,
417
+ background: patternUrl, // Use pattern as background
418
+ borderColor: 'transparent',
419
+ borderStyle: 'none',
420
+ borderWidth: '0px',
421
+ width: barWidth,
422
+ height: barHeight,
423
+ x: barX,
424
+ y: barY,
425
+ onMouseOver: () => {}, // No interaction
426
+ onMouseLeave: () => {}, // No interaction
427
+ tooltipHtml: '',
428
+ tooltipId: '',
429
+ onClick: () => {}, // No interaction
430
+ styleOverrides: {
431
+ transformOrigin: `0 ${barY + barHeight}px`,
432
+ opacity: transparentBar ? 0.2 : 1,
433
+ display: displayBar ? 'block' : 'none',
434
+ pointerEvents: 'none' // Let clicks pass through to base bar
435
+ }
436
+ })}
437
+
307
438
  {(absentDataLabel || isSuppressed) && (
308
439
  <rect
309
440
  x={barX}
@@ -337,13 +468,7 @@ export const BarChartVertical = () => {
337
468
  const hasAsterisk = String(pd.symbol).includes('Asterisk')
338
469
  const yPadding = hasAsterisk ? -5 : -8
339
470
  const verticalAnchor = hasAsterisk ? 'middle' : 'end'
340
- const iconSize =
341
- pd.symbol === 'Asterisk'
342
- ? barWidth * 1.2
343
- : pd.symbol === 'Double Asterisk'
344
- ? barWidth
345
- : barWidth / 1.5
346
- const fillColor = pd.displayGray ? '#8b8b8a' : '#000'
471
+ const fillColor = pd.displayGray ? '#8b8b8a' : APP_FONT_COLOR
347
472
 
348
473
  return (
349
474
  <Text // prettier-ignore
@@ -356,7 +481,7 @@ export const BarChartVertical = () => {
356
481
  verticalAnchor={verticalAnchor}
357
482
  fill={fillColor}
358
483
  textAnchor='middle'
359
- fontSize={`${iconSize}px`}
484
+ fontSize={LABEL_FONT_SIZE}
360
485
  >
361
486
  {pd.iconCode}
362
487
  </Text>
@@ -390,7 +515,11 @@ export const BarChartVertical = () => {
390
515
  cx={barX + lollipopShapeSize / 3.5}
391
516
  cy={bar.y}
392
517
  r={lollipopShapeSize / 2}
393
- fill={getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))}
518
+ fill={
519
+ isTwoToneLollipopColor
520
+ ? getLollipopHeadColor(colorScale(config.runtime.seriesLabels[bar.key]))
521
+ : colorScale(config.runtime.seriesLabels[bar.key])
522
+ }
394
523
  key={`circle--${bar.index}`}
395
524
  data-tooltip-html={tooltip}
396
525
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
@@ -404,7 +533,11 @@ export const BarChartVertical = () => {
404
533
  y={bar.y}
405
534
  width={lollipopShapeSize}
406
535
  height={lollipopShapeSize}
407
- fill={getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))}
536
+ fill={
537
+ isTwoToneLollipopColor
538
+ ? getLollipopHeadColor(colorScale(config.runtime.seriesLabels[bar.key]))
539
+ : colorScale(config.runtime.seriesLabels[bar.key])
540
+ }
408
541
  key={`circle--${bar.index}`}
409
542
  data-tooltip-html={tooltip}
410
543
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}