@cdc/chart 4.26.3 → 4.26.5

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 (192) hide show
  1. package/CONFIG.md +489 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39024 -36587
  4. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  5. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  6. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  7. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  8. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  9. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  10. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  11. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  12. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  13. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  14. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  15. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  16. package/examples/metadata-variables.json +1 -1
  17. package/examples/minimal-example.json +24 -0
  18. package/examples/private/bump-chart.json +1116 -0
  19. package/examples/private/test-123.json +624 -0
  20. package/examples/private/test2.json +35012 -0
  21. package/package.json +5 -4
  22. package/src/CdcChartComponent.tsx +144 -60
  23. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  24. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  25. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  26. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  27. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  28. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
  29. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  30. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  31. package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
  32. package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
  33. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  34. package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
  35. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  36. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  37. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  38. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  39. package/src/_stories/_mock/heatmap.json +156 -0
  40. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  41. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  42. package/src/_stories/_mock/paired-bar.json +1 -1
  43. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  44. package/src/_stories/_mock/sankey_mock.json +78 -0
  45. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  46. package/src/components/Annotations/components/AnnotationDraggable.tsx +2 -2
  47. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  48. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  49. package/src/components/Axis/BottomAxis.tsx +1 -1
  50. package/src/components/Axis/LeftAxis.tsx +15 -14
  51. package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
  52. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  53. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  54. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  55. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  56. package/src/components/BarChart/components/BarChart.tsx +2 -6
  57. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  58. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  59. package/src/components/Brush/BrushSelector.tsx +88 -187
  60. package/src/components/Brush/brushInteraction.ts +22 -0
  61. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  62. package/src/components/EditorPanel/EditorPanel.tsx +946 -960
  63. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  64. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
  65. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  66. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
  67. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
  68. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  69. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
  70. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  71. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  72. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  73. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  74. package/src/components/EditorPanel/useEditorPermissions.ts +32 -21
  75. package/src/components/Forecasting/Forecasting.tsx +2 -2
  76. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  77. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  78. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  79. package/src/components/HeatMap/heatmap.css +71 -0
  80. package/src/components/HeatMap/helpers/index.ts +261 -0
  81. package/src/components/HeatMap/index.tsx +5 -0
  82. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  83. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  84. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  85. package/src/components/HoverLine/HoverLine.tsx +6 -8
  86. package/src/components/Legend/Legend.Component.tsx +4 -4
  87. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  88. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  89. package/src/components/Legend/helpers/index.ts +0 -2
  90. package/src/components/LineChart/LineChartProps.ts +1 -0
  91. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
  92. package/src/components/LineChart/helpers.test.ts +345 -0
  93. package/src/components/LineChart/helpers.ts +15 -5
  94. package/src/components/LineChart/index.tsx +18 -5
  95. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  96. package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
  97. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  98. package/src/components/LinearChart.tsx +155 -77
  99. package/src/components/PairedBarChart.jsx +2 -2
  100. package/src/components/PieChart/PieChart.tsx +15 -10
  101. package/src/components/RadarChart/RadarChart.tsx +3 -1
  102. package/src/components/Regions/components/Regions.tsx +199 -35
  103. package/src/components/Sankey/components/Sankey.tsx +7 -3
  104. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  105. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  106. package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -3
  107. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  108. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  109. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  110. package/src/components/Sparkline/components/SparkLine.tsx +5 -10
  111. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  112. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  113. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  114. package/src/data/initial-state.js +12 -0
  115. package/src/helpers/axisLabelFontSize.ts +8 -0
  116. package/src/helpers/bumpChartTooltip.ts +54 -0
  117. package/src/helpers/calculateLeftYAxisWidth.ts +197 -0
  118. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  119. package/src/helpers/getMinMax.ts +2 -3
  120. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  121. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  122. package/src/helpers/handleChartAriaLabels.ts +23 -10
  123. package/src/helpers/seriesHighlight.ts +26 -0
  124. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  125. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  126. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  127. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  128. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  129. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  130. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  131. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  132. package/src/helpers/tooltipHelpers.ts +176 -0
  133. package/src/hooks/tests/useScales.test.tsx +105 -0
  134. package/src/hooks/useHighlightedBars.ts +9 -16
  135. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  136. package/src/hooks/useScales.ts +19 -9
  137. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  138. package/src/hooks/useTooltip.tsx +167 -38
  139. package/src/index.jsx +5 -2
  140. package/src/scss/main.scss +32 -49
  141. package/src/test/CdcChart.test.jsx +24 -0
  142. package/src/types/ChartConfig.ts +19 -0
  143. package/src/types/ChartContext.ts +2 -0
  144. package/examples/chart-regression-2.json +0 -2360
  145. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  146. package/examples/feature/__data__/city-temperature.json +0 -2198
  147. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  148. package/examples/feature/boxplot/box-plot-data.json +0 -71
  149. package/examples/feature/boxplot/boxplot-data.json +0 -156
  150. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  151. package/examples/feature/forest-plot/log.json +0 -26
  152. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  153. package/examples/region-issue.json +0 -2065
  154. package/examples/xaxis.json +0 -493
  155. package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
  156. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  157. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  158. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  159. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  160. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  161. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  162. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  163. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  164. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  165. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  166. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  167. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  168. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  169. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  170. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  171. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  172. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  173. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  174. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  175. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  176. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  177. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  178. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  179. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  180. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  181. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  182. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  183. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  184. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  185. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  186. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  187. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  188. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  189. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  190. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  191. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  192. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -0,0 +1,26 @@
