@cdc/chart 4.26.2 → 4.26.4

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 (165) hide show
  1. package/CONFIG.md +169 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39419 -35180
  4. package/examples/__data__/data-with-metadata.json +10 -0
  5. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  6. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  7. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  8. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  9. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  10. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  11. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  12. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  13. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  14. package/examples/metadata-variables.json +58 -0
  15. package/examples/minimal-example.json +24 -0
  16. package/examples/private/test2.json +35012 -0
  17. package/package.json +4 -4
  18. package/src/CdcChart.tsx +8 -4
  19. package/src/CdcChartComponent.tsx +326 -290
  20. package/src/_stories/Chart.CustomColors.smoke.stories.tsx +97 -0
  21. package/src/_stories/Chart.Defaults.smoke.stories.tsx +95 -0
  22. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  23. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  24. package/src/_stories/{Chart.SmallMultiples.stories.tsx → Chart.SmallMultiples.smoke.stories.tsx} +17 -1
  25. package/src/_stories/Chart.SmallestLeftAxisMax.smoke.stories.tsx +64 -0
  26. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +48 -2
  27. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  28. package/src/_stories/ChartBar.Editor.stories.tsx +177 -115
  29. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  30. package/src/_stories/ChartEditor.Editor.stories.tsx +24 -1
  31. package/src/_stories/{Chart.CustomColors.stories.tsx → ChartLine.DashedDates.smoke.stories.tsx} +4 -4
  32. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  33. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  34. package/src/_stories/_mock/paired-bar.json +1 -1
  35. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  36. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.styles.css +10 -10
  38. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
  39. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
  40. package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
  41. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  42. package/src/components/Axis/BottomAxis.tsx +10 -3
  43. package/src/components/Axis/LeftAxis.tsx +2 -4
  44. package/src/components/Axis/LeftAxisGridlines.tsx +1 -3
  45. package/src/components/Axis/PairedBarAxis.tsx +10 -4
  46. package/src/components/BarChart/components/BarChart.Horizontal.tsx +21 -33
  47. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +21 -35
  48. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +21 -36
  49. package/src/components/BarChart/components/BarChart.Vertical.tsx +21 -34
  50. package/src/components/BarChart/components/BarChart.tsx +2 -6
  51. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  52. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  53. package/src/components/BarChart/helpers/useBarChart.ts +4 -1
  54. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  55. package/src/components/Brush/BrushSelector.tsx +2 -1
  56. package/src/components/Brush/MiniChartPreview.tsx +21 -26
  57. package/src/components/EditorPanel/EditorPanel.tsx +692 -954
  58. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  59. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +218 -167
  60. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  61. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +39 -1
  62. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -135
  63. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +154 -109
  64. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  65. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +95 -28
  66. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
  67. package/src/components/EditorPanel/editor-panel.scss +1 -1
  68. package/src/components/EditorPanel/useEditorPermissions.ts +1 -10
  69. package/src/components/Forecasting/Forecasting.tsx +2 -2
  70. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  71. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  72. package/src/components/HoverLine/HoverLine.tsx +6 -8
  73. package/src/components/Legend/Legend.Component.tsx +4 -4
  74. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +9 -4
  75. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  76. package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
  77. package/src/components/Legend/helpers/index.ts +0 -2
  78. package/src/components/LineChart/LineChartProps.ts +1 -0
  79. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +3 -2
  80. package/src/components/LineChart/index.tsx +11 -4
  81. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  82. package/src/components/LinearChart/tests/LinearChart.test.tsx +224 -0
  83. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
  84. package/src/components/LinearChart.tsx +124 -47
  85. package/src/components/PairedBarChart.jsx +2 -2
  86. package/src/components/PieChart/PieChart.tsx +30 -13
  87. package/src/components/RadarChart/RadarChart.tsx +1 -1
  88. package/src/components/Regions/components/Regions.tsx +36 -26
  89. package/src/components/Sankey/components/Sankey.tsx +3 -3
  90. package/src/components/Sankey/sankey.scss +1 -1
  91. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  92. package/src/components/SmallMultiples/SmallMultipleTile.tsx +1 -0
  93. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  94. package/src/components/Sparkline/components/SparkLine.tsx +1 -1
  95. package/src/components/Sparkline/index.scss +4 -2
  96. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  97. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  98. package/src/data/initial-state.js +24 -14
  99. package/src/data/legacy-defaults.ts +18 -0
  100. package/src/helpers/abbreviateNumber.ts +24 -17
  101. package/src/helpers/calculateLeftYAxisWidth.ts +193 -0
  102. package/src/helpers/getChartPatternId.ts +17 -0
  103. package/src/helpers/getMinMax.ts +16 -2
  104. package/src/helpers/seriesColumnSettings.ts +114 -0
  105. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  106. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  107. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  108. package/src/helpers/tooltipHelpers.ts +176 -0
  109. package/src/hooks/useHighlightedBars.ts +9 -16
  110. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  111. package/src/hooks/useRightAxis.ts +14 -0
  112. package/src/hooks/useScales.ts +92 -56
  113. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  114. package/src/hooks/useTooltip.tsx +186 -40
  115. package/src/scss/main.scss +152 -79
  116. package/src/test/CdcChart.test.jsx +26 -2
  117. package/src/types/ChartConfig.ts +5 -0
  118. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  119. package/tests/fixtures/data-with-metadata.json +10 -0
  120. package/examples/chart-regression-2.json +0 -2360
  121. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  122. package/examples/feature/__data__/city-temperature.json +0 -2198
  123. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  124. package/examples/feature/boxplot/box-plot-data.json +0 -71
  125. package/examples/feature/boxplot/boxplot-data.json +0 -156
  126. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  127. package/examples/feature/forest-plot/log.json +0 -26
  128. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  129. package/examples/region-issue.json +0 -2065
  130. package/examples/xaxis.json +0 -493
  131. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  132. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  133. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  134. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  135. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  136. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  137. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  138. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  139. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  140. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  141. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  142. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  143. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  144. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  145. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  146. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  147. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  148. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  149. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  150. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  151. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  152. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  153. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  154. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  155. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  156. /package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +0 -0
  157. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  158. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  159. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  160. /package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +0 -0
  161. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  162. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  163. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  164. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  165. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -21,6 +21,9 @@ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
