@cdc/chart 4.26.2 → 4.26.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CONFIG.md +169 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39419 -35180
  4. package/examples/__data__/data-with-metadata.json +10 -0
  5. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  6. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  7. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  8. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  9. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  10. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  11. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  12. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  13. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  14. package/examples/metadata-variables.json +58 -0
  15. package/examples/minimal-example.json +24 -0
  16. package/examples/private/test2.json +35012 -0
  17. package/package.json +4 -4
  18. package/src/CdcChart.tsx +8 -4
  19. package/src/CdcChartComponent.tsx +326 -290
  20. package/src/_stories/Chart.CustomColors.smoke.stories.tsx +97 -0
  21. package/src/_stories/Chart.Defaults.smoke.stories.tsx +95 -0
  22. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  23. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  24. package/src/_stories/{Chart.SmallMultiples.stories.tsx → Chart.SmallMultiples.smoke.stories.tsx} +17 -1
  25. package/src/_stories/Chart.SmallestLeftAxisMax.smoke.stories.tsx +64 -0
  26. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +48 -2
  27. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  28. package/src/_stories/ChartBar.Editor.stories.tsx +177 -115
  29. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  30. package/src/_stories/ChartEditor.Editor.stories.tsx +24 -1
  31. package/src/_stories/{Chart.CustomColors.stories.tsx → ChartLine.DashedDates.smoke.stories.tsx} +4 -4
  32. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  33. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  34. package/src/_stories/_mock/paired-bar.json +1 -1
  35. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  36. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.styles.css +10 -10
  38. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
  39. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
  40. package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
  41. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  42. package/src/components/Axis/BottomAxis.tsx +10 -3
  43. package/src/components/Axis/LeftAxis.tsx +2 -4
  44. package/src/components/Axis/LeftAxisGridlines.tsx +1 -3
  45. package/src/components/Axis/PairedBarAxis.tsx +10 -4
  46. package/src/components/BarChart/components/BarChart.Horizontal.tsx +21 -33
  47. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +21 -35
  48. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +21 -36
  49. package/src/components/BarChart/components/BarChart.Vertical.tsx +21 -34
  50. package/src/components/BarChart/components/BarChart.tsx +2 -6
  51. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  52. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  53. package/src/components/BarChart/helpers/useBarChart.ts +4 -1
  54. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  55. package/src/components/Brush/BrushSelector.tsx +2 -1
  56. package/src/components/Brush/MiniChartPreview.tsx +21 -26
  57. package/src/components/EditorPanel/EditorPanel.tsx +692 -954
  58. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  59. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +218 -167
  60. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  61. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +39 -1
  62. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -135
  63. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +154 -109
  64. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  65. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +95 -28
  66. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
  67. package/src/components/EditorPanel/editor-panel.scss +1 -1
  68. package/src/components/EditorPanel/useEditorPermissions.ts +1 -10
  69. package/src/components/Forecasting/Forecasting.tsx +2 -2
  70. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  71. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  72. package/src/components/HoverLine/HoverLine.tsx +6 -8
  73. package/src/components/Legend/Legend.Component.tsx +4 -4
  74. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +9 -4
  75. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  76. package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
  77. package/src/components/Legend/helpers/index.ts +0 -2
  78. package/src/components/LineChart/LineChartProps.ts +1 -0
  79. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +3 -2
  80. package/src/components/LineChart/index.tsx +11 -4
  81. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  82. package/src/components/LinearChart/tests/LinearChart.test.tsx +224 -0
  83. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
  84. package/src/components/LinearChart.tsx +124 -47
  85. package/src/components/PairedBarChart.jsx +2 -2
  86. package/src/components/PieChart/PieChart.tsx +30 -13
  87. package/src/components/RadarChart/RadarChart.tsx +1 -1
  88. package/src/components/Regions/components/Regions.tsx +36 -26
  89. package/src/components/Sankey/components/Sankey.tsx +3 -3
  90. package/src/components/Sankey/sankey.scss +1 -1
  91. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  92. package/src/components/SmallMultiples/SmallMultipleTile.tsx +1 -0
  93. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  94. package/src/components/Sparkline/components/SparkLine.tsx +1 -1
  95. package/src/components/Sparkline/index.scss +4 -2
  96. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  97. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  98. package/src/data/initial-state.js +24 -14
  99. package/src/data/legacy-defaults.ts +18 -0
  100. package/src/helpers/abbreviateNumber.ts +24 -17
  101. package/src/helpers/calculateLeftYAxisWidth.ts +193 -0
  102. package/src/helpers/getChartPatternId.ts +17 -0
  103. package/src/helpers/getMinMax.ts +16 -2
  104. package/src/helpers/seriesColumnSettings.ts +114 -0
  105. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  106. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  107. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  108. package/src/helpers/tooltipHelpers.ts +176 -0
  109. package/src/hooks/useHighlightedBars.ts +9 -16
  110. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  111. package/src/hooks/useRightAxis.ts +14 -0
  112. package/src/hooks/useScales.ts +92 -56
  113. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  114. package/src/hooks/useTooltip.tsx +186 -40
  115. package/src/scss/main.scss +152 -79
  116. package/src/test/CdcChart.test.jsx +26 -2
  117. package/src/types/ChartConfig.ts +5 -0
  118. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  119. package/tests/fixtures/data-with-metadata.json +10 -0
  120. package/examples/chart-regression-2.json +0 -2360
  121. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  122. package/examples/feature/__data__/city-temperature.json +0 -2198
  123. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  124. package/examples/feature/boxplot/box-plot-data.json +0 -71
  125. package/examples/feature/boxplot/boxplot-data.json +0 -156
  126. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  127. package/examples/feature/forest-plot/log.json +0 -26
  128. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  129. package/examples/region-issue.json +0 -2065
  130. package/examples/xaxis.json +0 -493
  131. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  132. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  133. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  134. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  135. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  136. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  137. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  138. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  139. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  140. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  141. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  142. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  143. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  144. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  145. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  146. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  147. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  148. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  149. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  150. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  151. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  152. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  153. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  154. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  155. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  156. /package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +0 -0
  157. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  158. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  159. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  160. /package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +0 -0
  161. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  162. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  163. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  164. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  165. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -1,17 +1,24 @@
1
- export const abbreviateNumber = num => {
2
- let unit = ''
3
- let absNum = Math.abs(num)
4
-
5
- if (absNum >= 1e9) {
6
- unit = 'B'
7
- num = num / 1e9
8
- } else if (absNum >= 1e6) {
9
- unit = 'M'
10
- num = num / 1e6
11
- } else if (absNum >= 1e3) {
12
- unit = 'K'
13
- num = num / 1e3
14
- }
15
-
16
- return num + unit
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
+ }
@@ -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
+ })