@cdc/chart 4.26.4 → 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 (93) hide show
  1. package/CONFIG.md +349 -29
  2. package/LICENSE +201 -0
  3. package/dist/cdcchart.js +34247 -32805
  4. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  5. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  6. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  7. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  8. package/examples/private/bump-chart.json +1116 -0
  9. package/examples/private/test-123.json +624 -0
  10. package/package.json +4 -3
  11. package/src/CdcChartComponent.tsx +135 -54
  12. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  13. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  14. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  15. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  16. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  17. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  18. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  19. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  20. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  21. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  22. package/src/_stories/_mock/heatmap.json +156 -0
  23. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  24. package/src/_stories/_mock/sankey_mock.json +78 -0
  25. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  26. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
  27. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  28. package/src/components/Axis/LeftAxis.tsx +14 -11
  29. package/src/components/Axis/LeftAxisGridlines.tsx +14 -11
  30. package/src/components/Brush/BrushSelector.tsx +88 -187
  31. package/src/components/Brush/brushInteraction.ts +22 -0
  32. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  33. package/src/components/EditorPanel/EditorPanel.tsx +310 -49
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +13 -0
  35. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  37. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  38. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  39. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  40. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  41. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  42. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  43. package/src/components/EditorPanel/useEditorPermissions.ts +42 -22
  44. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  45. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  46. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  47. package/src/components/HeatMap/heatmap.css +71 -0
  48. package/src/components/HeatMap/helpers/index.ts +261 -0
  49. package/src/components/HeatMap/index.tsx +5 -0
  50. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  51. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  52. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  53. package/src/components/LineChart/helpers.test.ts +345 -0
  54. package/src/components/LineChart/helpers.ts +15 -5
  55. package/src/components/LineChart/index.tsx +7 -1
  56. package/src/components/LinearChart/tests/LinearChart.test.tsx +107 -33
  57. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  58. package/src/components/LinearChart.tsx +58 -37
  59. package/src/components/PieChart/PieChart.tsx +4 -1
  60. package/src/components/RadarChart/RadarChart.tsx +3 -1
  61. package/src/components/Regions/components/Regions.tsx +195 -41
  62. package/src/components/Sankey/components/Sankey.tsx +7 -3
  63. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  64. package/src/components/SmallMultiples/SmallMultipleTile.tsx +30 -3
  65. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  66. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  67. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  68. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  69. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  70. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  71. package/src/data/initial-state.js +11 -0
  72. package/src/helpers/axisLabelFontSize.ts +8 -0
  73. package/src/helpers/bumpChartTooltip.ts +54 -0
  74. package/src/helpers/calculateLeftYAxisWidth.ts +4 -0
  75. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  76. package/src/helpers/getMinMax.ts +2 -3
  77. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  78. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  79. package/src/helpers/handleChartAriaLabels.ts +23 -10
  80. package/src/helpers/seriesHighlight.ts +26 -0
  81. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  82. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  83. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  84. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  85. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  86. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  87. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  88. package/src/hooks/tests/useScales.test.tsx +105 -0
  89. package/src/hooks/useScales.ts +19 -9
  90. package/src/index.jsx +5 -2
  91. package/src/scss/main.scss +32 -49
  92. package/src/types/ChartConfig.ts +18 -0
  93. package/src/types/ChartContext.ts +2 -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,6 +28,9 @@ 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
 
@@ -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) => (
@@ -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
@@ -159,6 +161,8 @@ const createInitialState = () => {
159
161
  downloadUrlLabel: '',
160
162
  showDataTableLink: true,
161
163
  showDownloadLinkBelow: true,
164
+ search: false,
165
+ searchPlaceholder: '',
162
166
  indexLabel: '',
163
167
  download: false,
164
168
  showVertical: true,
@@ -306,6 +310,13 @@ const createInitialState = () => {
306
310
  leftLabel: '',
307
311
  rightLabel: ''
308
312
  },
313
+ heatmap: {
314
+ cellPadding: 1,
315
+ rowLabelGap: 32,
316
+ columnLabelGap: 56,
317
+ xAxisPosition: 'top',
318
+ showCellValues: false
319
+ },
309
320
  area: {
310
321
  isStacked: false
311
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
+ }
@@ -45,6 +45,10 @@ const getTickValues = ({ config, data, yScale, numTicks }: TickValueProps) => {
45
45
  }
46
46
 
47
47
  const getAxisLabelSpace = (config: any, axisLabelFontSize: number) => {
48
+ if (config.yAxis?.titlePlacement === 'top') {
49
+ return 0
50
+ }
51
+
48
52
  return !config.hideYAxisLabel && config.runtime.yAxis.label ? axisLabelFontSize + AXIS_LABEL_PADDING : 0
49
53
  }
50
54
 
@@ -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'
@@ -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
+ })