@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,152 @@
1
+ import React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import ConfigContext from '../../../ConfigContext'
5
+ import SmallMultipleTile from '../SmallMultipleTile'
6
+ import { createMockChartContext, createMockConfig } from '../../LinearChart/tests/mockConfigContext'
7
+
8
+ const linearChartConfigs = vi.hoisted(() => [] as any[])
9
+
10
+ vi.stubGlobal(
11
+ 'ResizeObserver',
12
+ vi.fn(callback => ({
13
+ observe: vi.fn(() => callback([{ contentRect: { width: 200, height: 300 } }])),
14
+ unobserve: vi.fn(),
15
+ disconnect: vi.fn()
16
+ }))
17
+ )
18
+
19
+ vi.mock('@visx/responsive/lib/components/ParentSize', () => ({
20
+ default: ({ children }) => children({ width: 200, height: 300 })
21
+ }))
22
+
23
+ vi.mock('../../LinearChart', async () => {
24
+ const React = await vi.importActual<typeof import('react')>('react')
25
+ const { default: ConfigContext } = await vi.importActual<any>('../../../ConfigContext')
26
+
27
+ const MockLinearChart = () => {
28
+ const { config } = React.useContext(ConfigContext)
29
+ linearChartConfigs.push(config)
30
+ return <div data-testid='mock-linear-chart' />
31
+ }
32
+
33
+ return {
34
+ default: MockLinearChart
35
+ }
36
+ })
37
+
38
+ describe('SmallMultipleTile', () => {
39
+ beforeEach(() => {
40
+ linearChartConfigs.length = 0
41
+ })
42
+
43
+ it('renders the top y-axis title below the tile title on the first tile in a row and sizes it from tile width', () => {
44
+ const config = createMockConfig({
45
+ smallMultiples: {
46
+ mode: 'by-series',
47
+ tilesPerRowDesktop: 3,
48
+ tilesPerRowMobile: 1
49
+ },
50
+ series: [{ dataKey: 'Value', type: 'Line' }] as any,
51
+ runtime: {
52
+ ...createMockConfig().runtime,
53
+ series: [{ dataKey: 'Value', type: 'Line' }] as any,
54
+ seriesKeys: ['Value'],
55
+ seriesLabels: { Value: 'Value' },
56
+ seriesLabelsAll: ['Value']
57
+ } as any
58
+ })
59
+
60
+ const context = createMockChartContext(config, {
61
+ vizViewport: 'lg'
62
+ } as any)
63
+
64
+ const { container } = render(
65
+ <ConfigContext.Provider value={context}>
66
+ <SmallMultipleTile mode='by-series' config={config} data={[]} tileKey='Value' seriesKey='Value' isFirstInRow />
67
+ </ConfigContext.Provider>
68
+ )
69
+
70
+ const header = container.querySelector('.tile-header')
71
+ const title = screen.getByText('Value')
72
+ const topLabel = container.querySelector('.y-axis-top-title')
73
+
74
+ expect(header).toBeTruthy()
75
+ expect(title).toBeTruthy()
76
+ expect(topLabel).toBeTruthy()
77
+ expect(topLabel).toHaveTextContent('Y-Axis')
78
+ expect(topLabel).toHaveStyle({ fontSize: '14px' })
79
+ expect(header?.firstElementChild).toBe(title)
80
+ expect(header?.lastElementChild).toBe(topLabel)
81
+ })
82
+
83
+ it('does not render the top y-axis title on non-leading tiles in a row', () => {
84
+ const config = createMockConfig({
85
+ smallMultiples: {
86
+ mode: 'by-series',
87
+ tilesPerRowDesktop: 3,
88
+ tilesPerRowMobile: 1
89
+ },
90
+ series: [{ dataKey: 'Value', type: 'Line' }] as any,
91
+ runtime: {
92
+ ...createMockConfig().runtime,
93
+ series: [{ dataKey: 'Value', type: 'Line' }] as any,
94
+ seriesKeys: ['Value'],
95
+ seriesLabels: { Value: 'Value' },
96
+ seriesLabelsAll: ['Value']
97
+ } as any
98
+ })
99
+
100
+ const context = createMockChartContext(config, {
101
+ vizViewport: 'lg'
102
+ } as any)
103
+
104
+ const { container } = render(
105
+ <ConfigContext.Provider value={context}>
106
+ <SmallMultipleTile
107
+ mode='by-series'
108
+ config={config}
109
+ data={[]}
110
+ tileKey='Value'
111
+ seriesKey='Value'
112
+ isFirstInRow={false}
113
+ />
114
+ </ConfigContext.Provider>
115
+ )
116
+
117
+ expect(container.querySelector('.y-axis-top-title')).toBeFalsy()
118
+ expect(linearChartConfigs[0].hideYAxisLabel).toBe(false)
119
+ })
120
+
121
+ it('does not render the top y-axis title when the author hides the y-axis label', () => {
122
+ const config = createMockConfig({
123
+ hideYAxisLabel: true,
124
+ smallMultiples: {
125
+ mode: 'by-series',
126
+ tilesPerRowDesktop: 3,
127
+ tilesPerRowMobile: 1
128
+ },
129
+ series: [{ dataKey: 'Value', type: 'Line' }] as any,
130
+ runtime: {
131
+ ...createMockConfig().runtime,
132
+ series: [{ dataKey: 'Value', type: 'Line' }] as any,
133
+ seriesKeys: ['Value'],
134
+ seriesLabels: { Value: 'Value' },
135
+ seriesLabelsAll: ['Value']
136
+ } as any
137
+ })
138
+
139
+ const context = createMockChartContext(config, {
140
+ vizViewport: 'lg'
141
+ } as any)
142
+
143
+ const { container } = render(
144
+ <ConfigContext.Provider value={context}>
145
+ <SmallMultipleTile mode='by-series' config={config} data={[]} tileKey='Value' seriesKey='Value' isFirstInRow />
146
+ </ConfigContext.Provider>
147
+ )
148
+
149
+ expect(container.querySelector('.y-axis-top-title')).toBeFalsy()
150
+ expect(linearChartConfigs[0].hideYAxisLabel).toBe(true)
151
+ })
152
+ })
@@ -28,13 +28,16 @@ const SparkLine: React.FC<SparkLineProps> = props => {
28
28
  colorScale,
29
29
  handleChartAriaLabels
30
30
  } = useContext(ConfigContext)