1
+ import type { Label } from '../types/Label'
2
+
3
+ type LegendHighlightSource = Pick<Label, 'datum'> | string
4
+
5
+ export const getLegendHighlightKey = (
6
+ seriesLabels: Record<string, string> = {},
7
+ legendSource: LegendHighlightSource
8
+ ): string => {
9
+ const labelDatum = typeof legendSource === 'string' ? legendSource : legendSource.datum
10
+
11
+ return Object.entries(seriesLabels).find(([, label]) => label === labelDatum)?.[0] || labelDatum
12
+ }
13
+
14
+ export const shouldResetSeriesHighlight = (
15
+ seriesHighlight: string[],
16
+ seriesKeys: string[] = [],
17
+ highlightKey: string,
18
+ visualizationType?: string
19
+ ): boolean => {
20
+ return (
21
+ seriesHighlight.length + 1 === seriesKeys.length &&
22
+ visualizationType !== 'Forecasting' &&
23
+ seriesKeys.includes(highlightKey) &&
24
+ !seriesHighlight.includes(highlightKey)
25
+ )
26
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { AXIS_LABEL_FONT_SIZE, AXIS_LABEL_FONT_SIZE_SMALL, getAxisLabelFontSize } from '../axisLabelFontSize'
3
+
4
+ describe('axisLabelFontSize', () => {
5
+ it('returns the desktop axis label size for non-mobile visualization viewports', () => {
6
+ expect(getAxisLabelFontSize('sm')).toBe(AXIS_LABEL_FONT_SIZE)
7
+ expect(getAxisLabelFontSize('md')).toBe(AXIS_LABEL_FONT_SIZE)
8
+ expect(getAxisLabelFontSize('lg')).toBe(AXIS_LABEL_FONT_SIZE)
9
+ })
10
+
11
+ it('returns the compact axis label size for mobile visualization viewports', () => {
12
+ expect(getAxisLabelFontSize('xxs')).toBe(AXIS_LABEL_FONT_SIZE_SMALL)
13
+ expect(getAxisLabelFontSize('xs')).toBe(AXIS_LABEL_FONT_SIZE_SMALL)
14
+ })
15
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildBumpChartTooltipHtml } from '../bumpChartTooltip'
3
+
4
+ const buildConfig = overrides => ({
5
+ xAxis: {
6
+ dataKey: 'Timestamp',
7
+ type: 'date-time'
8
+ },
9
+ tooltips: {
10
+ dateDisplayFormat: '%Y'
11
+ },
12
+ columns: {
13
+ Timestamp: {
14
+ name: 'Timestamp',
15
+ label: 'Year',
16
+ tooltips: true
17
+ },
18
+ Frequency: {
19
+ name: 'Frequency',
20
+ label: 'Frequency',
21
+ tooltips: true
22
+ }
23
+ },
24
+ legend: {
25
+ hide: false,
26
+ style: 'circles',
27
+ groupBy: ''
28
+ },
29
+ runtime: {
30
+ xAxis: {
31
+ label: 'Year'
32
+ },
33
+ seriesLabels: {
34
+ Rank: 'Rank'
35
+ },
36
+ seriesLabelsAll: ['Rank', 'Other'],
37
+ seriesKeys: ['Rank', 'Other']
38
+ },
39
+ visualizationType: 'Bump Chart',
40
+ series: [{ dataKey: 'Rank' }, { dataKey: 'Other' }],
41
+ ...overrides
42
+ })
43
+
44
+ const helpers = {
45
+ parseDate: (value: string) => new Date(value),
46
+ formatDate: (date: Date) => String(date.getUTCFullYear()),
47
+ formatTooltipsDate: (date: Date) => `tooltip-${date.getUTCFullYear()}`
48
+ }
49
+
50
+ describe('buildBumpChartTooltipHtml', () => {
51
+ it('uses the shared x-axis formatter in the tooltip heading', () => {
52
+ const html = buildBumpChartTooltipHtml({
53
+ config: buildConfig(),
54
+ colorScale: undefined,
55
+ dataRow: { Timestamp: '2014-01-01T00:00:00.000Z', Rank: '1', Frequency: '149' },
56
+ series: { dataKey: 'Rank' },
57
+ helpers
58
+ })
59
+
60
+ expect(html).toContain('Year: tooltip-2014')
61
+ })
62
+
63
+ it('does not render the raw x-axis value as an extra tooltip row', () => {
64
+ const html = buildBumpChartTooltipHtml({
65
+ config: buildConfig(),
66
+ colorScale: undefined,
67
+ dataRow: { Timestamp: '2014-01-01T00:00:00.000Z', Rank: '1', Frequency: '149' },
68
+ series: { dataKey: 'Rank' },
69
+ helpers
70
+ })
71
+
72
+ expect(html).not.toContain('2014-01-01T00:00:00.000Z')
73
+ expect(html).toContain('Frequency: 149')
74
+ })
75
+ })
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ensureSpecialChartAxisTypes } from '../ensureSpecialChartAxisTypes'
3
+
4
+ describe('ensureSpecialChartAxisTypes', () => {
5
+ it('forces bump charts to use a date-time x-axis', () => {
6
+ const config = {
7
+ visualizationType: 'Bump Chart',
8
+ xAxis: {
9
+ type: 'categorical',
10
+ dataKey: 'Timestamp',
11
+ dateParseFormat: '%m/%d/%Y',
12
+ dateDisplayFormat: '%Y'
13
+ }
14
+ }
15
+
16
+ expect(ensureSpecialChartAxisTypes(config as any).xAxis.type).toBe('date-time')
17
+ })
18
+
19
+ it('leaves non-bump chart axis types unchanged', () => {
20
+ const config = {
21
+ visualizationType: 'Line',
22
+ xAxis: {
23
+ type: 'categorical',
24
+ dataKey: 'Category'
25
+ }
26
+ }
27
+
28
+ expect(ensureSpecialChartAxisTypes(config as any).xAxis.type).toBe('categorical')
29
+ })
30
+ })
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getYAxisAutoPadding } from '../getYAxisAutoPadding'
3
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
+
5
+ const baseConfig = createMockConfig({
6
+ orientation: 'vertical'
7
+ })
8
+
9
+ describe('getYAxisAutoPadding', () => {
10
+ it('preserves the extra-tick escalation for inline-label mode', () => {
11
+ const yScale = {
12
+ ticks: () => [0, 1, 2]
13
+ }
14
+
15
+ const padding = getYAxisAutoPadding(yScale, 3, 2.95, 0, baseConfig, 'inline-label')
16
+
17
+ expect(padding).toBeCloseTo(35.69, 2)
18
+ })
19
+
20
+ it('returns the single-pass padding for top-title mode', () => {
21
+ const yScale = {
22
+ ticks: () => [0, 1, 2]
23
+ }
24
+
25
+ const padding = getYAxisAutoPadding(yScale, 3, 2.95, 0, baseConfig, 'top-title')
26
+
27
+ expect(padding).toBeCloseTo(1.79, 2)
28
+ })
29
+
30
+ it('returns 0 when the data is not close enough to the top tick', () => {
31
+ const yScale = {
32
+ ticks: () => [0, 50, 100]
33
+ }
34
+
35
+ expect(getYAxisAutoPadding(yScale, 3, 80, 0, baseConfig, 'top-title')).toBe(0)
36
+ })
37
+ })
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getYAxisAutoPaddingMode } from '../getYAxisAutoPaddingMode'
3
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
+
5
+ describe('getYAxisAutoPaddingMode', () => {
6
+ it('returns inline-label for multi-word inline labels', () => {
7
+ expect(
8
+ getYAxisAutoPaddingMode(
9
+ createMockConfig({
10
+ yAxis: {
11
+ ...createMockConfig().yAxis,
12
+ titlePlacement: 'side',
13
+ inlineLabel: 'per 100k'
14
+ }
15
+ })
16
+ )
17
+ ).toBe('inline-label')
18
+ })
19
+
20
+ it('returns inline-label for inline labels with leading space', () => {
21
+ expect(
22
+ getYAxisAutoPaddingMode(
23
+ createMockConfig({
24
+ yAxis: {
25
+ ...createMockConfig().yAxis,
26
+ titlePlacement: 'side',
27
+ inlineLabel: ' cases'
28
+ }
29
+ })
30
+ )
31
+ ).toBe('inline-label')
32
+ })
33
+
34
+ it('returns top-title for top y-axis titles with label content', () => {
35
+ expect(
36
+ getYAxisAutoPaddingMode(
37
+ createMockConfig({
38
+ yAxis: {
39
+ ...createMockConfig().yAxis,
40
+ titlePlacement: 'top',
41
+ inlineLabel: ''
42
+ }
43
+ })
44
+ )
45
+ ).toBe('top-title')
46
+ })
47
+
48
+ it('returns none for hidden top y-axis titles', () => {
49
+ expect(
50
+ getYAxisAutoPaddingMode(
51
+ createMockConfig({
52
+ hideYAxisLabel: true,
53
+ yAxis: {
54
+ ...createMockConfig().yAxis,
55
+ titlePlacement: 'top',
56
+ inlineLabel: ''
57
+ }
58
+ })
59
+ )
60
+ ).toBe('none')
61
+ })
62
+
63
+ it('returns none when neither trigger applies', () => {
64
+ expect(
65
+ getYAxisAutoPaddingMode(
66
+ createMockConfig({
67
+ yAxis: {
68
+ ...createMockConfig().yAxis,
69
+ titlePlacement: 'side',
70
+ inlineLabel: 'Rate'
71
+ }
72
+ })
73
+ )
74
+ ).toBe('none')
75
+ })
76
+
77
+ it('returns inline-label when both triggers are present', () => {
78
+ expect(
79
+ getYAxisAutoPaddingMode(
80
+ createMockConfig({
81
+ yAxis: {
82
+ ...createMockConfig().yAxis,
83
+ titlePlacement: 'top',
84
+ inlineLabel: 'per 100k'
85
+ }
86
+ })
87
+ )
88
+ ).toBe('inline-label')
89
+ })
90
+ })
@@ -0,0 +1,71 @@
1
+ import { handleChartAriaLabels } from '../handleChartAriaLabels'
2
+
3
+ describe('handleChartAriaLabels', () => {
4
+ it('returns auto-generated title from visualizationType and title', () => {
5
+ const result = handleChartAriaLabels({
6
+ visualizationType: 'Line',
7
+ title: 'COVID-19 Cases'
8
+ })
9
+ expect(result).toBe('Line chart with the title: COVID-19 Cases')
10
+ })
11
+
12
+ it('returns visualizationType-only title when chart title is absent', () => {
13
+ const result = handleChartAriaLabels({
14
+ visualizationType: 'Bar'
15
+ })
16
+ expect(result).toBe('Bar chart')
17
+ })
18
+
19
+ it('returns fallback when visualizationType is missing', () => {
20
+ const result = handleChartAriaLabels({})
21
+ expect(result).toBe('Data visualization container')
22
+ })
23
+
24
+ describe('description', () => {
25
+ it('concatenates static description after title', () => {
26
+ const result = handleChartAriaLabels({
27
+ visualizationType: 'Line',
28
+ title: 'Trends',
29
+ altText: { type: 'static', value: 'Cases rose steadily over 8 weeks.' }
30
+ })
31
+ expect(result).toBe('Line chart with the title: Trends. Cases rose steadily over 8 weeks.')
32
+ })
33
+
34
+ it('concatenates description from metadata after title', () => {
35
+ const result = handleChartAriaLabels({
36
+ visualizationType: 'Line',
37
+ title: 'Trends',
38
+ altText: { type: 'metadata', metadataKey: 'altDescription' },
39
+ dataMetadata: { altDescription: 'Positivity increased from 4% to 12%.' }
40
+ })
41
+ expect(result).toBe('Line chart with the title: Trends. Positivity increased from 4% to 12%.')
42
+ })
43
+
44
+ it('returns title only when metadata key is missing', () => {
45
+ const result = handleChartAriaLabels({
46
+ visualizationType: 'Line',
47
+ title: 'Trends',
48
+ altText: { type: 'metadata', metadataKey: 'missing' },
49
+ dataMetadata: { other: 'value' }
50
+ })
51
+ expect(result).toBe('Line chart with the title: Trends')
52
+ })
53
+
54
+ it('returns title only when not configured', () => {
55
+ const result = handleChartAriaLabels({
56
+ visualizationType: 'Line',
57
+ title: 'Trends'
58
+ })
59
+ expect(result).toBe('Line chart with the title: Trends')
60
+ })
61
+
62
+ it('returns title only when altText is empty object', () => {
63
+ const result = handleChartAriaLabels({
64
+ visualizationType: 'Line',
65
+ title: 'Trends',
66
+ altText: {}
67
+ })
68
+ expect(result).toBe('Line chart with the title: Trends')
69
+ })
70
+ })
71
+ })
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getLegendHighlightKey, shouldResetSeriesHighlight } from '../seriesHighlight'
3
+
4
+ describe('seriesHighlight helpers', () => {
5
+ const seriesLabels = {
6
+ nonfatal_rate_alldrugs: 'All drugs',
7
+ opioid_nonfatal_rate: 'All opioids',
8
+ stimulant_nonfatal_rate: 'All stimulants'
9
+ }
10
+
11
+ it('normalizes a legend label to the stored series highlight key', () => {
12
+ expect(getLegendHighlightKey(seriesLabels, 'All opioids')).toBe('opioid_nonfatal_rate')
13
+ expect(getLegendHighlightKey(seriesLabels, 'unmapped_series')).toBe('unmapped_series')
14
+ })
15
+
16
+ it('accepts legend label objects and legacy raw string callers', () => {
17
+ expect(
18
+ getLegendHighlightKey(seriesLabels, {
19
+ datum: 'All stimulants',
20
+ index: 2,
21
+ text: 'All stimulants',
22
+ value: '#075290'
23
+ })
24
+ ).toBe('stimulant_nonfatal_rate')
25
+
26
+ expect(getLegendHighlightKey(seriesLabels, 'Highlighted bar')).toBe('Highlighted bar')
27
+ })
28
+
29
+ it('does not reset when removing one selected item from a three-series legend', () => {
30
+ expect(
31
+ shouldResetSeriesHighlight(
32
+ ['nonfatal_rate_alldrugs', 'opioid_nonfatal_rate'],
33
+ Object.keys(seriesLabels),
34
+ 'opioid_nonfatal_rate',
35
+ 'Line'
36
+ )
37
+ ).toBe(false)
38
+ })
39
+
40
+ it('resets when selecting the only remaining unselected item', () => {
41
+ expect(
42
+ shouldResetSeriesHighlight(
43
+ ['nonfatal_rate_alldrugs', 'opioid_nonfatal_rate'],
44
+ Object.keys(seriesLabels),
45
+ 'stimulant_nonfatal_rate',
46
+ 'Line'
47
+ )
48
+ ).toBe(true)
49
+ })
50
+
51
+ it('does not reset when the clicked item is not a known series key', () => {
52
+ expect(
53
+ shouldResetSeriesHighlight(
54
+ ['nonfatal_rate_alldrugs', 'opioid_nonfatal_rate'],
55
+ Object.keys(seriesLabels),
56
+ 'suppressed-data-note',
57
+ 'Line'
58
+ )
59
+ ).toBe(false)
60
+ })
61
+ })
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ buildTooltipListHtml,
4
+ getTooltipMarkerColor,
5
+ getTooltipMarkerShape,
6
+ shouldUseTooltipLegendMarkers,
7
+ tooltipHasMarkerColumn,
8
+ tooltipShouldUseMarkerColumn,
9
+ type TooltipRow
10
+ } from '../tooltipHelpers'
11
+
12
+ const buildConfig = overrides => ({
13
+ visualizationType: 'Line',
14
+ legend: {
15
+ hide: false,
16
+ style: 'circles',
17
+ groupBy: '',
18
+ patterns: {},
19
+ ...overrides?.legend
20
+ },
21
+ runtime: {
22
+ seriesLabels: { cases: 'Cases', deaths: 'Deaths' },
23
+ seriesLabelsAll: ['Cases', 'Deaths'],
24
+ seriesKeys: ['cases', 'deaths'],
25
+ ...overrides?.runtime
26
+ },
27
+ series: [{ dataKey: 'cases' }, { dataKey: 'deaths' }],
28
+ smallMultiples: {
29
+ mode: '',
30
+ ...overrides?.smallMultiples
31
+ },
32
+ ...overrides
33
+ })
34
+
35
+ describe('tooltipHelpers', () => {
36
+ it('enables marker rendering for supported multi-series charts with visible legends', () => {
37
+ expect(shouldUseTooltipLegendMarkers(buildConfig())).toBe(true)
38
+ })
39
+
40
+ it('allows small-multiple tiles to use markers when the source legend is visible', () => {
41
+ expect(shouldUseTooltipLegendMarkers(buildConfig({ legend: { hide: true, tooltipLegendVisible: true } }))).toBe(
42
+ true
43
+ )
44
+ })
45
+
46
+ it('disables markers when the source legend is hidden or the legend is in a special mode', () => {
47
+ expect(shouldUseTooltipLegendMarkers(buildConfig({ legend: { hide: true } }))).toBe(false)
48
+ expect(shouldUseTooltipLegendMarkers(buildConfig({ legend: { hide: true, tooltipLegendVisible: false } }))).toBe(
49
+ false
50
+ )
51
+ expect(shouldUseTooltipLegendMarkers(buildConfig({ legend: { style: 'gradient' } }))).toBe(false)
52
+ expect(shouldUseTooltipLegendMarkers(buildConfig({ legend: { groupBy: 'category' } }))).toBe(false)
53
+ expect(
54
+ shouldUseTooltipLegendMarkers(
55
+ buildConfig({
56
+ runtime: { seriesLabelsAll: ['Cases'], seriesKeys: ['cases'] },
57
+ series: [{ dataKey: 'cases' }]
58
+ })
59
+ )
60
+ ).toBe(false)
61
+ })
62
+
63
+ it('disables markers for single-series small-multiple tiles even when the source legend is visible', () => {
64
+ expect(
65
+ shouldUseTooltipLegendMarkers(
66
+ buildConfig({
67
+ legend: { hide: true, tooltipLegendVisible: true },
68
+ runtime: { seriesLabelsAll: ['Cases'], seriesKeys: ['cases'] },
69
+ series: [{ dataKey: 'cases' }]
70
+ })
71
+ )
72
+ ).toBe(false)
73
+ })
74
+
75
+ it('keeps patterned legends eligible and still uses the base series color', () => {
76
+ expect(shouldUseTooltipLegendMarkers(buildConfig({ legend: { patterns: { striped: { color: '#000' } } } }))).toBe(
77
+ true
78
+ )
79
+ })
80
+
81
+ it('resolves marker colors using the legend label mapping', () => {
82
+ const colorScale = value => ({ Cases: '#1f77b4', deaths: '#d62728' }[value] || null)
83
+
84
+ expect(getTooltipMarkerColor(buildConfig(), colorScale as any, 'cases')).toBe('#1f77b4')
85
+ expect(getTooltipMarkerColor(buildConfig(), colorScale as any, 'unknown')).toBe(null)
86
+ })
87
+
88
+ it('uses square markers only when the legend explicitly uses boxes', () => {
89
+ expect(getTooltipMarkerShape(buildConfig())).toBe('circle')
90
+ expect(getTooltipMarkerShape(buildConfig({ legend: { style: 'boxes' } }))).toBe('square')
91
+ })
92
+
93
+ it('only reserves a marker column when at least one series row has a marker', () => {
94
+ const rows: TooltipRow[] = [
95
+ { key: 'Date', value: '2024-01-01', kind: 'heading' },
96
+ { key: 'Cases', value: '10', kind: 'series', markerColor: '#1f77b4' },
97
+ { key: 'Source', value: 'CDC', kind: 'extra' }
98
+ ]
99
+ const rowsWithoutMarkers: TooltipRow[] = [
100
+ { key: 'Date', value: '2024-01-01', kind: 'heading' },
101
+ { key: 'Source', value: 'CDC', kind: 'extra' }
102
+ ]
103
+
104
+ expect(tooltipHasMarkerColumn(rows)).toBe(true)
105
+ expect(tooltipHasMarkerColumn(rowsWithoutMarkers)).toBe(false)
106
+ })
107
+
108
+ it('suppresses the marker column when two series would render identical marker signatures', () => {
109
+ expect(
110
+ tooltipShouldUseMarkerColumn([
111
+ { markerColor: '#f28e2b', markerShape: 'circle' },
112
+ { markerColor: '#f28e2b', markerShape: 'circle' }
113
+ ])
114
+ ).toBe(false)
115
+
116
+ expect(
117
+ tooltipShouldUseMarkerColumn([
118
+ { markerColor: '#f28e2b', markerShape: 'circle' },
119
+ { markerColor: '#f28e2b', markerShape: 'square' }
120
+ ])
121
+ ).toBe(true)
122
+ })
123
+
124
+ it('renders aligned raw HTML rows only when the tooltip contains a marker', () => {
125
+ const withMarker = buildTooltipListHtml({
126
+ heading: 'Date: Jan 1, 2024',
127
+ bodyRows: [{ text: 'Cases: 10', markerColor: '#1f77b4', markerShape: 'square' }, { text: 'Source: CDC' }]
128
+ })
129
+ const withoutMarker = buildTooltipListHtml({
130
+ heading: 'Date: Jan 1, 2024',
131
+ bodyRows: [{ text: 'Source: CDC' }]
132
+ })
133
+ const ambiguousMarkers = buildTooltipListHtml({
134
+ heading: 'Date: Jan 1, 2024',
135
+ bodyRows: [
136
+ { text: 'Dose 1: 10', markerColor: '#f28e2b', markerShape: 'circle' },
137
+ { text: 'Dose 2: 20', markerColor: '#f28e2b', markerShape: 'circle' }
138
+ ]
139
+ })
140
+
141
+ expect(withMarker).toContain('tooltip-body--marker-layout')
142
+ expect(withMarker).toContain('tooltip-marker-slot')
143
+ expect(withMarker).toContain('tooltip-marker-slot" aria-hidden="true"')
144
+ expect(withMarker).toContain('tooltip-marker-swatch')
145
+ expect(withMarker).toContain('tooltip-marker-swatch--square')
146
+ expect(withoutMarker).not.toContain('tooltip-body--marker-layout')
147
+ expect(withoutMarker).not.toContain('tooltip-marker-slot')
148
+ expect(ambiguousMarkers).not.toContain('tooltip-body--marker-layout')
149
+ expect(ambiguousMarkers).not.toContain('tooltip-marker-slot')
150
+ })
151
+ })