@cdc/chart 4.25.7 → 4.25.10

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 (95) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/cdcchart.js +39551 -37016
  3. package/examples/feature/__data__/planet-example-data.json +0 -30
  4. package/examples/grouped-bar-test.json +400 -0
  5. package/examples/private/d.json +382 -0
  6. package/examples/private/example-2.json +49784 -0
  7. package/examples/private/f2.json +1 -0
  8. package/examples/private/f4.json +1577 -0
  9. package/examples/private/forecast.json +1180 -0
  10. package/examples/private/lollipop.json +468 -0
  11. package/examples/private/new.json +48756 -0
  12. package/examples/private/pie-chart-legend.json +904 -0
  13. package/examples/suppressed_tooltip.json +480 -0
  14. package/index.html +10 -22
  15. package/package.json +25 -7
  16. package/src/CdcChart.tsx +10 -4
  17. package/src/CdcChartComponent.tsx +188 -32
  18. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  19. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  20. package/src/_stories/Chart.CI.stories.tsx +1 -1
  21. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  22. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  23. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  24. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  25. package/src/_stories/Chart.Patterns.stories.tsx +19 -0
  26. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  27. package/src/_stories/Chart.stories.tsx +8 -5
  28. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  29. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  30. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  31. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  32. package/src/_stories/ChartEditor.stories.tsx +60 -60
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  34. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  35. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  36. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  38. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  39. package/src/components/BarChart/components/BarChart.Horizontal.tsx +170 -25
  40. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +139 -6
  41. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  42. package/src/components/BarChart/components/BarChart.Vertical.tsx +172 -23
  43. package/src/components/BarChart/helpers/index.ts +43 -4
  44. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  45. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  46. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  47. package/src/components/Brush/BrushChart.tsx +65 -10
  48. package/src/components/Brush/BrushController.tsx +37 -5
  49. package/src/components/Brush/types.tsx +8 -0
  50. package/src/components/DeviationBar.jsx +9 -6
  51. package/src/components/EditorPanel/EditorPanel.tsx +364 -39
  52. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +2 -2
  54. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +30 -54
  56. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
  57. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  58. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  59. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  60. package/src/components/Forecasting/Forecasting.tsx +36 -6
  61. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  62. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  63. package/src/components/Legend/Legend.Component.tsx +110 -2
  64. package/src/components/Legend/Legend.tsx +3 -1
  65. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  66. package/src/components/LegendWrapper.tsx +1 -1
  67. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +27 -26
  68. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  69. package/src/components/LineChart/index.tsx +2 -2
  70. package/src/components/LinearChart.tsx +26 -9
  71. package/src/components/PairedBarChart.jsx +6 -4
  72. package/src/components/PieChart/PieChart.tsx +170 -54
  73. package/src/components/Sankey/components/Sankey.tsx +7 -1
  74. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  75. package/src/data/initial-state.js +315 -292
  76. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  77. package/src/helpers/buildForecastPaletteOptions.ts +109 -0
  78. package/src/helpers/getColorScale.ts +72 -8
  79. package/src/helpers/getNewRuntime.ts +1 -1
  80. package/src/helpers/getTransformedData.ts +1 -1
  81. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  82. package/src/hooks/useReduceData.ts +105 -70
  83. package/src/hooks/useTooltip.tsx +58 -16
  84. package/src/index.jsx +6 -3
  85. package/src/scss/main.scss +12 -0
  86. package/src/store/chart.reducer.ts +1 -1
  87. package/src/test/CdcChart.test.jsx +8 -3
  88. package/src/types/ChartConfig.ts +30 -6
  89. package/src/types/ChartContext.ts +1 -0
  90. package/vite.config.js +1 -1
  91. package/vitest.config.ts +16 -0
  92. package/src/coreStyles_chart.scss +0 -3
  93. package/src/helpers/configHelpers.ts +0 -28
  94. package/src/helpers/generateColorsArray.ts +0 -8
  95. package/src/hooks/useColorPalette.js +0 -76
@@ -8,18 +8,17 @@ import { useHighlightedBars } from '../../../hooks/useHighlightedBars'
8
8
  import { Group } from '@visx/group'
9
9
  import { Text } from '@visx/text'
10
10
  import { BarGroup } from '@visx/shape'
11
+ import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
11
12
 
12
13
  // CDC core components and helpers
13
14
  import { getColorContrast, getContrastColor } from '@cdc/core/helpers/cove/accessibility'