21
21
  import { type ChartContext } from '../../../types/ChartContext'
22
22
  import _ from 'lodash'
23
23
  import { getBarData } from '../helpers/getBarData'
24
+ import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
25
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
26
+ import { buildSeriesTooltipListHtml } from '../../../helpers/tooltipHelpers'
24
27
 
25
28
  const BarChartVertical = () => {
26
29
  const { xScale, yScale, xMax, yMax, seriesScale, convertLineToBarGraph, barChart } =
@@ -86,7 +89,7 @@ const BarChartVertical = () => {
86
89
  return (
87
90
  <defs>
88
91
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
89
- const patternId = `chart-pattern-${key}`
92
+ const patternId = getChartPatternId(key)
90
93
  const size = pattern.patternSize || 8
91
94
 
92
95
  switch (pattern.shape) {
@@ -227,12 +230,14 @@ const BarChartVertical = () => {
227
230
  ? `${config.runtime.xAxis.label}: ${xAxisValue}`
228
231
  : xAxisValue
229
232
  const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${yAxisValue}`
230
-
231
- const tooltip = `<ul>
232
- <li class="tooltip-heading">${xAxisTooltip}</li>
233
- <li class="tooltip-body ">${tooltipBody}</li>
234
- ${additionalColTooltip ? '<li class="tooltip-body ">' + additionalColTooltip + '</li>' : ''}
235
- </li></ul>`
233
+ const tooltip = buildSeriesTooltipListHtml({
234
+ config,
235
+ colorScale,
236
+ heading: xAxisTooltip,
237
+ seriesKey: bar.key,
238
+ seriesText: tooltipBody,
239
+ extraRows: additionalColTooltip ? [additionalColTooltip] : []
240
+ })
236
241
  const { barHeight, isSuppressed, getBarY, absentDataLabel } = getBarConfig({
237
242
  bar,
238
243
  defaultBarHeight,
@@ -325,33 +330,15 @@ const BarChartVertical = () => {
325
330
  }
326
331
 
327
332
  // Check if this bar should use a pattern
328
- const getPatternUrl = (): string | null => {
329
- if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
330
- return null
331
- }
332
-
333
- // Find a pattern that matches this specific bar
334
- for (const [patternKey, pattern] of Object.entries(config.legend.patterns)) {
335
- if (pattern.dataKey && pattern.dataValue) {
336
- // For grouped bar charts, check if the pattern's dataKey matches the current bar's series key
337
- // and if the pattern's dataValue matches the current bar's value
338
- if (pattern.dataKey === bar.key && String(bar.value) === String(pattern.dataValue)) {
339
- return `url(#chart-pattern-${patternKey})`
340
- }
341
- // Fallback for non-grouped charts: check datum field value
342
- else if (!config.series || config.series.length <= 1) {
343
- const dataFieldValue = datum[pattern.dataKey]
344
- if (String(dataFieldValue) === String(pattern.dataValue)) {
345
- return `url(#chart-pattern-${patternKey})`
346
- }
347
- }
348
- }
349
- }
350
-
351
- return null
352
- }
353
-
354
- const patternUrl = getPatternUrl()
333
+ const patternUrl = getPatternUrlForBar({
334
+ patterns: config.legend?.patterns,
335
+ datum,
336
+ seriesKey: bar.key,
337
+ seriesValue: bar.value,
338
+ seriesLabels: config.runtime?.seriesLabels,
339
+ seriesKeys: config.series?.map(series => series.dataKey),
340
+ allowNonSeriesFieldMatch: !config.series || config.series.length <= 1
341
+ })
355
342
  const baseBackground = getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))
