@cdc/chart 4.26.2 → 4.26.3

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 (70) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +35674 -32430
  3. package/examples/data/data-with-metadata.json +10 -0
  4. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  5. package/examples/metadata-variables.json +58 -0
  6. package/package.json +3 -3
  7. package/src/CdcChart.tsx +8 -4
  8. package/src/CdcChartComponent.tsx +321 -288
  9. package/src/_stories/Chart.CustomColors.stories.tsx +74 -0
  10. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  11. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  12. package/src/_stories/Chart.stories.tsx +36 -2
  13. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  14. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  15. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  16. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  17. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  18. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  19. package/src/components/Annotations/components/AnnotationDraggable.styles.css +10 -10
  20. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
  21. package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
  22. package/src/components/Axis/BottomAxis.tsx +10 -3
  23. package/src/components/Axis/PairedBarAxis.tsx +10 -4
  24. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  25. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  26. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  27. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  28. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  29. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  30. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  31. package/src/components/Brush/BrushSelector.tsx +2 -1
  32. package/src/components/Brush/MiniChartPreview.tsx +21 -26
  33. package/src/components/EditorPanel/EditorPanel.tsx +56 -43
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +9 -9
  35. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  36. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +39 -1
  37. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +24 -42
  38. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -2
  39. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
  40. package/src/components/EditorPanel/editor-panel.scss +1 -1
  41. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  42. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  43. package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
  44. package/src/components/LinearChart/tests/LinearChart.test.tsx +77 -0
  45. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
  46. package/src/components/LinearChart.tsx +26 -6
  47. package/src/components/PieChart/PieChart.tsx +19 -4
  48. package/src/components/RadarChart/RadarChart.tsx +1 -1
  49. package/src/components/Regions/components/Regions.tsx +6 -6
  50. package/src/components/Sankey/components/Sankey.tsx +3 -3
  51. package/src/components/Sankey/sankey.scss +1 -1
  52. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  53. package/src/components/Sparkline/index.scss +4 -2
  54. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  55. package/src/data/initial-state.js +23 -14
  56. package/src/data/legacy-defaults.ts +18 -0
  57. package/src/helpers/abbreviateNumber.ts +24 -17
  58. package/src/helpers/getChartPatternId.ts +17 -0
  59. package/src/helpers/getMinMax.ts +16 -2
  60. package/src/helpers/seriesColumnSettings.ts +114 -0
  61. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  62. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  63. package/src/hooks/useRightAxis.ts +14 -0
  64. package/src/hooks/useScales.ts +92 -56
  65. package/src/hooks/useTooltip.tsx +20 -3
  66. package/src/scss/main.scss +152 -79
  67. package/src/test/CdcChart.test.jsx +2 -2
  68. package/src/types/ChartConfig.ts +4 -0
  69. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  70. package/tests/fixtures/data-with-metadata.json +10 -0
@@ -8,6 +8,8 @@ import Regions from '../../Regions'
8
8
  import { addMinimumBarHeights } from '../helpers'
9
9
 
10
10
  import createBarElement from '@cdc/core/components/createBarElement'
11
+ import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
12
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
11
13
 
