@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
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
const abbreviationUnits: Record<string, { K: string; M: string; B: string }> = {
|
|
2
|
+
'es-MX': { K: ' mil', M: ' M', B: ' mil M' }
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const defaultUnits = { K: 'K', M: 'M', B: 'B' }
|
|
6
|
+
|
|
7
|
+
export const abbreviateNumber = (num, locale?: string) => {
|
|
8
|
+
const units = (locale && abbreviationUnits[locale]) || defaultUnits
|
|
9
|
+
let unit = ''
|
|
10
|
+
let absNum = Math.abs(num)
|
|
11
|
+
|
|
12
|
+
if (absNum >= 1e9) {
|
|
13
|
+
unit = units.B
|
|
14
|
+
num = num / 1e9
|
|
15
|
+
} else if (absNum >= 1e6) {
|
|
16
|
+
unit = units.M
|
|
17
|
+
num = num / 1e6
|
|
18
|
+
} else if (absNum >= 1e3) {
|
|
19
|
+
unit = units.K
|
|
20
|
+
num = num / 1e3
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return num + unit
|
|
24
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_TICK_LENGTH,
|
|
4
|
+
TICK_LABEL_MARGIN_RIGHT,
|
|
5
|
+
VALUE_ON_LINE_PADDING_NO_AXIS,
|
|
6
|
+
VALUE_ON_LINE_PADDING_WITH_AXIS
|
|
7
|
+
} from '../components/Axis/axis.constants'
|
|
8
|
+
|
|
9
|
+
type CalculateLeftYAxisWidthProps = {
|
|
10
|
+
config: any
|
|
11
|
+
data: any[]
|
|
12
|
+
yScale: any
|
|
13
|
+
numTicks: number
|
|
14
|
+
parentWidth: number
|
|
15
|
+
tickLabelFont: string
|
|
16
|
+
axisLabelFontSize: number
|
|
17
|
+
handleLeftTickFormatting: (tick: any, index: number, ticks: any[]) => string
|
|
18
|
+
categoryLabelSpace?: number
|
|
19
|
+
horizontalYAxisLabelSpace?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AXIS_LABEL_PADDING = 5
|
|
23
|
+
const MIN_LEFT_AXIS_WIDTH = 0
|
|
24
|
+
const MAX_LEFT_AXIS_WIDTH_RATIO = 0.45
|
|
25
|
+
|
|
26
|
+
type TickValueProps = Omit<
|
|
27
|
+
CalculateLeftYAxisWidthProps,
|
|
28
|
+
'parentWidth' | 'tickLabelFont' | 'axisLabelFontSize' | 'handleLeftTickFormatting'
|
|
29
|
+
>
|
|
30
|
+
|
|
31
|
+
const getTickValues = ({ config, data, yScale, numTicks }: TickValueProps) => {
|
|
32
|
+
if (config.visualizationType === 'Forest Plot') {
|
|
33
|
+
return data.map((_, index) => index)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof yScale?.ticks === 'function') {
|
|
37
|
+
return yScale.ticks(numTicks)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof yScale?.domain === 'function') {
|
|
41
|
+
return yScale.domain()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const getAxisLabelSpace = (config: any, axisLabelFontSize: number) => {
|
|
48
|
+
return !config.hideYAxisLabel && config.runtime.yAxis.label ? axisLabelFontSize + AXIS_LABEL_PADDING : 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const measureWidestLabel = (labels: string[], tickLabelFont: string) => {
|
|
52
|
+
return labels.length ? Math.max(...labels.map(label => getTextWidth(label, tickLabelFont))) : 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const clampYAxisWidth = (calculatedWidth: number, parentWidth: number) => {
|
|
56
|
+
const maxAllowedWidth = parentWidth * MAX_LEFT_AXIS_WIDTH_RATIO
|
|
57
|
+
return Math.max(MIN_LEFT_AXIS_WIDTH, Math.min(Math.ceil(calculatedWidth), maxAllowedWidth))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const formatMeasuredTickLabels = ({
|
|
61
|
+
tickValues,
|
|
62
|
+
handleLeftTickFormatting,
|
|
63
|
+
inlineLabel,
|
|
64
|
+
labelsAboveGridlines
|
|
65
|
+
}: {
|
|
66
|
+
tickValues: any[]
|
|
67
|
+
handleLeftTickFormatting: (tick: any, index: number, ticks: any[]) => string
|
|
68
|
+
inlineLabel?: string
|
|
69
|
+
labelsAboveGridlines?: boolean
|
|
70
|
+
}) => {
|
|
71
|
+
const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
|
|
72
|
+
|
|
73
|
+
return tickValues.map((tick, index, ticks) => {
|
|
74
|
+
const formatted = handleLeftTickFormatting(tick, index, ticks)
|
|
75
|
+
const isLastTick = index === ticks.length - 1
|
|
76
|
+
|
|
77
|
+
if (isLastTick && inlineLabel && (labelsAboveGridlines || inlineLabelHasNoSpace)) {
|
|
78
|
+
return `${formatted}${inlineLabel}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return String(formatted ?? '')
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const getHorizonChartWidth = ({
|
|
86
|
+
config,
|
|
87
|
+
parentWidth,
|
|
88
|
+
tickLabelFont,
|
|
89
|
+
axisLabelFontSize
|
|
90
|
+
}: Pick<CalculateLeftYAxisWidthProps, 'config' | 'parentWidth' | 'tickLabelFont' | 'axisLabelFontSize'>) => {
|
|
91
|
+
const seriesKeys =
|
|
92
|
+
config.runtime?.seriesKeys && config.runtime.seriesKeys.length > 0
|
|
93
|
+
? config.runtime.seriesKeys
|
|
94
|
+
: config.series?.map(series => series.dataKey) || []
|
|
95
|
+
const labels = seriesKeys.map(seriesKey => String(config.runtime?.seriesLabels?.[seriesKey] || seriesKey))
|
|
96
|
+
const labelWidth = config.yAxis.hideLabel ? 0 : measureWidestLabel(labels, tickLabelFont)
|
|
97
|
+
const calculatedWidth =
|
|
98
|
+
labelWidth +
|
|
99
|
+
DEFAULT_TICK_LENGTH +
|
|
100
|
+
TICK_LABEL_MARGIN_RIGHT +
|
|
101
|
+
getAxisLabelSpace(config, axisLabelFontSize) +
|
|
102
|
+
Number(config.yAxis.axisPadding || 0)
|
|
103
|
+
|
|
104
|
+
return clampYAxisWidth(calculatedWidth, parentWidth)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const getHorizontalAxisWidth = ({
|
|
108
|
+
config,
|
|
109
|
+
tickLabelWidth,
|
|
110
|
+
categoryLabelSpace,
|
|
111
|
+
horizontalYAxisLabelSpace
|
|
112
|
+
}: Pick<CalculateLeftYAxisWidthProps, 'config' | 'categoryLabelSpace' | 'horizontalYAxisLabelSpace'> & {
|
|
113
|
+
tickLabelWidth: number
|
|
114
|
+
}) => {
|
|
115
|
+
const horizontalTickWidth = tickLabelWidth + DEFAULT_TICK_LENGTH + TICK_LABEL_MARGIN_RIGHT
|
|
116
|
+
return Math.max(horizontalTickWidth + horizontalYAxisLabelSpace, categoryLabelSpace + horizontalYAxisLabelSpace)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const getCategoricalAxisWidth = ({
|
|
120
|
+
config,
|
|
121
|
+
baseWidth,
|
|
122
|
+
tickLabelFont
|
|
123
|
+
}: Pick<CalculateLeftYAxisWidthProps, 'config' | 'tickLabelFont'> & {
|
|
124
|
+
baseWidth: number
|
|
125
|
+
}) => {
|
|
126
|
+
if (config.yAxis.type !== 'categorical' || config.orientation !== 'vertical' || config.yAxis.hideLabel) {
|
|
127
|
+
return baseWidth
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const categoryLabels = (config.yAxis.categories || []).map(category => String(category?.label || ''))
|
|
131
|
+
const categoryLabelWidth = measureWidestLabel(categoryLabels, tickLabelFont)
|
|
132
|
+
|
|
133
|
+
return Math.max(baseWidth, categoryLabelWidth + DEFAULT_TICK_LENGTH + TICK_LABEL_MARGIN_RIGHT)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const calculateLeftYAxisWidth = ({
|
|
137
|
+
config,
|
|
138
|
+
data,
|
|
139
|
+
yScale,
|
|
140
|
+
numTicks,
|
|
141
|
+
parentWidth,
|
|
142
|
+
tickLabelFont,
|
|
143
|
+
axisLabelFontSize,
|
|
144
|
+
handleLeftTickFormatting,
|
|
145
|
+
categoryLabelSpace = 0,
|
|
146
|
+
horizontalYAxisLabelSpace = 0
|
|
147
|
+
}: CalculateLeftYAxisWidthProps): number => {
|
|
148
|
+
if (config.visualizationType === 'Horizon Chart') {
|
|
149
|
+
return getHorizonChartWidth({ config, parentWidth, tickLabelFont, axisLabelFontSize })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const tickValues = getTickValues({ config, data, yScale, numTicks })
|
|
153
|
+
const { hideAxis, inlineLabel, labelsAboveGridlines } = config.yAxis
|
|
154
|
+
const formattedTicks = formatMeasuredTickLabels({
|
|
155
|
+
tickValues,
|
|
156
|
+
handleLeftTickFormatting,
|
|
157
|
+
inlineLabel,
|
|
158
|
+
labelsAboveGridlines
|
|
159
|
+
})
|
|
160
|
+
const tickLabelWidth = config.yAxis.hideLabel ? 0 : measureWidestLabel(formattedTicks, tickLabelFont)
|
|
161
|
+
|
|
162
|
+
const valueOnLinePadding = hideAxis
|
|
163
|
+
? Math.abs(VALUE_ON_LINE_PADDING_NO_AXIS)
|
|
164
|
+
: Math.abs(VALUE_ON_LINE_PADDING_WITH_AXIS)
|
|
165
|
+
const tickPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT + DEFAULT_TICK_LENGTH
|
|
166
|
+
const verticalAxisLabelSpace = getAxisLabelSpace(config, axisLabelFontSize)
|
|
167
|
+
const reservedTickLabelWidth = labelsAboveGridlines ? 0 : tickLabelWidth
|
|
168
|
+
const reservedTickPadding = labelsAboveGridlines || config.yAxis.hideLabel ? 0 : tickPadding
|
|
169
|
+
|
|
170
|
+
const defaultCalculatedWidth =
|
|
171
|
+
reservedTickLabelWidth + reservedTickPadding + verticalAxisLabelSpace + Number(config.yAxis.axisPadding || 0)
|
|
172
|
+
|
|
173
|
+
if (config.orientation === 'horizontal') {
|
|
174
|
+
const calculatedWidth = getHorizontalAxisWidth({
|
|
175
|
+
config,
|
|
176
|
+
tickLabelWidth,
|
|
177
|
+
categoryLabelSpace,
|
|
178
|
+
horizontalYAxisLabelSpace
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return clampYAxisWidth(calculatedWidth, parentWidth)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const calculatedWidth = getCategoricalAxisWidth({
|
|
185
|
+
config,
|
|
186
|
+
baseWidth: defaultCalculatedWidth,
|
|
187
|
+
tickLabelFont
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return clampYAxisWidth(calculatedWidth, parentWidth)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default calculateLeftYAxisWidth
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sanitizeToSvgId } from '@cdc/core/helpers/cove/string'
|
|
2
|
+
|
|
3
|
+
const getStableKeyHash = (input: string): string => {
|
|
4
|
+
// djb2 variant, deterministic and cheap for short editor keys
|
|
5
|
+
let hash = 5381
|
|
6
|
+
for (let i = 0; i < input.length; i++) {
|
|
7
|
+
hash = (hash * 33) ^ input.charCodeAt(i)
|
|
8
|
+
}
|
|
9
|
+
return (hash >>> 0).toString(36)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const getChartPatternId = (patternKey: string): string => {
|
|
13
|
+
const rawKey = String(patternKey)
|
|
14
|
+
const sanitizedKey = sanitizeToSvgId(rawKey)
|
|
15
|
+
const hashSuffix = getStableKeyHash(rawKey)
|
|
16
|
+
return `chart-pattern-${sanitizedKey}-${hashSuffix}`
|
|
17
|
+
}
|
package/src/helpers/getMinMax.ts
CHANGED
|
@@ -37,8 +37,8 @@ const getMinMax = ({
|
|
|
37
37
|
let leftMax = 0
|
|
38
38
|
let rightMax = 0
|
|
39
39
|
|
|
40
|
-
if (!data) {
|
|
41
|
-
return { min, max }
|
|
40
|
+
if (!data || !config.runtime) {
|
|
41
|
+
return { min, max, leftMax, rightMax }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const { visualizationType, series } = config
|
|
@@ -248,6 +248,20 @@ const getMinMax = ({
|
|
|
248
248
|
min = min / 1.1
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// Enforce smallest left axis max so small-data charts don't show misleading decimal ticks
|
|
252
|
+
const smallestLeftAxisMaxRaw = config.yAxis.smallestLeftAxisMax
|
|
253
|
+
if (smallestLeftAxisMaxRaw !== null && smallestLeftAxisMaxRaw !== '') {
|
|
254
|
+
const smallestLeftAxisMax = Number(smallestLeftAxisMaxRaw)
|
|
255
|
+
if (!Number.isNaN(smallestLeftAxisMax)) {
|
|
256
|
+
if (max < smallestLeftAxisMax) {
|
|
257
|
+
max = smallestLeftAxisMax
|
|
258
|
+
}
|
|
259
|
+
if (leftMax < smallestLeftAxisMax) {
|
|
260
|
+
leftMax = smallestLeftAxisMax
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
251
265
|
return { min, max, leftMax, rightMax }
|
|
252
266
|
}
|
|
253
267
|
export default getMinMax
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Column } from '@cdc/core/types/Column'
|
|
2
|
+
import { Series } from '@cdc/core/types/Series'
|
|
3
|
+
|
|
4
|
+
type ChartColumns = Record<string, Partial<Column>>
|
|
5
|
+
type SeriesItem = Series[number]
|
|
6
|
+
type ColumnFormattingParams = {
|
|
7
|
+
addColPrefix?: string
|
|
8
|
+
addColSuffix?: string
|
|
9
|
+
addColRoundTo?: number
|
|
10
|
+
addColCommas?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const hasOwn = (object: object, key: keyof Column) => Object.prototype.hasOwnProperty.call(object, key)
|
|
14
|
+
|
|
15
|
+
export const createDefaultSeriesColumnConfig = (columnName: string): Column => ({
|
|
16
|
+
name: columnName,
|
|
17
|
+
label: columnName,
|
|
18
|
+
prefix: '',
|
|
19
|
+
suffix: '',
|
|
20
|
+
roundToPlace: 0,
|
|
21
|
+
commas: false,
|
|
22
|
+
dataTable: true,
|
|
23
|
+
order: undefined,
|
|
24
|
+
showInViz: false,
|
|
25
|
+
startingPoint: '0',
|
|
26
|
+
series: undefined,
|
|
27
|
+
tooltips: false,
|
|
28
|
+
forestPlot: false,
|
|
29
|
+
forestPlotAlignRight: false,
|
|
30
|
+
forestPlotStartingPoint: 0
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const getSeriesOwnedColumnNames = (series: Partial<SeriesItem>[] = []): string[] => {
|
|
34
|
+
return series.map(item => item?.dataKey).filter(Boolean)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const findColumnConfigByName = (
|
|
38
|
+
columns: ChartColumns = {},
|
|
39
|
+
columnName: string
|
|
40
|
+
): { columnKey: string; columnConfig: Partial<Column> } | null => {
|
|
41
|
+
for (const [columnKey, columnConfig] of Object.entries(columns)) {
|
|
42
|
+
if (columnConfig?.name === columnName || (!columnConfig?.name && columnKey === columnName)) {
|
|
43
|
+
return { columnKey, columnConfig }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const getSeriesColumnConfig = (columns: ChartColumns = {}, seriesKey: string) => {
|
|
51
|
+
const existingEntry = findColumnConfigByName(columns, seriesKey)
|
|
52
|
+
const baseColumnConfig = createDefaultSeriesColumnConfig(seriesKey)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
columnKey: existingEntry?.columnKey || seriesKey,
|
|
56
|
+
columnConfig: {
|
|
57
|
+
...baseColumnConfig,
|
|
58
|
+
...(existingEntry?.columnConfig || {}),
|
|
59
|
+
name: seriesKey,
|
|
60
|
+
label: existingEntry?.columnConfig?.label ?? baseColumnConfig.label
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const upsertSeriesColumnConfig = (
|
|
66
|
+
columns: ChartColumns = {},
|
|
67
|
+
seriesKey: string,
|
|
68
|
+
updates: Partial<Column>
|
|
69
|
+
): ChartColumns => {
|
|
70
|
+
const existingEntry = findColumnConfigByName(columns, seriesKey)
|
|
71
|
+
const columnKey = existingEntry?.columnKey || seriesKey
|
|
72
|
+
const nextColumnConfig = {
|
|
73
|
+
...(existingEntry?.columnConfig || {}),
|
|
74
|
+
...updates,
|
|
75
|
+
name: seriesKey
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
nextColumnConfig.label === undefined &&
|
|
80
|
+
!hasOwn(existingEntry?.columnConfig || {}, 'label') &&
|
|
81
|
+
!hasOwn(updates, 'label')
|
|
82
|
+
) {
|
|
83
|
+
delete nextColumnConfig.label
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...columns,
|
|
88
|
+
[columnKey]: nextColumnConfig
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const getSeriesColumnFormattingParams = (columnConfig?: Partial<Column>): ColumnFormattingParams | undefined => {
|
|
93
|
+
if (!columnConfig) return undefined
|
|
94
|
+
|
|
95
|
+
const formattingParams: ColumnFormattingParams = {}
|
|
96
|
+
|
|
97
|
+
if (hasOwn(columnConfig, 'prefix')) {
|
|
98
|
+
formattingParams.addColPrefix = columnConfig.prefix ?? ''
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (hasOwn(columnConfig, 'suffix')) {
|
|
102
|
+
formattingParams.addColSuffix = columnConfig.suffix ?? ''
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (hasOwn(columnConfig, 'roundToPlace')) {
|
|
106
|
+
formattingParams.addColRoundTo = columnConfig.roundToPlace ?? 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (hasOwn(columnConfig, 'commas')) {
|
|
110
|
+
formattingParams.addColCommas = columnConfig.commas ?? false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Object.keys(formattingParams).length ? formattingParams : undefined
|
|
114
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { countNumOfTicks } from '../countNumOfTicks'
|
|
2
|
+
import { expect, describe, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
const baseArgs = {
|
|
5
|
+
max: 100,
|
|
6
|
+
min: 0,
|
|
7
|
+
data: [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }],
|
|
8
|
+
config: { visualizationType: 'Bar' } as any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('countNumOfTicks', () => {
|
|
12
|
+
it('uses viewport-specific tick count when viewportNumTicks[currentViewport] is set', () => {
|
|
13
|
+
const result = countNumOfTicks({
|
|
14
|
+
...baseArgs,
|
|
15
|
+
axis: 'xAxis',
|
|
16
|
+
runtime: { xAxis: { numTicks: 6, viewportNumTicks: { xs: 3, xxs: 2 } } },
|
|
17
|
+
currentViewport: 'xs',
|
|
18
|
+
isHorizontal: false
|
|
19
|
+
})
|
|
20
|
+
expect(result).toBe(3)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('falls back to numTicks when viewportNumTicks is absent', () => {
|
|
24
|
+
const result = countNumOfTicks({
|
|
25
|
+
...baseArgs,
|
|
26
|
+
axis: 'xAxis',
|
|
27
|
+
runtime: { xAxis: { numTicks: 6 } },
|
|
28
|
+
currentViewport: 'xs',
|
|
29
|
+
isHorizontal: false
|
|
30
|
+
})
|
|
31
|
+
expect(result).toBe(6)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('falls back to numTicks when current viewport has no entry in viewportNumTicks', () => {
|
|
35
|
+
const result = countNumOfTicks({
|
|
36
|
+
...baseArgs,
|
|
37
|
+
axis: 'xAxis',
|
|
38
|
+
runtime: { xAxis: { numTicks: 6, viewportNumTicks: { xxs: 2 } } },
|
|
39
|
+
currentViewport: 'lg',
|
|
40
|
+
isHorizontal: false
|
|
41
|
+
})
|
|
42
|
+
expect(result).toBe(6)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('xAxis vertical with numTicks: 6 returns 6', () => {
|
|
46
|
+
const result = countNumOfTicks({
|
|
47
|
+
...baseArgs,
|
|
48
|
+
axis: 'xAxis',
|
|
49
|
+
runtime: { xAxis: { numTicks: 6 } },
|
|
50
|
+
currentViewport: 'lg',
|
|
51
|
+
isHorizontal: false
|
|
52
|
+
})
|
|
53
|
+
expect(result).toBe(6)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('yAxis with numTicks: 4 returns 4', () => {
|
|
57
|
+
const result = countNumOfTicks({
|
|
58
|
+
...baseArgs,
|
|
59
|
+
axis: 'yAxis',
|
|
60
|
+
runtime: { yAxis: { numTicks: 4 } },
|
|
61
|
+
currentViewport: 'lg',
|
|
62
|
+
isHorizontal: false
|
|
63
|
+
})
|
|
64
|
+
expect(result).toBe(4)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('xAxis horizontal with no numTicks returns 4 (hardcoded fallback)', () => {
|
|
68
|
+
const result = countNumOfTicks({
|
|
69
|
+
...baseArgs,
|
|
70
|
+
axis: 'xAxis',
|
|
71
|
+
runtime: { xAxis: { numTicks: '' } },
|
|
72
|
+
currentViewport: 'lg',
|
|
73
|
+
isHorizontal: true
|
|
74
|
+
})
|
|
75
|
+
expect(result).toBe(4)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDefaultSeriesColumnConfig,
|
|
3
|
+
findColumnConfigByName,
|
|
4
|
+
getSeriesColumnConfig,
|
|
5
|
+
getSeriesColumnFormattingParams,
|
|
6
|
+
getSeriesOwnedColumnNames,
|
|
7
|
+
upsertSeriesColumnConfig
|
|
8
|
+
} from '../seriesColumnSettings'
|
|
9
|
+
|
|
10
|
+
describe('seriesColumnSettings', () => {
|
|
11
|
+
it('returns all current series data keys as owned column names', () => {
|
|
12
|
+
expect(getSeriesOwnedColumnNames([{ dataKey: 'cases' }, { dataKey: 'deaths' }, {}])).toEqual(['cases', 'deaths'])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('finds an existing column config by configured name', () => {
|
|
16
|
+
const result = findColumnConfigByName(
|
|
17
|
+
{
|
|
18
|
+
additionalColumn1: { name: 'cases', label: 'Cases', tooltips: true }
|
|
19
|
+
},
|
|
20
|
+
'cases'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
columnKey: 'additionalColumn1',
|
|
25
|
+
columnConfig: { name: 'cases', label: 'Cases', tooltips: true }
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns a default-backed series column config when none exists yet', () => {
|
|
30
|
+
const result = getSeriesColumnConfig({}, 'cases')
|
|
31
|
+
|
|
32
|
+
expect(result.columnKey).toBe('cases')
|
|
33
|
+
expect(result.columnConfig).toEqual(createDefaultSeriesColumnConfig('cases'))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('updates an existing matching column config without changing its key', () => {
|
|
37
|
+
const updatedColumns = upsertSeriesColumnConfig(
|
|
38
|
+
{
|
|
39
|
+
additionalColumn1: { name: 'cases', label: 'Cases', dataTable: false }
|
|
40
|
+
},
|
|
41
|
+
'cases',
|
|
42
|
+
{ prefix: '$', commas: true }
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(updatedColumns).toEqual({
|
|
46
|
+
additionalColumn1: {
|
|
47
|
+
name: 'cases',
|
|
48
|
+
label: 'Cases',
|
|
49
|
+
dataTable: false,
|
|
50
|
+
prefix: '$',
|
|
51
|
+
commas: true
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('does not persist display defaults when creating a new series-owned column config', () => {
|
|
57
|
+
expect(upsertSeriesColumnConfig({}, 'cases', { label: 'Cases' })).toEqual({
|
|
58
|
+
cases: {
|
|
59
|
+
name: 'cases',
|
|
60
|
+
label: 'Cases'
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('only returns explicit formatting overrides and preserves falsey values', () => {
|
|
66
|
+
expect(
|
|
67
|
+
getSeriesColumnFormattingParams({
|
|
68
|
+
prefix: '',
|
|
69
|
+
suffix: ' units',
|
|
70
|
+
roundToPlace: 0,
|
|
71
|
+
commas: false
|
|
72
|
+
})
|
|
73
|
+
).toEqual({
|
|
74
|
+
addColPrefix: '',
|
|
75
|
+
addColSuffix: ' units',
|
|
76
|
+
addColRoundTo: 0,
|
|
77
|
+
addColCommas: false
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns undefined when no formatting overrides were explicitly configured', () => {
|
|
82
|
+
expect(getSeriesColumnFormattingParams({ label: 'Cases' })).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
})
|