31
+
32
+ const a11y = handleChartAriaLabels(config)
33
+
31
34
  let width = Number(parentWidth)
32
35
  const { minValue, maxValue } = useReduceData(config, data, ConfigContext)
33
36
 
34
37
  const margin = { top: 5, right: 20, bottom: 10, left: 10 }
35
38
  const height = Number(parentHeight)
36
39
 
37
- const xMax = width - config.runtime.yAxis.size
40
+ const xMax = width - 50
38
41
  const yMax = height - margin.top - 20
39
42
 
40
43
  const getXAxisData = d =>
@@ -109,15 +112,7 @@ const SparkLine: React.FC<SparkLineProps> = props => {
109
112
 
110
113
  return (
111
114
  <ErrorBoundary component='SparkLine'>
112
- <svg
113
- role='img'
114
- aria-label={handleChartAriaLabels(config)}
115
- width={parentWidth}
116
- height={100}
117
- className={'sparkline'}
118
- tabIndex={0}
119
- >
120
- <title>{`Spark line graphic with the title ${config.title ? config.title : 'No Title Found'}`}</title>
115
+ <svg role='img' aria-label={a11y} width={parentWidth} height={100} className={'sparkline'} tabIndex={0}>
121
116
  {config.runtime.lineSeriesKeys?.length > 0
122
117
  ? config.runtime.lineSeriesKeys
123
118
  : config.runtime.seriesKeys.map((seriesKey, index) => (
@@ -15,6 +15,7 @@ type WarmingStripesProps = {
15
15
  yScale: any
16
16
  xMax: number
17
17
  yMax: number
18
+ yAxisWidth: number
18
19
  synchronizedXValue?: any
19
20
  showTooltip: (args: any) => void
20
21
  handleTooltipMouseOff: () => void
@@ -23,6 +24,7 @@ type WarmingStripesProps = {
23
24
  const WarmingStripes = ({
24
25
  xMax,
25
26
  yMax,
27
+ yAxisWidth,
26
28
  synchronizedXValue,
27
29
  showTooltip,
28
30
  handleTooltipMouseOff
@@ -119,7 +121,7 @@ const WarmingStripes = ({
119
121
  [valueKey, formattedValue, 'left']
120
122
  ]
121
123
 
122
- const dataXPosition = index * stripeWidth + stripeWidth / 2 + Number(config.yAxis.size)
124
+ const dataXPosition = index * stripeWidth + stripeWidth / 2 + yAxisWidth
123
125
  const dataYPosition = mouseY ?? yMax / 2
124
126
 
125
127
  showTooltip({
@@ -132,7 +134,7 @@ const WarmingStripes = ({
132
134
  }
133
135
  })
134
136
  },
135
- [valueKey, xAxisDataKey, stripeWidth, config.yAxis.size, yMax, formatNumber, showTooltip]
137
+ [valueKey, xAxisDataKey, stripeWidth, yAxisWidth, yMax, formatNumber, showTooltip]
136
138
  )
137
139
 
138
140
  // Handle incoming synchronized tooltip from sibling small multiple tiles
@@ -156,7 +158,7 @@ const WarmingStripes = ({
156
158
  }
157
159
 
158
160
  return (
159
- <Group className='warming-stripes' left={config.yAxis.size}>
161
+ <Group className='warming-stripes' left={yAxisWidth}>
160
162
  {displayData.map((item, index) => {
161
163
  const value = Number(item[valueKey])
162
164
  if (isNaN(value)) return null
@@ -1,4 +1,5 @@
1
1
  .warming-stripes-gradient-legend {
2
+ color: var(--baseColor);
2
3
  margin: 1rem 0;
3
4
  width: 100%;
4
5
  }
@@ -27,7 +28,6 @@
27
28
  }
28
29
 
29
30
  .warming-stripes-gradient-legend__series-label {
30
- color: #333;
31
31
  font-size: 14px;
32
32
  font-weight: 500;
33
33
  margin-top: 0.5rem;
@@ -84,12 +84,12 @@ const WarmingStripesGradientLegend = () => {
84
84
  <rect x='1' y='1' width='calc(100% - 2px)' height='23' fill={`url(#${uniqueId})`} />
85
85
 
86
86
  {/* Min label */}
87
- <text x='0' y='40' fontSize='14' textAnchor='start' fill='#333'>
87
+ <text x='0' y='40' fontSize='14' textAnchor='start' fill='currentColor'>
88
88
  {formatNumber(minValue, 'left')}
89
89
  </text>
90
90
 
91
91
  {/* Max label */}
92
- <text x='100%' y='40' fontSize='14' textAnchor='end' fill='#333'>
92
+ <text x='100%' y='40' fontSize='14' textAnchor='end' fill='currentColor'>
93
93
  {formatNumber(maxValue, 'left')}
94
94
  </text>
95
95
  </svg>
@@ -75,6 +75,7 @@ const createInitialState = () => {
75
75
  rightAxisTickColor: '#1c1d1f',
76
76
  numTicks: 4,
77
77
  axisPadding: 0,
78
+ titlePlacement: 'top',
78
79
  scalePadding: 10,
79
80
  tickRotation: 0,
80
81
  anchors: [],
@@ -144,6 +145,7 @@ const createInitialState = () => {
144
145
  sortByRecentDate: false,
145
146
  brushActive: false,
146
147
  brushDefaultRecentDateCount: undefined,
148
+ brushDynamicYAxis: false,
147
149
  viewportNumTicks: {
148
150
  xs: 4,
149
151
  xxs: 4
@@ -156,8 +158,11 @@ const createInitialState = () => {
156
158
  height: '',
157
159
  caption: '',
158
160
  showDownloadUrl: false,
161
+ downloadUrlLabel: '',
159
162
  showDataTableLink: true,
160
163
  showDownloadLinkBelow: true,
164
+ search: false,
165
+ searchPlaceholder: '',
161
166
  indexLabel: '',
162
167
  download: false,
163
168
  showVertical: true,
@@ -305,6 +310,13 @@ const createInitialState = () => {
305
310
  leftLabel: '',
306
311
  rightLabel: ''
307
312
  },
313
+ heatmap: {
314
+ cellPadding: 1,
315
+ rowLabelGap: 32,
316
+ columnLabelGap: 56,
317
+ xAxisPosition: 'top',
318
+ showCellValues: false
319
+ },
308
320
  area: {
309
321
  isStacked: false
310
322
  },
@@ -0,0 +1,8 @@
1
+ import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
2
+ import { ViewportSize } from '../types/ChartConfig'
3
+
4
+ export const AXIS_LABEL_FONT_SIZE = 18
5
+ export const AXIS_LABEL_FONT_SIZE_SMALL = 14
6
+
7
+ export const getAxisLabelFontSize = (vizViewport: ViewportSize) =>
8
+ isMobileFontViewport(vizViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
@@ -0,0 +1,54 @@
1
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
2
+ import { buildTooltipListHtml, getTooltipSeriesMarker } from './tooltipHelpers'
3
+ import { type ChartConfig } from '../types/ChartConfig'
4
+ import { type ColorScale } from '../types/ChartContext'
5
+
6
+ type TooltipDateHelpers = {
7
+ formatDate: (date: Date) => string
8
+ formatTooltipsDate: (date: Date) => string
9
+ parseDate: (dateString: string, showError?: boolean) => Date
10
+ }
11
+
12
+ type BumpTooltipParams = {
13
+ config: ChartConfig
14
+ colorScale?: ColorScale
15
+ dataRow: Record<string, any>
16
+ series: { dataKey: string; name?: string }
17
+ helpers: TooltipDateHelpers
18
+ }
19
+
20
+ const formatXAxisValue = (
21
+ rawValue: unknown,
22
+ config: ChartConfig,
23
+ { formatDate, formatTooltipsDate, parseDate }: TooltipDateHelpers
24
+ ) => {
25
+ if (!isDateScale(config.xAxis) || rawValue === undefined || rawValue === null || rawValue === '') {
26
+ return rawValue
27
+ }
28
+
29
+ const parsedDate = parseDate(String(rawValue), false)
30
+ return config.tooltips.dateDisplayFormat ? formatTooltipsDate(parsedDate) : formatDate(parsedDate)
31
+ }
32
+
33
+ export const buildBumpChartTooltipHtml = ({ config, colorScale, dataRow, series, helpers }: BumpTooltipParams) => {
34
+ const formattedXAxisValue = formatXAxisValue(dataRow[config.xAxis.dataKey], config, helpers)
35
+ const xAxisLabel = config.runtime?.xAxis?.label || config.xAxis.dataKey
36
+ const heading = `${xAxisLabel ? `${xAxisLabel}: ` : ''}${formattedXAxisValue ?? ''}`
37
+
38
+ const seriesName = series.name || config.runtime?.seriesLabels?.[series.dataKey] || series.dataKey
39
+ const marker = getTooltipSeriesMarker(config, colorScale, series.dataKey)
40
+ const bodyRows = [
41
+ {
42
+ text: `${seriesName}: ${dataRow[series.dataKey]}`,
43
+ markerColor: marker?.markerColor,
44
+ markerShape: marker?.markerShape
45
+ },
46
+ ...Object.values(config.columns || {})
47
+ .filter(column => column.tooltips && column.name !== config.xAxis.dataKey && column.name !== series.dataKey)
48
+ .map(column => ({
49
+ text: `${column.label || column.name}: ${dataRow[column.name]}`
50
+ }))
51
+ ]
52
+
53
+ return buildTooltipListHtml({ heading, bodyRows })
54
+ }
@@ -0,0 +1,197 @@
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
+ if (config.yAxis?.titlePlacement === 'top') {
49
+ return 0
50
+ }
51
+
52
+ return !config.hideYAxisLabel && config.runtime.yAxis.label ? axisLabelFontSize + AXIS_LABEL_PADDING : 0
53
+ }
54
+
55
+ const measureWidestLabel = (labels: string[], tickLabelFont: string) => {
56
+ return labels.length ? Math.max(...labels.map(label => getTextWidth(label, tickLabelFont))) : 0
57
+ }
58
+
59
+ const clampYAxisWidth = (calculatedWidth: number, parentWidth: number) => {
60
+ const maxAllowedWidth = parentWidth * MAX_LEFT_AXIS_WIDTH_RATIO
61
+ return Math.max(MIN_LEFT_AXIS_WIDTH, Math.min(Math.ceil(calculatedWidth), maxAllowedWidth))
62
+ }
63
+
64
+ const formatMeasuredTickLabels = ({
65
+ tickValues,
66
+ handleLeftTickFormatting,
67
+ inlineLabel,
68
+ labelsAboveGridlines
69
+ }: {
70
+ tickValues: any[]
71
+ handleLeftTickFormatting: (tick: any, index: number, ticks: any[]) => string
72
+ inlineLabel?: string
73
+ labelsAboveGridlines?: boolean
74
+ }) => {
75
+ const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
76
+
77
+ return tickValues.map((tick, index, ticks) => {
78
+ const formatted = handleLeftTickFormatting(tick, index, ticks)
79
+ const isLastTick = index === ticks.length - 1
80
+
81
+ if (isLastTick && inlineLabel && (labelsAboveGridlines || inlineLabelHasNoSpace)) {
82
+ return `${formatted}${inlineLabel}`
83
+ }
84
+
85
+ return String(formatted ?? '')
86
+ })
87
+ }
88
+
89
+ const getHorizonChartWidth = ({
90
+ config,
91
+ parentWidth,
92
+ tickLabelFont,
93
+ axisLabelFontSize
94
+ }: Pick<CalculateLeftYAxisWidthProps, 'config' | 'parentWidth' | 'tickLabelFont' | 'axisLabelFontSize'>) => {
95
+ const seriesKeys =
96
+ config.runtime?.seriesKeys && config.runtime.seriesKeys.length > 0
97
+ ? config.runtime.seriesKeys
98
+ : config.series?.map(series => series.dataKey) || []
99
+ const labels = seriesKeys.map(seriesKey => String(config.runtime?.seriesLabels?.[seriesKey] || seriesKey))
100
+ const labelWidth = config.yAxis.hideLabel ? 0 : measureWidestLabel(labels, tickLabelFont)
101
+ const calculatedWidth =
102
+ labelWidth +
103
+ DEFAULT_TICK_LENGTH +
104
+ TICK_LABEL_MARGIN_RIGHT +
105
+ getAxisLabelSpace(config, axisLabelFontSize) +
106
+ Number(config.yAxis.axisPadding || 0)
107
+
108
+ return clampYAxisWidth(calculatedWidth, parentWidth)
109
+ }
110
+
111
+ const getHorizontalAxisWidth = ({
112
+ config,
113
+ tickLabelWidth,
114
+ categoryLabelSpace,
115
+ horizontalYAxisLabelSpace
116
+ }: Pick<CalculateLeftYAxisWidthProps, 'config' | 'categoryLabelSpace' | 'horizontalYAxisLabelSpace'> & {
117
+ tickLabelWidth: number
118
+ }) => {
119
+ const horizontalTickWidth = tickLabelWidth + DEFAULT_TICK_LENGTH + TICK_LABEL_MARGIN_RIGHT
120
+ return Math.max(horizontalTickWidth + horizontalYAxisLabelSpace, categoryLabelSpace + horizontalYAxisLabelSpace)
121
+ }
122
+
123
+ const getCategoricalAxisWidth = ({
124
+ config,
125
+ baseWidth,
126
+ tickLabelFont
127
+ }: Pick<CalculateLeftYAxisWidthProps, 'config' | 'tickLabelFont'> & {
128
+ baseWidth: number
129
+ }) => {
130
+ if (config.yAxis.type !== 'categorical' || config.orientation !== 'vertical' || config.yAxis.hideLabel) {
131
+ return baseWidth
132
+ }
133
+
134
+ const categoryLabels = (config.yAxis.categories || []).map(category => String(category?.label || ''))
135
+ const categoryLabelWidth = measureWidestLabel(categoryLabels, tickLabelFont)
136
+
137
+ return Math.max(baseWidth, categoryLabelWidth + DEFAULT_TICK_LENGTH + TICK_LABEL_MARGIN_RIGHT)
138
+ }
139
+
140
+ export const calculateLeftYAxisWidth = ({
141
+ config,
142
+ data,
143
+ yScale,
144
+ numTicks,
145
+ parentWidth,
146
+ tickLabelFont,
147
+ axisLabelFontSize,
148
+ handleLeftTickFormatting,
149
+ categoryLabelSpace = 0,
150
+ horizontalYAxisLabelSpace = 0
151
+ }: CalculateLeftYAxisWidthProps): number => {
152
+ if (config.visualizationType === 'Horizon Chart') {
153
+ return getHorizonChartWidth({ config, parentWidth, tickLabelFont, axisLabelFontSize })
154
+ }
155
+
156
+ const tickValues = getTickValues({ config, data, yScale, numTicks })
157
+ const { hideAxis, inlineLabel, labelsAboveGridlines } = config.yAxis
158
+ const formattedTicks = formatMeasuredTickLabels({
159
+ tickValues,
160
+ handleLeftTickFormatting,
161
+ inlineLabel,
162
+ labelsAboveGridlines
163
+ })
164
+ const tickLabelWidth = config.yAxis.hideLabel ? 0 : measureWidestLabel(formattedTicks, tickLabelFont)
165
+
166
+ const valueOnLinePadding = hideAxis
167
+ ? Math.abs(VALUE_ON_LINE_PADDING_NO_AXIS)
168
+ : Math.abs(VALUE_ON_LINE_PADDING_WITH_AXIS)
169
+ const tickPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT + DEFAULT_TICK_LENGTH
170
+ const verticalAxisLabelSpace = getAxisLabelSpace(config, axisLabelFontSize)
171
+ const reservedTickLabelWidth = labelsAboveGridlines ? 0 : tickLabelWidth
172
+ const reservedTickPadding = labelsAboveGridlines || config.yAxis.hideLabel ? 0 : tickPadding
173
+
174
+ const defaultCalculatedWidth =
175
+ reservedTickLabelWidth + reservedTickPadding + verticalAxisLabelSpace + Number(config.yAxis.axisPadding || 0)
176
+
177
+ if (config.orientation === 'horizontal') {
178
+ const calculatedWidth = getHorizontalAxisWidth({
179
+ config,
180
+ tickLabelWidth,
181
+ categoryLabelSpace,
182
+ horizontalYAxisLabelSpace
183
+ })
184
+
185
+ return clampYAxisWidth(calculatedWidth, parentWidth)
186
+ }
187
+
188
+ const calculatedWidth = getCategoricalAxisWidth({
189
+ config,
190
+ baseWidth: defaultCalculatedWidth,
191
+ tickLabelFont
192
+ })
193
+
194
+ return clampYAxisWidth(calculatedWidth, parentWidth)
195
+ }
196
+
197
+ export default calculateLeftYAxisWidth
@@ -0,0 +1,11 @@
1
+ import { type ChartConfig } from '../types/ChartConfig'
2
+
3
+ export const ensureSpecialChartAxisTypes = <T extends Pick<ChartConfig, 'visualizationType' | 'xAxis'>>(
4
+ config: T
5
+ ): T => {
6
+ if (config.visualizationType === 'Bump Chart' && config.xAxis) {
7
+ config.xAxis.type = 'date-time'
8
+ }
9
+
10
+ return config
11
+ }
@@ -55,9 +55,8 @@ const getMinMax = ({
55
55
  max = enteredMaxValue && isMaxValid ? Number(enteredMaxValue) : Number.MIN_VALUE
56
56
  const { lower, upper } = config?.confidenceKeys || {}
57
57
 
58
- // When brush is active, use tableData (full dataset) for min/max calculations
59
- // so the y-axis shows the full range, but still use filtered data for rendering
60
- const dataForMinMax = config.xAxis.brushActive && tableData && tableData.length > 0 ? tableData : data
58
+ const useBrushFullRange = config.xAxis.brushActive && !config.xAxis.brushDynamicYAxis
59
+ const dataForMinMax = useBrushFullRange && tableData && tableData.length > 0 ? tableData : data
61
60
 
62
61
  if (lower && upper && config.visualizationType === 'Bar') {
63
62
  const buffer = min < 0 ? 1.1 : 0
@@ -1,4 +1,5 @@
1
1
  import { ChartConfig } from '../types/ChartConfig'
2
+ import { YAxisAutoPaddingMode } from './getYAxisAutoPaddingMode'
2
3
 
3
4
  /**
4
5
  * Calculates the Y-axis auto padding to prevent data labels from overlapping with axis tick labels.
@@ -16,7 +17,8 @@ export const getYAxisAutoPadding = (
16
17
  handleNumTicks: number,
17
18
  maxValue: number,
18
19
  minValue: number,
19
- config: ChartConfig
20
+ config: ChartConfig,
21
+ mode: Exclude<YAxisAutoPaddingMode, 'none'>
20
22
  ): number => {
21
23
  // Early returns for cases where auto padding is not needed
22
24
  if (!yScale?.ticks || config.orientation === 'horizontal' || config.yAxis?.max) {
@@ -41,13 +43,13 @@ export const getYAxisAutoPadding = (
41
43
  const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
42
44
  const nextTick = Math.max(...ticks) + tickGap
43
45
  const divideBy = minValue < 0 ? maxValue / 2 : maxValue
44
- const calculatedPadding = (nextTick - maxValue) / divideBy
46
+ let calculatedPadding = (nextTick - maxValue) / divideBy
45
47
 
46
- // if auto padding is too close to next tick, add one more ticks worth of padding
47
- const newPadding =
48
- calculatedPadding > MINIMUM_DISTANCE_PERCENTAGE ? calculatedPadding : calculatedPadding + tickGap / divideBy
48
+ if (mode === 'inline-label' && calculatedPadding <= MINIMUM_DISTANCE_PERCENTAGE) {
49
+ calculatedPadding += tickGap / divideBy
50
+ }
49
51
 
50
52
  /* sometimes even though the padding is getting to the next tick exactly,
51
53
  d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
52
- return newPadding * 100 + 0.1
54
+ return calculatedPadding * 100 + 0.1
53
55
  }
@@ -0,0 +1,17 @@
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+
3
+ export type YAxisAutoPaddingMode = 'none' | 'inline-label' | 'top-title'
4
+
5
+ export const getYAxisAutoPaddingMode = (config: ChartConfig): YAxisAutoPaddingMode => {
6
+ const inlineLabel = config.yAxis?.inlineLabel
7
+ if (inlineLabel && inlineLabel.includes(' ')) {
8
+ return 'inline-label'
9
+ }
10
+
11
+ const yAxisTitle = String(config.yAxis?.label || config.runtime?.yAxis?.label || '').trim()
12
+ if (config.yAxis?.titlePlacement === 'top' && yAxisTitle && !config.hideYAxisLabel) {
13
+ return 'top-title'
14
+ }
15
+
16
+ return 'none'
17
+ }
@@ -1,17 +1,30 @@
1
- export const handleChartAriaLabels = state => {
1
+ import type { AltTextConfig } from '@cdc/core/types/AltText'
2
+ import { resolveAltTextDescription } from '@cdc/core/helpers/resolveAltTextDescription'
3
+
4
+ const getAutoLabel = (state: { visualizationType?: string; title?: string }): string => {
5
+ let ariaLabel = ''
6
+ if (state.visualizationType) {
7
+ ariaLabel += `${state.visualizationType} chart`
8
+ }
9
+ if (state.title && state.visualizationType) {
10
+ ariaLabel += ` with the title: ${state.title}`
11
+ }
12
+ return ariaLabel
13
+ }
14
+
15
+ export const handleChartAriaLabels = (state: {
16
+ visualizationType?: string
17
+ title?: string
18
+ altText?: AltTextConfig
19
+ dataMetadata?: Record<string, string>
20
+ }): string => {
2
21
  try {
3
22
  if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
4
- let ariaLabel = ''
5
-
6
- if (state.visualizationType) {
7
- ariaLabel += `${state.visualizationType} chart`
8
- }
9
23
 
10
- if (state.title && state.visualizationType) {
11
- ariaLabel += ` with the title: ${state.title}`
12
- }
24
+ const title = getAutoLabel(state)
25
+ const description = resolveAltTextDescription(state.altText, state.dataMetadata)
13
26
 
14
- return ariaLabel
27
+ return description ? `${title}. ${description}` : title
15
28
  } catch (e) {
16
29
  console.error('COVE: ', e.message) // eslint-disable-line
17
30
  return 'Data visualization container'