356
343
 
357
344
  // Confidence Interval Variables
@@ -18,7 +18,7 @@ type BarChartProps = {
18
18
  seriesScale: PositionScale
19
19
  xMax: number
20
20
  yMax: number
21
- yAxisWidth?: number
21
+ yAxisWidth: number
22
22
  handleTooltipMouseOver: MouseEventHandler<SVGRectElement>
23
23
  handleTooltipMouseOff: MouseEventHandler<SVGRectElement>
24
24
  handleTooltipClick: MouseEventHandler<SVGRectElement>
@@ -49,14 +49,10 @@ const BarChart: React.FC<BarChartProps> = ({
49
49
  barChart
50
50
  }
51
51
 
52
- // Use yAxisWidth prop if provided (for horizontal bar charts with dynamic labels)
53
- // otherwise fall back to config value
54
- const leftOffset = yAxisWidth ?? parseFloat(config.runtime.yAxis.size)
55
-
56
52
  return (
57
53
  <ErrorBoundary component='BarChart'>
58
54
  <BarChartContext.Provider value={contextValues}>
59
- <Group left={leftOffset}>
55
+ <Group left={yAxisWidth}>
60
56
  <BarChartType.StackedVertical />
61
57
  <BarChartType.StackedHorizontal />
62
58
  <BarChartType.Vertical />
@@ -0,0 +1,94 @@
1
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
2
+
3
+ type LegendPattern = {
4
+ dataKey?: string
5
+ dataValue?: string | number
6
+ }
7
+
8
+ type SeriesLabels = Record<string, string> | undefined
9
+
10
+ type GetPatternUrlArgs = {
11
+ patterns?: Record<string, LegendPattern>
12
+ datum: Record<string, any>
13
+ seriesKey: string
14
+ seriesValue: string | number
15
+ seriesLabels?: SeriesLabels
16
+ seriesKeys?: string[]
17
+ allowNonSeriesFieldMatch?: boolean
18
+ }
19
+
20
+ const normalizeString = (value: unknown): string => String(value ?? '').trim()
21
+
22
+ const hasPatternValue = (value: unknown): boolean => normalizeString(value) !== ''
23
+
24
+ const isNumericLike = (value: string): boolean => value !== '' && !Number.isNaN(Number(value))
25
+
26
+ const valuesMatch = (left: unknown, right: unknown): boolean => {
27
+ const normalizedLeft = normalizeString(left)
28
+ const normalizedRight = normalizeString(right)
29
+
30
+ if (normalizedLeft === '' || normalizedRight === '') {
31
+ return false
32
+ }
33
+
34
+ if (isNumericLike(normalizedLeft) && isNumericLike(normalizedRight)) {
35
+ return Number(normalizedLeft) === Number(normalizedRight)
36
+ }
37
+
38
+ return normalizedLeft === normalizedRight
39
+ }
40
+
41
+ const isSeriesDataKey = (dataKey: string, seriesLabels?: SeriesLabels, seriesKeys?: string[]): boolean => {
42
+ if (Array.isArray(seriesKeys) && seriesKeys.length > 0) {
43
+ return seriesKeys.includes(dataKey)
44
+ }
45
+ if (!seriesLabels) return false
46
+ return Object.prototype.hasOwnProperty.call(seriesLabels, dataKey)
47
+ }
48
+
49
+ export const getPatternUrl = ({
50
+ patterns,
51
+ datum,
52
+ seriesKey,
53
+ seriesValue,
54
+ seriesLabels,
55
+ seriesKeys,
56
+ allowNonSeriesFieldMatch = true
57
+ }: GetPatternUrlArgs): string | null => {
58
+ if (!patterns) {
59
+ return null
60
+ }
61
+
62
+ let broadMatchUrl: string | null = null
63
+
64
+ for (const patternKey in patterns) {
65
+ if (!Object.prototype.hasOwnProperty.call(patterns, patternKey)) continue
66
+ const pattern = patterns[patternKey]
67
+ const dataKey = normalizeString(pattern.dataKey)
68
+
69
+ if (!hasPatternValue(pattern.dataValue)) {
70
+ continue
71
+ }
72
+
73
+ if (dataKey === '') {
74
+ if (!broadMatchUrl && valuesMatch(seriesValue, pattern.dataValue)) {
75
+ broadMatchUrl = `url(#${getChartPatternId(patternKey)})`
76
+ }
77
+ continue
78
+ }
79
+
80
+ if (dataKey === seriesKey && valuesMatch(seriesValue, pattern.dataValue)) {
81
+ return `url(#${getChartPatternId(patternKey)})`
82
+ }
83
+
84
+ if (
85
+ allowNonSeriesFieldMatch &&
86
+ !isSeriesDataKey(dataKey, seriesLabels, seriesKeys) &&
87
+ valuesMatch(datum?.[dataKey], pattern.dataValue)
88
+ ) {
89
+ return `url(#${getChartPatternId(patternKey)})`
90
+ }
91
+ }
92
+
93
+ return broadMatchUrl
94
+ }
@@ -0,0 +1,134 @@
1
+ import { getPatternUrl } from '../getPatternUrl'
2
+ import { getChartPatternId } from '../../../../helpers/getChartPatternId'
3
+
4
+ describe('getPatternUrl', () => {
5
+ const pattern1Url = `url(#${getChartPatternId('Pattern1')})`
6
+ const pattern2Url = `url(#${getChartPatternId('Pattern2')})`
7
+
8
+ it('matches specific series patterns by series key and value', () => {
9
+ const patternUrl = getPatternUrl({
10
+ patterns: {
11
+ Pattern1: { dataKey: 'y1', dataValue: '19000' }
12
+ },
13
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
14
+ seriesKey: 'y1',
15
+ seriesValue: 19000,
16
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
17
+ })
18
+
19
+ expect(patternUrl).toBe(pattern1Url)
20
+ })
21
+
22
+ it('matches specific non-series field patterns', () => {
23
+ const patternUrl = getPatternUrl({
24
+ patterns: {
25
+ Pattern1: { dataKey: 'category', dataValue: 'Q1' }
26
+ },
27
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
28
+ seriesKey: 'y2',
29
+ seriesValue: 47000,
30
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
31
+ })
32
+
33
+ expect(patternUrl).toBe(pattern1Url)
34
+ })
35
+
36
+ it('does not match non-series field patterns when non-series matching is disabled', () => {
37
+ const patternUrl = getPatternUrl({
38
+ patterns: {
39
+ Pattern1: { dataKey: 'category', dataValue: 'Q1' }
40
+ },
41
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
42
+ seriesKey: 'y2',
43
+ seriesValue: 47000,
44
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' },
45
+ allowNonSeriesFieldMatch: false
46
+ })
47
+
48
+ expect(patternUrl).toBeNull()
49
+ })
50
+
51
+ it('matches blank dataKey pattern by value across series', () => {
52
+ const patternUrl = getPatternUrl({
53
+ patterns: {
54
+ Pattern1: { dataKey: '', dataValue: '47000' }
55
+ },
56
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
57
+ seriesKey: 'y2',
58
+ seriesValue: 47000,
59
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
60
+ })
61
+
62
+ expect(patternUrl).toBe(pattern1Url)
63
+ })
64
+
65
+ it('does not match blank dataKey pattern with empty dataValue', () => {
66
+ const patternUrl = getPatternUrl({
67
+ patterns: {
68
+ Pattern1: { dataKey: '', dataValue: '' }
69
+ },
70
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
71
+ seriesKey: 'y2',
72
+ seriesValue: 47000,
73
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
74
+ })
75
+
76
+ expect(patternUrl).toBeNull()
77
+ })
78
+
79
+ it('prioritizes specific match over broad match', () => {
80
+ const patternUrl = getPatternUrl({
81
+ patterns: {
82
+ Pattern1: { dataKey: '', dataValue: '19000' },
83
+ Pattern2: { dataKey: 'y1', dataValue: '19000' }
84
+ },
85
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
86
+ seriesKey: 'y1',
87
+ seriesValue: 19000,
88
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
89
+ })
90
+
91
+ expect(patternUrl).toBe(pattern2Url)
92
+ })
93
+
94
+ it('uses seriesKeys as fallback to identify series keys when seriesLabels are missing', () => {
95
+ const patternUrl = getPatternUrl({
96
+ patterns: {
97
+ Pattern1: { dataKey: 'y1', dataValue: '19000' }
98
+ },
99
+ datum: { category: 'Q1', y1: 19000, y2: 19000 },
100
+ seriesKey: 'y2',
101
+ seriesValue: 19000,
102
+ seriesKeys: ['y1', 'y2']
103
+ })
104
+
105
+ expect(patternUrl).toBeNull()
106
+ })
107
+
108
+ it('sanitizes special-character pattern keys in returned url fragments', () => {
109
+ const patternKey = 'Pattern 1 / @ value'
110
+ const patternUrl = getPatternUrl({
111
+ patterns: {
112
+ [patternKey]: { dataKey: 'y1', dataValue: '19000' }
113
+ },
114
+ datum: { category: 'Q1', y1: 19000, y2: 47000 },
115
+ seriesKey: 'y1',
116
+ seriesValue: 19000,
117
+ seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
118
+ })
119
+
120
+ expect(patternUrl).toBe(`url(#${getChartPatternId(patternKey)})`)
121
+ })
122
+
123
+ it('creates distinct ids for keys that sanitize to the same base id', () => {
124
+ const keyA = 'A B'
125
+ const keyB = 'A@B'
126
+
127
+ const idA = getChartPatternId(keyA)
128
+ const idB = getChartPatternId(keyB)
129
+
130
+ expect(idA).not.toBe(idB)
131
+ expect(idA.startsWith('chart-pattern-A-B-')).toBe(true)
132
+ expect(idB.startsWith('chart-pattern-A-B-')).toBe(true)
133
+ })
134
+ })
@@ -6,6 +6,7 @@ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
6
6
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
7
7
  import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
8
8
  import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
9
+ import { getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
9
10
 
10
11
  export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, configContext) => {
11
12
  const {
@@ -51,6 +52,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
51
52
  isBarAndLegendIsolate && seriesHighlight?.length
52
53
  ? seriesHighlight
53
54
  : config.runtime.barSeriesKeys || config.runtime.seriesKeys
55
+ const seriesOwnedColumnNames = getSeriesOwnedColumnNames(config.series)
54
56
 
55
57
  useEffect(() => {
56
58
  if (orientation === 'horizontal' && !config.yAxis.labelPlacement) {
@@ -179,6 +181,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
179
181
  }) || {}
180
182
  Object.keys(columns).forEach(colKeys => {
181
183
  const colConfig = config.columns[colKeys]
184
+ if (seriesOwnedColumnNames.includes(colConfig.name || colKeys)) return
182
185
  if (series && colConfig.series && colConfig.series !== series && !colConfig.tooltips) return
183
186
  const formattingParams = {
184
187
  addColPrefix: config.columns[colKeys].prefix,
@@ -190,7 +193,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
190
193
  const formattedValue = formatColNumber(
191
194
  closestVal[config.columns[colKeys].name],
192
195
  'left',
193
- true,
196
+ false,
194
197
  config,
195
198
  formattingParams
196
199
  )
@@ -9,7 +9,7 @@ import { handleTooltip, createPlots } from './helpers/index'
9
9
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
10
10
  import _ from 'lodash'
11
11
 
12
- const BoxPlotVertical = ({ xScale, yScale, seriesScale }) => {
12
+ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
13
13
  const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
14
14
  const { boxplot } = config
15
15
 
@@ -23,7 +23,7 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale }) => {
23
23
  const plots = createPlots(data, config)
24
24
  return (
25
25
  <ErrorBoundary component='BoxPlot Vertical'>
26
- <Group left={Number(config.yAxis.size)} className='boxplot' key={`boxplot-group`}>
26
+ <Group left={yAxisWidth} className='boxplot' key={`boxplot-group`}>
27
27
  {plots.map((d, i) => {
28
28
  const offset = boxWidth - constrainedWidth
29
29
  const radius = 4
@@ -63,7 +63,7 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale }) => {
63
63
  {displayPlot && (
64
64
  <BoxPlot
65
65
  display={displayPlot ? 'block' : 'none'}
66
- data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
66
+ data-left={xScale(d.columnCategory) + yAxisWidth + offset / 2 + 0.5}
67
67
  key={`box-plot-${i}-${item}`}
68
68
  min={Number(d.min[item.dataKey])}
69
69
  max={Number(d.max[item.dataKey])}
@@ -8,6 +8,7 @@ import { Bounds } from '@visx/brush/lib/types'
8
8
  import type { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'
9
9
  import { isDateScale } from '@cdc/core/helpers/cove/date'
10
10
  import ConfigContext, { ChartDispatchContext } from '../../ConfigContext'
11
+ import { getChartPatternId } from '../../helpers/getChartPatternId'
11
12
  import MiniChartPreview from './MiniChartPreview'
12
13
 
13
14
  interface BrushSelectorProps {
@@ -125,7 +126,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
125
126
  return (
126
127
  <>
127
128
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
128
- const patternId = `chart-pattern-${key}`
129
+ const patternId = getChartPatternId(key)
129
130
  const size = pattern.patternSize || 8
130
131
 
131
132
  switch (pattern.shape) {
@@ -3,6 +3,7 @@ import { LinePath, AreaClosed, AreaStack } from '@visx/shape'
3
3
  import * as allCurves from '@visx/curve'
4
4
  import { handleLineType } from '../../helpers/handleLineType'
5
5
  import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
6
+ import { getPatternUrl as getPatternUrlForBar } from '../BarChart/helpers/getPatternUrl'
6
7
 
7
8
  interface MiniChartPreviewProps {
8
9
  series: any[]
@@ -33,6 +34,8 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
33
34
  const lineSeries = isComboChart
34
35
  ? series.filter(s => !barSeriesTypes.has(s.type) && s.type !== 'Area Chart')
35
36
  : series
37
+ const patternSeriesKeys = Array.isArray(series) ? series.map(_series => _series.dataKey) : []
38
+ const allowGroupedNonSeriesFieldMatch = !config.series || config.series.length <= 1
36
39
 
37
40
  let barElements: React.ReactElement[] = []
38
41
 
@@ -44,30 +47,6 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
44
47
  const barStrokeColor = config?.barHasBorder === 'true' ? '#000' : 'transparent'
45
48
  const barStrokeWidth = config?.barHasBorder === 'true' ? 1 : 0
46
49
 
47
- const getPatternUrl = (datum, seriesKey: string, value: string | number) => {
48
- if (!config.legend?.patterns || Object.keys(config.legend.patterns).length === 0) {
49
- return null
50
- }
51
-
52
- for (const [patternKey, patternObj] of Object.entries(config.legend.patterns)) {
53
- const pattern = patternObj as any
54
- if (pattern.dataKey && pattern.dataValue) {
55
- if (pattern.dataKey === seriesKey && String(value) === String(pattern.dataValue)) {
56
- return `url(#chart-pattern-${patternKey})`
57
- }
58
-
59
- if (!config.runtime?.seriesLabels || !config.runtime.seriesLabels[pattern.dataKey]) {
60
- const dataFieldValue = datum[pattern.dataKey]
61
- if (String(dataFieldValue) === String(pattern.dataValue)) {
62
- return `url(#chart-pattern-${patternKey})`
63
- }
64
- }
65
- }
66
- }
67
-
68
- return null
69
- }
70
-
71
50
  tableData.forEach((d, i) => {
72
51
  const xVal = xScale(d[dataKey])
73
52
  if (xVal === undefined || isNaN(xVal)) {
@@ -90,7 +69,15 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
90
69
  if (isNaN(value) || value === 0) return
91
70
 
92
71
  const seriesColor = colorScale?.(config.runtime.seriesLabels?.[s.dataKey] || s.dataKey) || '#666'
93
- const patternUrl = getPatternUrl(d, s.dataKey, value)
72
+ const patternUrl = getPatternUrlForBar({
73
+ patterns: config.legend?.patterns,
74
+ datum: d,
75
+ seriesKey: s.dataKey,
76
+ seriesValue: value,
77
+ seriesLabels: config.runtime?.seriesLabels,
78
+ seriesKeys: patternSeriesKeys,
79
+ allowNonSeriesFieldMatch: true
80
+ })
94
81
 
95
82
  // Calculate the bottom and top of this segment
96
83
  // For stacked bars, each segment sits on top of the previous one
@@ -145,7 +132,15 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
145
132
  if (isNaN(value)) return
146
133
 
147
134
  const seriesColor = colorScale?.(config.runtime.seriesLabels?.[s.dataKey] || s.dataKey) || '#666'
148
- const patternUrl = getPatternUrl(d, s.dataKey, value)
135
+ const patternUrl = getPatternUrlForBar({
136
+ patterns: config.legend?.patterns,
137
+ datum: d,
138
+ seriesKey: s.dataKey,
139
+ seriesValue: value,
140
+ seriesLabels: config.runtime?.seriesLabels,
141
+ seriesKeys: patternSeriesKeys,
142
+ allowNonSeriesFieldMatch: allowGroupedNonSeriesFieldMatch
143
+ })
149
144
 
150
145
  // Calculate bar position and height
151
146
  const valueY = miniYScale(value)