@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
@@ -1,4 +1,4 @@
1
- import React, { useContext } from 'react'
1
+ import { useContext } from 'react'
2
2
 
3
3
  // Local context and hooks
4
4
  import ConfigContext from '../../../ConfigContext'
@@ -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'
@@ -27,7 +26,7 @@ import _ from 'lodash'
27
26
  import { getBarData } from '../helpers/getBarData'
28
27
  import { getHorizontalBarHeights } from '../helpers/getBarHeights'
29
28
 
30
- export const BarChartHorizontal = () => {
29
+ const BarChartHorizontal = () => {
31
30
  const { xScale, yScale, yMax, seriesScale, barChart } = useContext<BarChartContextValues>(BarChartContext)
32
31
  const {
33
32
  isHorizontal,
@@ -54,15 +53,85 @@ export const BarChartHorizontal = () => {
54
53
  formatNumber,
55
54
  formatDate,
56
55
  parseDate,
57
- setSharedFilter
56
+ setSharedFilter,
57
+ vizViewport
58
58
  } = useContext<ChartContext>(ConfigContext)
59
59
 
60
60
  const { HighLightedBarUtils } = useHighlightedBars(config)
61
61
 
62
+ const LABEL_FONT_SIZE = isMobileFontViewport(vizViewport) ? 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}
@@ -132,7 +202,15 @@ export const BarChartHorizontal = () => {
132
202
  barWidthHorizontal: barWidth,
133
203
  isSuppressed,
134
204
  absentDataLabel
135
- } = 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
+ })
136
214
 
137
215
  const barPosition = !isPositiveBar ? 'below' : 'above'
138
216
 
@@ -179,7 +257,7 @@ export const BarChartHorizontal = () => {
179
257
  ? colorScale(config.runtime.seriesLabels[bar.key])
180
258
  : colorScale(bar.key)
181
259
  const hasDynamicCategory = config.series.find(s => s.dynamicCategory)
182
- if (!hasDynamicCategory) {
260
+ if (!hasDynamicCategory && config.legend.colorCode) {
183
261
  barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor) // Color code by category
184
262
  }
185
263
  const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
@@ -215,13 +293,23 @@ export const BarChartHorizontal = () => {
215
293
  labelColor = '#fff'
216
294
  }
217
295
  }
218
- const background = () => {
296
+ const getBarBackgroundColor = () => {
219
297
  if (isRegularLollipopColor) return barColor
220
- if (isTwoToneLollipopColor) return chroma(barColor).brighten(1)
298
+ if (isTwoToneLollipopColor) {
299
+ return getLollipopStemColor(barColor)
300
+ }
221
301
  if (isHighlightedBar) return 'transparent'
222
302
  return barColor
223
303
  }
224
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
+
225
313
  // Confidence Interval Variables
226
314
  const tickWidth = 5
227
315
  const yPos = barHeight * bar.index + barHeight / 2
@@ -237,14 +325,45 @@ export const BarChartHorizontal = () => {
237
325
  return xScale(d)
238
326
  })
239
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
+
240
358
  return (
241
359
  <Group display={hideGroup} key={`${barGroup.index}--${index}`}>
242
360
  <Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
361
+ {/* Base colored bar */}
243
362
  {createBarElement({
244
363
  config: config,
245
364
  index: newIndex,
246
365
  id: `barGroup${barGroup.index}`,
247
- background: background(),
366
+ background: baseBackground,
248
367
  borderColor,
249
368
  borderStyle: 'solid',
250
369
  borderWidth: `${borderWidth}px`,
@@ -252,7 +371,7 @@ export const BarChartHorizontal = () => {
252
371
  height: numbericBarHeight,
253
372
  x: barX,
254
373
  y: barHeight * bar.index,
255
- onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data),
374
+ onMouseOver: e => onMouseOverBar(xAxisValue, bar.key, e, data, bar.value),
256
375
  onMouseLeave: onMouseLeaveBar,
257
376
  tooltipHtml: tooltip,
258
377
  tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`,
@@ -270,6 +389,32 @@ export const BarChartHorizontal = () => {
270
389
  }
271
390
  })}
272
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
+
273
418
  {(absentDataLabel || isSuppressed) && (
274
419
  <rect
275
420
  x={barX}
@@ -298,17 +443,11 @@ export const BarChartHorizontal = () => {
298
443
 
299
444
  const hasAsterisk = String(pd.symbol).includes('Asterisk')
300
445
  const verticalAnchor = hasAsterisk ? 'middle' : 'end'
301
- const iconSize =
302
- pd.symbol === 'Asterisk'
303
- ? barHeight * 1.2
304
- : pd.symbol === 'Double Asterisk'
305
- ? barHeight
306
- : barHeight / 1.5
307
446
  const fillColor = pd.displayGray ? '#8b8b8a' : APP_FONT_COLOR
308
447
  return (
309
448
  <Text // prettier-ignore
310
449
  key={index}
311
- fontSize={iconSize}
450
+ fontSize={LABEL_FONT_SIZE}
312
451
  display={displayBar ? 'block' : 'none'}
313
452
  opacity={transparentBar ? 0.5 : 1}
314
453
  x={barX}
@@ -403,7 +542,7 @@ export const BarChartHorizontal = () => {
403
542
  cx={bar.y}
404
543
  cy={barHeight * bar.index + lollipopBarWidth / 2}
405
544
  r={lollipopShapeSize / 2}
406
- fill={barColor}
545
+ fill={getLocalLollipopHeadColor()}
407
546
  key={`circle--${bar.index}`}
408
547
  data-tooltip-html={tooltip}
409
548
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
@@ -417,7 +556,7 @@ export const BarChartHorizontal = () => {
417
556
  y={0 - lollipopBarWidth / 2}
418
557
  width={lollipopShapeSize}
419
558
  height={lollipopShapeSize}
420
- fill={barColor}
559
+ fill={getLocalLollipopHeadColor()}
421
560
  key={`circle--${bar.index}`}
422
561
  data-tooltip-html={tooltip}
423
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 &&
@@ -158,7 +291,7 @@ const BarChartStackedHorizontal = () => {
158
291
  </Text>
159
292
  )}
160
293
  </Group>
161
- </>
294
+ </React.Fragment>
162
295
  )
163
296
  })
164
297
  )