@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.
- package/.claude/settings.local.json +9 -0
- package/dist/cdcchart.js +39551 -37016
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/d.json +382 -0
- package/examples/private/example-2.json +49784 -0
- package/examples/private/f2.json +1 -0
- package/examples/private/f4.json +1577 -0
- package/examples/private/forecast.json +1180 -0
- package/examples/private/lollipop.json +468 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +10 -22
- package/package.json +25 -7
- package/src/CdcChart.tsx +10 -4
- package/src/CdcChartComponent.tsx +188 -32
- package/src/_stories/Chart.Anchors.stories.tsx +2 -2
- package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
- package/src/_stories/Chart.CI.stories.tsx +1 -1
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
- package/src/_stories/Chart.Filters.stories.tsx +2 -2
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +19 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +1 -1
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartEditor.stories.tsx +60 -60
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
- package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
- package/src/_stories/_mock/stacked-pattern-test.json +520 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +170 -25
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +139 -6
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
- package/src/components/BarChart/components/BarChart.Vertical.tsx +172 -23
- package/src/components/BarChart/helpers/index.ts +43 -4
- package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
- package/src/components/BarChart/helpers/useBarChart.ts +25 -3
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
- package/src/components/Brush/BrushChart.tsx +65 -10
- package/src/components/Brush/BrushController.tsx +37 -5
- package/src/components/Brush/types.tsx +8 -0
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +364 -39
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +30 -54
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/Forecasting/Forecasting.tsx +36 -6
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +110 -2
- package/src/components/Legend/Legend.tsx +3 -1
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +27 -26
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/index.tsx +2 -2
- package/src/components/LinearChart.tsx +26 -9
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- package/src/data/initial-state.js +315 -292
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +109 -0
- package/src/helpers/getColorScale.ts +72 -8
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useTooltip.tsx +58 -16
- package/src/index.jsx +6 -3
- package/src/scss/main.scss +12 -0
- package/src/store/chart.reducer.ts +1 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +30 -6
- package/src/types/ChartContext.ts +1 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- 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
|
-
|
|
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({
|
|
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 =
|
|
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
|
-
?
|
|
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
|
|
296
|
+
const getBarBackgroundColor = () => {
|
|
213
297
|
if (isRegularLollipopColor) return barColor
|
|
214
|
-
if (isTwoToneLollipopColor)
|
|
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:
|
|
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
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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
|
)
|