@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.
- package/CONFIG.md +169 -0
- package/README.md +42 -17
- package/dist/cdcchart.js +39419 -35180
- package/examples/__data__/data-with-metadata.json +10 -0
- package/examples/feature/bar/additional-column-tooltip.json +3 -3
- package/examples/feature/deviation/planet-deviation-config.json +2 -2
- package/examples/feature/paired-bar/paired-bar-example.json +2 -2
- package/examples/feature/pie/planet-pie-example-config.json +2 -1
- package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
- package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
- package/examples/feature/tests-covid/covid-example-config.json +2 -2
- package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
- package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
- package/examples/metadata-variables.json +58 -0
- package/examples/minimal-example.json +24 -0
- package/examples/private/test2.json +35012 -0
- package/package.json +4 -4
- package/src/CdcChart.tsx +8 -4
- package/src/CdcChartComponent.tsx +326 -290
- package/src/_stories/Chart.CustomColors.smoke.stories.tsx +97 -0
- package/src/_stories/Chart.Defaults.smoke.stories.tsx +95 -0
- package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
- package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
- package/src/_stories/{Chart.SmallMultiples.stories.tsx → Chart.SmallMultiples.smoke.stories.tsx} +17 -1
- package/src/_stories/Chart.SmallestLeftAxisMax.smoke.stories.tsx +64 -0
- package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +48 -2
- package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +177 -115
- package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
- package/src/_stories/ChartEditor.Editor.stories.tsx +24 -1
- package/src/_stories/{Chart.CustomColors.stories.tsx → ChartLine.DashedDates.smoke.stories.tsx} +4 -4
- package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
- package/src/_stories/_mock/paired-bar-abbr.json +421 -0
- package/src/_stories/_mock/paired-bar.json +1 -1
- 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/AnnotationDraggable.tsx +1 -1
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
- package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
- package/src/components/Axis/BottomAxis.tsx +10 -3
- package/src/components/Axis/LeftAxis.tsx +2 -4
- package/src/components/Axis/LeftAxisGridlines.tsx +1 -3
- package/src/components/Axis/PairedBarAxis.tsx +10 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +21 -33
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +21 -35
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +21 -36
- package/src/components/BarChart/components/BarChart.Vertical.tsx +21 -34
- package/src/components/BarChart/components/BarChart.tsx +2 -6
- 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 +4 -1
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
- package/src/components/Brush/BrushSelector.tsx +2 -1
- package/src/components/Brush/MiniChartPreview.tsx +21 -26
- package/src/components/EditorPanel/EditorPanel.tsx +692 -954
- package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +218 -167
- 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 +169 -135
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +154 -109
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +95 -28
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
- package/src/components/EditorPanel/editor-panel.scss +1 -1
- package/src/components/EditorPanel/useEditorPermissions.ts +1 -10
- package/src/components/Forecasting/Forecasting.tsx +2 -2
- package/src/components/ForestPlot/ForestPlot.tsx +26 -22
- package/src/components/HorizonChart/HorizonChart.tsx +10 -2
- package/src/components/HoverLine/HoverLine.tsx +6 -8
- package/src/components/Legend/Legend.Component.tsx +4 -4
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +9 -4
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
- package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
- package/src/components/Legend/helpers/index.ts +0 -2
- package/src/components/LineChart/LineChartProps.ts +1 -0
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +3 -2
- package/src/components/LineChart/index.tsx +11 -4
- package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
- package/src/components/LinearChart/tests/LinearChart.test.tsx +224 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
- package/src/components/LinearChart.tsx +124 -47
- package/src/components/PairedBarChart.jsx +2 -2
- package/src/components/PieChart/PieChart.tsx +30 -13
- package/src/components/RadarChart/RadarChart.tsx +1 -1
- package/src/components/Regions/components/Regions.tsx +36 -26
- package/src/components/Sankey/components/Sankey.tsx +3 -3
- package/src/components/Sankey/sankey.scss +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +1 -0
- package/src/components/SmallMultiples/SmallMultiples.css +5 -5
- package/src/components/Sparkline/components/SparkLine.tsx +1 -1
- package/src/components/Sparkline/index.scss +4 -2
- package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
- package/src/data/initial-state.js +24 -14
- package/src/data/legacy-defaults.ts +18 -0
- package/src/helpers/abbreviateNumber.ts +24 -17
- package/src/helpers/calculateLeftYAxisWidth.ts +193 -0
- 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/helpers/tests/tooltipHelpers.test.ts +151 -0
- package/src/helpers/tooltipHelpers.ts +176 -0
- package/src/hooks/useHighlightedBars.ts +9 -16
- package/src/hooks/useProgrammaticTooltip.ts +16 -2
- package/src/hooks/useRightAxis.ts +14 -0
- package/src/hooks/useScales.ts +92 -56
- package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
- package/src/hooks/useTooltip.tsx +186 -40
- package/src/scss/main.scss +152 -79
- package/src/test/CdcChart.test.jsx +26 -2
- package/src/types/ChartConfig.ts +5 -0
- package/tests/fixtures/chart-config-with-metadata.json +29 -0
- package/tests/fixtures/data-with-metadata.json +10 -0
- package/examples/chart-regression-2.json +0 -2360
- package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
- package/examples/feature/__data__/city-temperature.json +0 -2198
- package/examples/feature/bar/Barchart_with_negative.json +0 -34
- package/examples/feature/boxplot/box-plot-data.json +0 -71
- package/examples/feature/boxplot/boxplot-data.json +0 -156
- package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
- package/examples/feature/forest-plot/log.json +0 -26
- package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
- package/examples/region-issue.json +0 -2065
- package/examples/xaxis.json +0 -493
- /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
- /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
- /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
- /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
- /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
- /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
- /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
- /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
- /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
- /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
- /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 =
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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()
|
|
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
|
|
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={
|
|
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
|
-
|
|
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={
|
|
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) +
|
|
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 =
|
|
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)
|