14
15
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
16
+ import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
15
17
  import createBarElement from '@cdc/core/components/createBarElement'
16
- import { getBarConfig, testZeroValue } from '../helpers'
18
+ import { getBarConfig, testZeroValue, getLollipopStemColor, getLollipopHeadColor } from '../helpers'
17
19
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
18
20
  import isNumber from '@cdc/core/helpers/isNumber'
19
21
 
20
- // Third party libraries
21
- import chroma from 'chroma-js'
22
-
23
22
  // Local context and types
24
23
  import BarChartContext, { BarChartContextValues } from './context'
25
24
  import { ChartContext } from '../../../types/ChartContext'
@@ -54,15 +53,85 @@ export const BarChartHorizontal = () => {
54
53
  formatNumber,
55
54
  formatDate,
56
55
  parseDate,
57
- setSharedFilter
56
+ setSharedFilter,
57
+ currentViewport
58
58
  } = useContext<ChartContext>(ConfigContext)
59
59
 
60
60
  const { HighLightedBarUtils } = useHighlightedBars(config)
61
61
 
62
+ const LABEL_FONT_SIZE = isMobileFontViewport(currentViewport) ? 13 : 16
63
+
62
64
  const hasConfidenceInterval = [config.confidenceKeys?.upper, config.confidenceKeys?.lower].every(
63
65
  v => v != null && String(v).trim() !== ''
64
66
  )
65
67
 
68
+ // Pattern helper function
69
+ const renderPatternDefs = () => {
70
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
71
+ return null
72
+ }
73
+
74
+ return (
75
+ <defs>
76
+ {Object.entries(config.legend.patterns).map(([key, pattern]) => {
77
+ const patternId = `chart-pattern-${key}`
78
+ const size = pattern.patternSize || 8
79
+
80
+ switch (pattern.shape) {
81
+ case 'circles':
82
+ return (
83
+ <PatternCircles
84
+ key={patternId}
85
+ id={patternId}
86
+ height={size}
87
+ width={size}
88
+ fill={pattern.color}
89
+ radius={1.25}
90
+ />
91
+ )
92
+ case 'lines':
93
+ return (
94
+ <PatternLines
95
+ key={patternId}
96
+ id={patternId}
97
+ height={size}
98
+ width={size}
99
+ stroke={pattern.color}
100
+ strokeWidth={0.75}
101
+ orientation={['horizontal']}
102
+ />
103
+ )
104
+ case 'diagonalLines':
105
+ return (
106
+ <PatternLines
107
+ key={patternId}
108
+ id={patternId}
109
+ height={size}
110
+ width={size}
111
+ stroke={pattern.color}
112
+ strokeWidth={0.75}
113
+ orientation={['diagonalRightToLeft']}
114
+ />
115
+ )
116
+ case 'waves':
117
+ return (
118
+ <PatternWaves
119
+ key={patternId}
120
+ id={patternId}
121
+ height={size}
122
+ width={size}
123
+ fill={pattern.color}
124
+ strokeWidth={0.25}
125
+ />
126
+ )
127
+ default:
128
+ return null
129
+ }
130
+ })}
131
+ </defs>
132
+ )
133
+ }
134
+
66
135
  const _data = getBarData(config, data, hasConfidenceInterval)
67
136
 
