@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.
- package/LICENSE +201 -0
- package/dist/cdcchart.js +35674 -32430
- package/examples/data/data-with-metadata.json +10 -0
- package/examples/feature/pie/planet-pie-example-config.json +2 -1
- package/examples/metadata-variables.json +58 -0
- package/package.json +3 -3
- package/src/CdcChart.tsx +8 -4
- package/src/CdcChartComponent.tsx +321 -288
- package/src/_stories/Chart.CustomColors.stories.tsx +74 -0
- package/src/_stories/Chart.Defaults.stories.tsx +95 -0
- package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
- package/src/_stories/Chart.stories.tsx +36 -2
- package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
- package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
- package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
- package/src/_stories/_mock/paired-bar-abbr.json +421 -0
- package/src/_stories/_mock/pie_custom_colors.json +268 -0
- package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +10 -10
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
- package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
- package/src/components/Axis/BottomAxis.tsx +10 -3
- package/src/components/Axis/PairedBarAxis.tsx +10 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
- package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
- package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
- package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
- package/src/components/BarChart/helpers/useBarChart.ts +3 -0
- package/src/components/Brush/BrushSelector.tsx +2 -1
- package/src/components/Brush/MiniChartPreview.tsx +21 -26
- package/src/components/EditorPanel/EditorPanel.tsx +56 -43
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +9 -9
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +39 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +24 -42
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -2
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
- package/src/components/EditorPanel/editor-panel.scss +1 -1
- package/src/components/ForestPlot/ForestPlot.tsx +26 -22
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
- package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
- package/src/components/LinearChart/tests/LinearChart.test.tsx +77 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
- package/src/components/LinearChart.tsx +26 -6
- package/src/components/PieChart/PieChart.tsx +19 -4
- package/src/components/RadarChart/RadarChart.tsx +1 -1
- package/src/components/Regions/components/Regions.tsx +6 -6
- package/src/components/Sankey/components/Sankey.tsx +3 -3
- package/src/components/Sankey/sankey.scss +1 -1
- package/src/components/SmallMultiples/SmallMultiples.css +5 -5
- package/src/components/Sparkline/index.scss +4 -2
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
- package/src/data/initial-state.js +23 -14
- package/src/data/legacy-defaults.ts +18 -0
- package/src/helpers/abbreviateNumber.ts +24 -17
- package/src/helpers/getChartPatternId.ts +17 -0
- package/src/helpers/getMinMax.ts +16 -2
- package/src/helpers/seriesColumnSettings.ts +114 -0
- package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
- package/src/hooks/useRightAxis.ts +14 -0
- package/src/hooks/useScales.ts +92 -56
- package/src/hooks/useTooltip.tsx +20 -3
- package/src/scss/main.scss +152 -79
- package/src/test/CdcChart.test.jsx +2 -2
- package/src/types/ChartConfig.ts +4 -0
- package/tests/fixtures/chart-config-with-metadata.json +29 -0
- 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 =
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 =
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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' />
|