12
14
  const BarChartStackedVertical = () => {
13
15
  const [barWidth, setBarWidth] = useState(0)
@@ -40,7 +42,7 @@ const BarChartStackedVertical = () => {
40
42
  return (
41
43
  <defs>
42
44
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
43
- const patternId = `chart-pattern-${key}`
45
+ const patternId = getChartPatternId(key)
44
46
  const size = pattern.patternSize || 8
45
47
 
46
48
  switch (pattern.shape) {
@@ -150,36 +152,15 @@ const BarChartStackedVertical = () => {
150
152
  setBarWidth(barThickness)
151
153
 
152
154
  // Check if this bar should use a pattern
153
- const getPatternUrl = (): string | null => {
154
- if (!config.legend.patterns || Object.keys(config.legend.patterns).length === 0) {
155
- return null
156
- }
157
-
158
- // Find a pattern that matches this specific bar
159
- for (const [patternKey, patternObj] of Object.entries(config.legend.patterns)) {
160
- const pattern = patternObj as any
161
- if (pattern?.dataKey && pattern?.dataValue) {
162
- // For stacked bar charts, check if the pattern's dataKey matches the current bar's series key
163
- // and if the pattern's dataValue matches the current bar's value
164
- const barValue = bar.bar.data[bar.key]
165
- if (pattern.dataKey === bar.key && String(barValue) === String(pattern.dataValue)) {
166
- return `url(#chart-pattern-${patternKey})`
167
- }
168
- // Fallback for non-series pattern matching (like the original stacked pattern test)
169
- // Only check this if the pattern dataKey is NOT a series key
170
- else if (!config.runtime.seriesLabels || !config.runtime.seriesLabels[pattern.dataKey]) {
171
- const dataFieldValue = bar.bar.data[pattern.dataKey]
172
- if (String(dataFieldValue) === String(pattern.dataValue)) {
173
- return `url(#chart-pattern-${patternKey})`
174
- }
175
- }
176
- }
177
- }
178
-
179
- return null
180
- }
181
-
182
- const patternUrl = getPatternUrl()
155
+ const patternUrl = getPatternUrlForBar({
156
+ patterns: config.legend?.patterns,
157
+ datum: bar.bar.data,
158
+ seriesKey: bar.key,
159
+ seriesValue: bar.bar.data[bar.key],
160
+ seriesLabels: config.runtime?.seriesLabels,
161
+ seriesKeys: config.series?.map(series => series.dataKey),
162
+ allowNonSeriesFieldMatch: true
163
+ })
183
164
 
184
165
  return (
185
166
  <Group key={`${barStack.index}--${bar.index}--${orientation}`}>
@@ -21,6 +21,8 @@ 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'
24
26
 
25
27
  const BarChartVertical = () => {
26
28
  const { xScale, yScale, xMax, yMax, seriesScale, convertLineToBarGraph, barChart } =
@@ -86,7 +88,7 @@ const BarChartVertical = () => {
86
88
  return (
87
89
  <defs>
88
90
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
89
- const patternId = `chart-pattern-${key}`
91
+ const patternId = getChartPatternId(key)
90
92
  const size = pattern.patternSize || 8
91
93
 
92
94
  switch (pattern.shape) {
@@ -325,33 +327,15 @@ const BarChartVertical = () => {
325
327
  }
326
328
 
327
329
  // 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()
330
+ const patternUrl = getPatternUrlForBar({
331
+ patterns: config.legend?.patterns,
332
+ datum,
333
+ seriesKey: bar.key,
334
+ seriesValue: bar.value,
335
+ seriesLabels: config.runtime?.seriesLabels,
336
+ seriesKeys: config.series?.map(series => series.dataKey),
337
+ allowNonSeriesFieldMatch: !config.series || config.series.length <= 1
338
+ })
355
339
  const baseBackground = getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))
356
340
 
357
341
  // Confidence Interval Variables
@@ -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,
@@ -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)
@@ -45,7 +45,10 @@ import '@cdc/core/components/EditorPanel/EditorPanel.styles.css'
45
45
  import './editor-panel.scss'
46
46
  import { Anchor } from '@cdc/core/types/Axis'
47
47
  import EditorPanelContext from './EditorPanelContext'
48
- import _ from 'lodash'
48
+ import cloneDeep from 'lodash/cloneDeep'
49
+ import flatMap from 'lodash/flatMap'
50
+ import keys from 'lodash/keys'
51
+ import uniq from 'lodash/uniq'
49
52
  import { adjustedSymbols as symbolCodes } from '@cdc/core/helpers/footnoteSymbols'
50
53
  import { updateFieldRankByValue } from './helpers/updateFieldRankByValue'
51
54
  import cloneConfig from '@cdc/core/helpers/cloneConfig'
@@ -55,6 +58,7 @@ import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
55
58
  import { paletteMigrationMap, twoColorPaletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
56
59
  import { isV1Palette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
57
60
  import { USE_V2_MIGRATION } from '@cdc/core/helpers/constants'
61
+ import { getSeriesOwnedColumnNames } from '../../helpers/seriesColumnSettings'
58
62
 
59
63
  interface PreliminaryProps {
60
64
  config: ChartConfig
@@ -70,7 +74,7 @@ const PreliminaryData: React.FC<PreliminaryProps> = ({ config, updateConfig, dat
70
74
  const hasComboBarSeries = isCombo && barSeriesExists
71
75
 
72
76
  const getColumnOptions = () => {
73
- return _.uniq(_.flatMap(data, _.keys))
77
+ return uniq(flatMap(data, keys))
74
78
  }
75
79
 
76
80
  const getTypeOptions = () => {
@@ -935,6 +939,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
935
939
 
936
940
  // Extract column names from data with memoization (replaces getColumns)
937
941
  const allColumns = useDataColumns(dataSourceForColumns)
942
+ const seriesOwnedColumnNames = useMemo(() => getSeriesOwnedColumnNames(config.series), [config.series])
938
943
 
939
944
  // Filter out series columns and confidence key columns (except lower and upper)
940
945
  const filteredColumns = useMemo(() => {
@@ -1519,46 +1524,8 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
1519
1524
  'Deviation Bar'
1520
1525
  ]
1521
1526
 
1522
- const columnsOptions = [
1523
- <option value='' key={'Select Option'}>
1524
- - Select Option -
1525
- </option>
1526
- ]
1527
-
1528
- if (config.data && config.series) {
1529
- Object.keys(config.data?.[0] || []).map(colName => {
1530
- // OMIT ANY COLUMNS THAT ARE IN DATA SERIES!
1531
- const found = config?.series.some(series => series.dataKey === colName)
1532
- if (colName !== config.xAxis.dataKey && !found) {
1533
- // if not the index then add it
1534
- return columnsOptions.push(
1535
- <option value={colName} key={colName}>
1536
- {colName}
1537
- </option>
1538
- )
1539
- }
1540
- })
1541
- }
1542
-
1543
- // for pie charts
1544
- if (!config.data && data) {
1545
- if (!data[0]) return
1546
- Object.keys(data[0]).map(colName => {
1547
- // OMIT ANY COLUMNS THAT ARE IN DATA SERIES!
1548
- const found = data.some(el => el.dataKey === colName)
1549
- if (colName !== config.xAxis.dataKey && !found) {
1550
- // if not the index then add it
1551
- return columnsOptions.push(
1552
- <option value={colName} key={colName}>
1553
- {colName}
1554
- </option>
1555
- )
1556
- }
1557
- })
1558
- }
1559
-
1560
1527
  const removeAdditionalColumn = columnName => {
1561
- const newColumns = _.cloneDeep(config.columns)
1528
+ const newColumns = cloneDeep(config.columns)
1562
1529
 
1563
1530
  delete newColumns[columnName]
1564
1531
 
@@ -2487,6 +2454,28 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
2487
2454
  <span style={{ color: 'red', display: 'block' }}>{warningMsg.minMsg}</span>
2488
2455
  </>
2489
2456
  )}
2457
+ <TextField
2458
+ value={config.yAxis.smallestLeftAxisMax}
2459
+ section='yAxis'
2460
+ fieldName='smallestLeftAxisMax'
2461
+ type='number'
2462
+ label='Smallest Left Axis Maximum'
2463
+ placeholder='Auto'
2464
+ tooltip={
2465
+ <Tooltip style={{ textTransform: 'none' }}>
2466
+ <Tooltip.Target>
2467
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2468
+ </Tooltip.Target>
2469
+ <Tooltip.Content>
2470
+ <p>
2471
+ Example: If your data only goes up to 1, the axis might show 0, 0.2, 0.4, 0.6,
2472
+ 0.8, 1. Setting this to 5 would make the axis show 0, 1, 2, 3, 4, 5 instead.
2473
+ </p>
2474
+ </Tooltip.Content>
2475
+ </Tooltip>
2476
+ }
2477
+ updateField={updateFieldDeprecated}
2478
+ />
2490
2479
  </>
2491
2480
  )
2492
2481
  )}
@@ -2886,7 +2875,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
2886
2875
  />
2887
2876
 
2888
2877
  <TextField
2889
- value={config.yAxis.max}
2878
+ value={config.yAxis.rightMax}
2890
2879
  section='yAxis'
2891
2880
  fieldName='rightMax'
2892
2881
  type='number'
@@ -2896,7 +2885,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
2896
2885
  />
2897
2886
  <span style={{ color: 'red', display: 'block' }}>{warningMsg.rightMaxMessage}</span>
2898
2887
  <TextField
2899
- value={config.yAxis.min}
2888
+ value={config.yAxis.rightMin}
2900
2889
  section='yAxis'
2901
2890
  fieldName='rightMin'
2902
2891
  type='number'
@@ -2905,6 +2894,28 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
2905
2894
  updateField={updateFieldDeprecated}
2906
2895
  />
2907
2896
  <span style={{ color: 'red', display: 'block' }}>{warningMsg.minMsg}</span>
2897
+ <TextField
2898
+ value={config.yAxis.smallestRightAxisMax}
2899
+ section='yAxis'
2900
+ fieldName='smallestRightAxisMax'
2901
+ type='number'
2902
+ label='Smallest Right Axis Maximum'
2903
+ placeholder='Auto'
2904
+ tooltip={
2905
+ <Tooltip style={{ textTransform: 'none' }}>
2906
+ <Tooltip.Target>
2907
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2908
+ </Tooltip.Target>
2909
+ <Tooltip.Content>
2910
+ <p>
2911
+ Example: If your data only goes up to 1, the axis might show 0, 0.2, 0.4, 0.6, 0.8, 1.
2912
+ Setting this to 5 would make the axis show 0, 1, 2, 3, 4, 5 instead.
2913
+ </p>
2914
+ </Tooltip.Content>
2915
+ </Tooltip>
2916
+ }
2917
+ updateField={updateFieldDeprecated}
2918
+ />
2908
2919
  </AccordionItemPanel>
2909
2920
  </AccordionItem>
2910
2921
  )}
@@ -4143,6 +4154,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4143
4154
  config={config}
4144
4155
  updateField={updateFieldDeprecated}
4145
4156
  deleteColumn={removeAdditionalColumn}
4157
+ hiddenColumnNames={seriesOwnedColumnNames}
4146
4158
  />{' '}
4147
4159
  </AccordionItemPanel>
4148
4160
  </AccordionItem>
@@ -4600,6 +4612,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4600
4612
  enableMarkupVariables={config.enableMarkupVariables || false}
4601
4613
  onMarkupVariablesChange={variables => updateField(null, null, 'markupVariables', variables)}
4602
4614
  onToggleEnable={enabled => updateField(null, null, 'enableMarkupVariables', enabled)}
4615
+ dataMetadata={config.dataMetadata}
4603
4616
  />
4604
4617
  )}
4605
4618
  <Panels.SmallMultiples name='Small Multiples' />