68
137
  return (
@@ -70,6 +139,7 @@ export const BarChartHorizontal = () => {
70
139
  config.visualizationType === 'Bar' &&
71
140
  config.orientation === 'horizontal' && (
72
141
  <Group>
142
+ {renderPatternDefs()}
73
143
  <BarGroup
74
144
  data={config.preliminaryData?.some(pd => pd.value && pd.type === 'suppression') ? tableData : _data}
75
145
  keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
@@ -115,9 +185,15 @@ export const BarChartHorizontal = () => {
115
185
  numbericBarHeight = 25
116
186
  }
117
187
  let barY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(scaleVal)
118
- const defaultBarWidth = Math.abs(xScale(bar.value) - xScale(scaleVal))
188
+ let defaultBarWidth = Math.abs(xScale(bar.value) - xScale(scaleVal))
119
189
  const isPositiveBar = bar.value >= 0 && isNumber(bar.value)
120
190
 
191
+ const MINIMUM_BAR_HEIGHT = 3
192
+ if (isPositiveBar && barGroup.bars.length === 1 && defaultBarWidth < MINIMUM_BAR_HEIGHT) {
193
+ defaultBarWidth = MINIMUM_BAR_HEIGHT
194
+ barY = yScale(0) - MINIMUM_BAR_HEIGHT
195
+ }
196
+
121
197
  const barX = bar.value < 0 ? Math.abs(xScale(bar.value)) : xScale(scaleVal)
122
198
  const yAxisValue = formatNumber(bar.value, 'left')
123
199
  const xAxisValue =
@@ -126,7 +202,15 @@ export const BarChartHorizontal = () => {
126
202
  barWidthHorizontal: barWidth,
127
203
  isSuppressed,
128
204
  absentDataLabel
129
- } = getBarConfig({ bar, defaultBarWidth, config, isVertical: false, yAxisValue, barWidth: 0 })
205
+ } = getBarConfig({
206
+ bar,
207
+ defaultBarWidth,
208
+ config,
209
+ isVertical: false,
210
+ yAxisValue,
211
+ barWidth: 0,
212
+ labelFontSize: LABEL_FONT_SIZE
213
+ })
130
214
 
131
215
  const barPosition = !isPositiveBar ? 'below' : 'above'
132
216
 
@@ -166,14 +250,14 @@ export const BarChartHorizontal = () => {
166
250
  </li></ul>`
167
251
 
168
252
  // configure colors
169
- let labelColor = '#000000'
253
+ let labelColor = APP_FONT_COLOR
170
254
  labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
171
255
  let barColor =
172
256
  config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key]
173
257
  ? colorScale(config.runtime.seriesLabels[bar.key])
174
258
  : colorScale(bar.key)
175
259
  const hasDynamicCategory = config.series.find(s => s.dynamicCategory)
176
- if (!hasDynamicCategory) {
260
+ if (!hasDynamicCategory && config.legend.colorCode) {
177
261
  barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor) // Color code by category
178
262
  }
179
263
  const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
@@ -184,7 +268,7 @@ export const BarChartHorizontal = () => {
184
268
  const borderColor = isHighlightedBar
185
269
  ? highlightedBarColor
186
270
  : config.barHasBorder === 'true'
187
- ? '#000'
271
+ ? APP_FONT_COLOR
188
272
  : 'transparent'
189
273
  const borderWidth = isHighlightedBar
190
274
  ? highlightedBar.borderWidth
@@ -209,13 +293,23 @@ export const BarChartHorizontal = () => {
209
293
  labelColor = '#fff'
210
294
  }
211
295
  }
212
- const background = () => {
296
+ const getBarBackgroundColor = () => {
213
297
  if (isRegularLollipopColor) return barColor
214
- if (isTwoToneLollipopColor) return chroma(barColor).brighten(1)
298
+ if (isTwoToneLollipopColor) {
299
+ return getLollipopStemColor(barColor)
300
+ }
215
301
  if (isHighlightedBar) return 'transparent'
216
302
  return barColor
217
303
  }
218
304
 
305
+ // Function to get the lollipop head color (should be darker than stem)
306
+ const getLocalLollipopHeadColor = (): string => {
307
+ if (!isTwoToneLollipopColor) {
308
+ return barColor
309
+ }
310
+ return getLollipopHeadColor(barColor)
311
+ }
312
+
219
313
  // Confidence Interval Variables
220
314
  const tickWidth = 5
221
315
  const yPos = barHeight * bar.index + barHeight / 2
@@ -231,14 +325,45 @@ export const BarChartHorizontal = () => {
231
325
  return xScale(d)
232
326
  })
233
327
 
328
+ // Check if this bar should use a pattern
329
+ const getPatternUrl = (): string | null => {
330
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
331
+ return null
332
+ }
333
+
334
+ // Find a pattern that matches this specific bar
335
+ for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
336
+ if (pattern.dataKey && pattern.dataValue) {
337
+ // For grouped bar charts, check if the pattern's dataKey matches the current bar's series key
338
+ // and if the pattern's dataValue matches the current bar's value
339
+ if (pattern.dataKey === bar.key && String(bar.value) === String(pattern.dataValue)) {
340
+ return `url(#chart-pattern-${patternKey})`
341
+ }
342
+ // Fallback for non-grouped charts: check datum field value
343
+ else if (!config.series || config.series.length <= 1) {
344
+ const dataFieldValue = datum[pattern.dataKey]
345
+ if (String(dataFieldValue) === String(pattern.dataValue)) {
346
+ return `url(#chart-pattern-${patternKey})`
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ return null
353
+ }
354
+
355
+ const patternUrl = getPatternUrl()
356
+ const baseBackground = getBarBackgroundColor()
357
+
234
358
  return (
235
359
  <Group display={hideGroup} key={`${barGroup.index}--${index}`}>
236
360
  <Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
361
+ {/* Base colored bar */}
237
362
  {createBarElement({
238
363
  config: config,
239
364
  index: newIndex,
240
365
  id: `barGroup${barGroup.index}`,
241
- background: background(),
366
+ background: baseBackground,
242
367
  borderColor,
243
368
  borderStyle: 'solid',
244
369
  borderWidth: `${borderWidth}px`,
@@ -246,7 +371,7 @@ export const BarChartHorizontal = () => {
246
371
  height: numbericBarHeight,
247
372
  x: barX,
248
373
  y: barHeight * bar.index,
249
- onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data),
374
+ onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data, bar.value),
250
375
  onMouseLeave: onMouseLeaveBar,
251
376
  tooltipHtml: tooltip,
252
377
  tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`,
@@ -264,6 +389,32 @@ export const BarChartHorizontal = () => {
264
389
  }
265
390
  })}
266
391
 
392
+ {/* Pattern overlay if pattern exists */}
393
+ {patternUrl &&
394
+ createBarElement({
395
+ config: config,
396
+ index: newIndex,
397
+ background: patternUrl, // Use pattern as background
398
+ borderColor: 'transparent',
399
+ borderStyle: 'none',
400
+ borderWidth: '0px',
401
+ width: barWidth,
402
+ height: numbericBarHeight,
403
+ x: barX,
404
+ y: barHeight * bar.index,
405
+ onMouseOver: () => {}, // No interaction
406
+ onMouseLeave: () => {}, // No interaction
407
+ tooltipHtml: '',
408
+ tooltipId: '',
409
+ onClick: () => {}, // No interaction
410
+ styleOverrides: {
411
+ transformOrigin: `0 ${barY + barHeight}px`,
412
+ opacity: transparentBar ? 0.2 : 1,
413
+ display: displayBar ? 'block' : 'none',
414
+ pointerEvents: 'none' // Let clicks pass through to base bar
415
+ }
416
+ })}
417
+
267
418
  {(absentDataLabel || isSuppressed) && (
268
419
  <rect
269
420
  x={barX}
@@ -292,17 +443,11 @@ export const BarChartHorizontal = () => {
292
443
 
293
444
  const hasAsterisk = String(pd.symbol).includes('Asterisk')
294
445
  const verticalAnchor = hasAsterisk ? 'middle' : 'end'
295
- const iconSize =
296
- pd.symbol === 'Asterisk'
297
- ? barHeight * 1.2
298
- : pd.symbol === 'Double Asterisk'
299
- ? barHeight
300
- : barHeight / 1.5
301
- const fillColor = pd.displayGray ? '#8b8b8a' : '#000'
446
+ const fillColor = pd.displayGray ? '#8b8b8a' : APP_FONT_COLOR
302
447
  return (
303
448
  <Text // prettier-ignore
304
449
  key={index}
305
- fontSize={iconSize}
450
+ fontSize={LABEL_FONT_SIZE}
306
451
  display={displayBar ? 'block' : 'none'}
307
452
  opacity={transparentBar ? 0.5 : 1}
308
453
  x={barX}
@@ -366,7 +511,7 @@ export const BarChartHorizontal = () => {
366
511
  display={displayBar ? 'block' : 'none'}
367
512
  x={bar.y}
368
513
  y={0}
369
- fill={'#000000'}
514
+ fill={APP_FONT_COLOR}
370
515
  dx={textPaddingLollipop}
371
516
  textAnchor={textAnchorLollipop}
372
517
  verticalAnchor='middle'
@@ -397,7 +542,7 @@ export const BarChartHorizontal = () => {
397
542
  cx={bar.y}
398
543
  cy={barHeight * bar.index + lollipopBarWidth / 2}
399
544
  r={lollipopShapeSize / 2}
400
- fill={barColor}
545
+ fill={getLocalLollipopHeadColor()}
401
546
  key={`circle--${bar.index}`}
402
547
  data-tooltip-html={tooltip}
403
548
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
@@ -411,7 +556,7 @@ export const BarChartHorizontal = () => {
411
556
  y={0 - lollipopBarWidth / 2}
412
557
  width={lollipopShapeSize}
413
558
  height={lollipopShapeSize}
414
- fill={barColor}
559
+ fill={getLocalLollipopHeadColor()}
415
560
  key={`circle--${bar.index}`}
416
561
  data-tooltip-html={tooltip}
417
562
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
@@ -3,6 +3,7 @@ import ConfigContext from '../../../ConfigContext'
3
3
  import { BarStackHorizontal } from '@visx/shape'
4
4
  import { Group } from '@visx/group'
5
5
  import { Text } from '@visx/text'
6
+ import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
6
7
  import { getColorContrast, getContrastColor } from '@cdc/core/helpers/cove/accessibility'
7
8
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
8
9
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
@@ -43,10 +44,79 @@ const BarChartStackedHorizontal = () => {
43
44
  } = barChart
44
45
 
45
46
  const { orientation, visualizationSubType } = config
47
+
48
+ // Pattern helper function
49
+ const renderPatternDefs = () => {
50
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
51
+ return null
52
+ }
53
+
54
+ return (
55
+ <defs>
56
+ {Object.entries(config.legend.patterns).map(([key, pattern]) => {
57
+ const patternId = `chart-pattern-${key}`
58
+ const size = pattern.patternSize || 8
59
+
60
+ switch (pattern.shape) {
61
+ case 'circles':
62
+ return (
63
+ <PatternCircles
64
+ key={patternId}
65
+ id={patternId}
66
+ height={size}
67
+ width={size}
68
+ fill={pattern.color}
69
+ radius={1.25}
70
+ />
71
+ )
72
+ case 'lines':
73
+ return (
74
+ <PatternLines
75
+ key={patternId}
76
+ id={patternId}
77
+ height={size}
78
+ width={size}
79
+ stroke={pattern.color}
80
+ strokeWidth={0.75}
81
+ orientation={['horizontal']}
82
+ />
83
+ )
84
+ case 'diagonalLines':
85
+ return (
86
+ <PatternLines
87
+ key={patternId}
88
+ id={patternId}
89
+ height={size}
90
+ width={size}
91
+ stroke={pattern.color}
92
+ strokeWidth={0.75}
93
+ orientation={['diagonalRightToLeft']}
94
+ />
95
+ )
96
+ case 'waves':
97
+ return (
98
+ <PatternWaves
99
+ key={patternId}
100
+ id={patternId}
101
+ height={size}
102
+ width={size}
103
+ fill={pattern.color}
104
+ strokeWidth={0.25}
105
+ />
106
+ )
107
+ default:
108
+ return null
109
+ }
110
+ })}
111
+ </defs>
112
+ )
113
+ }
114
+
46
115
  return (
47
116
  config.visualizationSubType === 'stacked' &&
48
117
  isHorizontal && (
49
118
  <>
119
+ {renderPatternDefs()}
50
120
  <BarStackHorizontal
51
121
  data={data}
52
122
  keys={barStackedSeriesKeys}
@@ -58,7 +128,7 @@ const BarChartStackedHorizontal = () => {
58
128
  offset='none'
59
129
  >
60
130
  {barStacks =>
61
- barStacks.map(barStack =>
131
+ barStacks.map((barStack, stackIndex) =>
62
132
  getHorizontalBarHeights(config, barStack.bars).map((bar, index) => {
63
133
  const transparentBar =
64
134
  config.legend.behavior === 'highlight' &&
@@ -73,7 +143,7 @@ const BarChartStackedHorizontal = () => {
73
143
  let labelColor = getContrastColor(APP_FONT_COLOR, barColor)
74
144
  let constrast = getColorContrast(APP_FONT_COLOR, barColor)
75
145
  const contrastLevel = 7
76
- if (constrast < contrastLevel) {
146
+ if (typeof constrast === 'number' && constrast < contrastLevel) {
77
147
  labelColor = '#fff'
78
148
  }
79
149
  // tooltips
@@ -94,9 +164,41 @@ const BarChartStackedHorizontal = () => {
94
164
  <li class="tooltip-body ">${additionalColTooltip}</li>
95
165
  </li></ul>`
96
166
 
167
+ // Check if this bar should use a pattern
168
+ const getPatternUrl = (): string | null => {
169
+ if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
170
+ return null
171
+ }
172
+
173
+ // Find a pattern that matches this specific bar
174
+ for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
175
+ if (pattern.dataKey && pattern.dataValue) {
176
+ // For stacked bar charts, check if the pattern's dataKey matches the current bar's series key
177
+ // and if the pattern's dataValue matches the current bar's value
178
+ const barValue = data[bar.index][bar.key]
179
+ if (pattern.dataKey === bar.key && String(barValue) === String(pattern.dataValue)) {
180
+ return `url(#chart-pattern-${patternKey})`
181
+ }
182
+ // Fallback for non-series pattern matching (like the original stacked pattern test)
183
+ // Only check this if the pattern dataKey is NOT a series key
184
+ else if (!config.runtime.seriesLabels || !config.runtime.seriesLabels[pattern.dataKey]) {
185
+ const dataFieldValue = data[bar.index][pattern.dataKey]
186
+ if (String(dataFieldValue) === String(pattern.dataValue)) {
187
+ return `url(#chart-pattern-${patternKey})`
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ return null
194
+ }
195
+
196
+ const patternUrl = getPatternUrl()
197
+
97
198
  return (
98
- <>
199
+ <React.Fragment key={`stack-${stackIndex}-bar-${index}-${barStack.index}`}>
99
200
  <Group key={index} id={`barStack${barStack.index}-${bar.index}`} className='stack horizontal'>
201
+ {/* Base colored bar */}
100
202
  {createBarElement({
101
203
  config: config,
102
204
  seriesHighlight,
@@ -110,7 +212,7 @@ const BarChartStackedHorizontal = () => {
110
212
  height: bar.height,
111
213
  x: bar.x,
112
214
  y: bar.y,
113
- onMouseOver: e => onMouseOverBar(yAxisValue, bar.key, e, data),
215
+ onMouseOver: e => onMouseOverBar(yAxisValue, bar.key, e, data, bar.bar.data[bar.key]),
114
216
  onMouseLeave: onMouseLeaveBar,
115
217
  tooltipHtml: tooltip,
116
218
  tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`,
@@ -129,6 +231,37 @@ const BarChartStackedHorizontal = () => {
129
231
  }
130
232
  })}
131
233
 
234
+ {/* Pattern overlay using createBarElement for consistent animation */}
235
+ {patternUrl &&
236
+ createBarElement({
237
+ config: config,
238
+ seriesHighlight,
239
+ index: barStack.index,
240
+ className: `animated-chart pattern-overlay ${animatedChart ? 'animated' : ''}`,
241
+ background: patternUrl, // Use pattern as background
242
+ borderColor: 'transparent',
243
+ borderStyle: 'none',
244
+ borderWidth: '0px',
245
+ width: bar.width,
246
+ height: bar.height,
247
+ x: bar.x,
248
+ y: bar.y,
249
+ onMouseOver: () => {}, // No interaction
250
+ onMouseLeave: () => {}, // No interaction
251
+ tooltipHtml: '',
252
+ tooltipId: '',
253
+ onClick: () => {}, // No interaction
254
+ styleOverrides: {
255
+ animationDelay: `${barStack.index * 0.5}s`,
256
+ transformOrigin: `${bar.x}px 0`,
257
+ opacity: animatedChart ? 0 : transparentBar ? 0.2 : 1, // Start hidden if animated
258
+ display: displayBar ? 'block' : 'none',
259
+ pointerEvents: 'none', // Let clicks pass through to base bar
260
+ // Force the initial transform state to match CSS animation
261
+ transform: animatedChart ? 'scale(0, 1)' : 'scale(1, 1)'
262
+ }
263
+ })}
264
+
132
265
  {orientation === 'horizontal' &&
133
266
  visualizationSubType === 'stacked' &&
134
267
  isLabelBelowBar &&
@@ -137,7 +270,7 @@ const BarChartStackedHorizontal = () => {
137
270
  <Text
138
271
  x={`${bar.x + (config.isLollipopChart ? 15 : 5)}`} // padding
139
272
  y={bar.y + bar.height * 1.2}
140
- fill={'#000000'}
273
+ fill={APP_FONT_COLOR}
141
274
  textAnchor='start'
142
275
  verticalAnchor='start'
143
276
  >
@@ -158,7 +291,7 @@ const BarChartStackedHorizontal = () => {
158
291
  </Text>
159
292
  )}
160
293
  </Group>
161
- </>
294
+ </React.Fragment>
162
295
  )
163
296
  })
164
297
  )