@cdc/chart 4.26.4 → 4.26.6

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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -1,8 +1,9 @@
1
- import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'
1
+ import React, { useContext, useRef, useState, useMemo, useCallback, useLayoutEffect } from 'react'
2
2
  import SmallMultipleTile from './SmallMultipleTile'
3
3
  import ConfigContext from '../../ConfigContext'
4
4
  import useReduceData from '../../hooks/useReduceData'
5
5
  import useScales from '../../hooks/useScales'
6
+ import { hasSpacedInlineLabel } from '../../helpers/hasSpacedInlineLabel'
6
7
  import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
7
8
  import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
8
9
  import './SmallMultiples.css'
@@ -11,6 +12,8 @@ import { ChartConfig } from '../../types/ChartConfig'
11
12
  interface SmallMultiplesProps {
12
13
  config: ChartConfig
13
14
  data: object[]
15
+ yAxisDomainData?: object[]
16
+ hasBoundarySuppression?: boolean
14
17
  svgRef?: React.RefObject<SVGAElement>
15
18
  parentWidth?: number
16
19
  parentHeight?: number
@@ -33,12 +36,21 @@ type TileHeaderRows = Array<Array<HTMLDivElement>>
33
36
 
34
37
  type TileHeaderEntries = Array<[string, HTMLDivElement]>
35
38
 
36
- const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, parentWidth, parentHeight }) => {
39
+ const SmallMultiples: React.FC<SmallMultiplesProps> = ({
40
+ config,
41
+ data,
42
+ yAxisDomainData,
43
+ hasBoundarySuppression = false,
44
+ svgRef,
45
+ parentWidth,
46
+ parentHeight
47
+ }) => {
37
48
  const { currentViewport, colorScale, parentRef } = useContext(ConfigContext)
38
49
  const { mode, tileColumn, tilesPerRowDesktop, tilesPerRowMobile } = config.smallMultiples || {}
39
50
 
40
51
  const isMobile = isMobileSmallMultiplesViewport(currentViewport)
41
52
  const tilesPerRow = isMobile ? tilesPerRowMobile || 1 : tilesPerRowDesktop || 3
53
+ const dataForSharedYAxis = Array.isArray(yAxisDomainData) && yAxisDomainData.length > 0 ? yAxisDomainData : data
42
54
 
43
55
  // Figure out what objects to iterate over based on mode - memoized to prevent recalculation
44
56
  const tileItems = useMemo<Array<TileItem>>(() => {
@@ -79,6 +91,22 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
79
91
  config.smallMultiples?.tileTitles
80
92
  ])
81
93
 
94
+ const sharedYAxisTileItems = useMemo<Array<TileItem>>(
95
+ () =>
96
+ mode === 'by-column'
97
+ ? Array.from(new Set(dataForSharedYAxis.map(row => row[tileColumn])))
98
+ .filter(val => val != null)
99
+ .sort()
100
+ .map(value => ({
101
+ key: value,
102
+ mode: 'by-column' as const,
103
+ tileValue: value,
104
+ tileColumn: tileColumn
105
+ }))
106
+ : tileItems,
107
+ [mode, dataForSharedYAxis, tileColumn, tileItems]
108
+ )
109
+
82
110
  // Calculate the grid styling based on tiles per row
83
111
  const gridGap = isMobile ? '1rem' : '2rem'
84
112
  const gridStyle = {
@@ -96,8 +124,8 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
96
124
 
97
125
  // Create combined data and config for consistent Y-axis calculation
98
126
  const combinedDataForYAxis = useMemo(
99
- () => createCombinedDataForYAxis(config, data, tileItems),
100
- [config, data, tileItems]
127
+ () => createCombinedDataForYAxis(config, dataForSharedYAxis, sharedYAxisTileItems),
128
+ [config, dataForSharedYAxis, sharedYAxisTileItems]
101
129
  )
102
130
 
103
131
  const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(
@@ -105,14 +133,13 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
105
133
  combinedDataForYAxis.data
106
134
  )
107
135
 
108
- const inlineLabel = config.yAxis?.inlineLabel
109
- const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
110
- const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
136
+ const usesSpacedInlineLabel = hasSpacedInlineLabel(config)
111
137
 
112
- const { min, max } = useScales({
138
+ const { min, max, yTickValues } = useScales({
113
139
  config: combinedDataForYAxis.config,
114
140
  data: combinedDataForYAxis.data,
115
141
  tableData: combinedDataForYAxis.data,
142
+ hasBoundarySuppression,
116
143
  minValue,
117
144
  maxValue,
118
145
  existPositiveValue,
@@ -120,7 +147,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
120
147
  xAxisDataMapped: [],
121
148
  xMax: parentWidth,
122
149
  yMax: parentHeight,
123
- needsYAxisAutoPadding,
150
+ hasSpacedInlineLabel: usesSpacedInlineLabel,
124
151
  currentViewport
125
152
  })
126
153
 
@@ -130,8 +157,8 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
130
157
  if (typeof min !== 'number' || typeof max !== 'number') return null
131
158
  if (combinedDataForYAxis.data.length === 0) return null
132
159
 
133
- return { min, max }
134
- }, [config.smallMultiples?.independentYAxis, min, max, combinedDataForYAxis.data.length])
160
+ return { min, max, tickValues: yTickValues }
161
+ }, [config.smallMultiples?.independentYAxis, min, max, yTickValues, combinedDataForYAxis.data.length])
135
162
 
136
163
  const numberOfRows = useMemo(() => Math.ceil(tileItems.length / tilesPerRow), [tileItems.length, tilesPerRow])
137
164
 
@@ -168,7 +195,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
168
195
  )
169
196
 
170
197
  // Align tile header heights per row
171
- useEffect(() => {
198
+ useLayoutEffect(() => {
172
199
  const headerEntries = Object.entries(headerRefs.current).filter(([_, ref]) => ref) as TileHeaderEntries
173
200
  if (headerEntries.length === 0) return
174
201
 
@@ -209,7 +236,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
209
236
  }, [tileItems, tilesPerRow])
210
237
 
211
238
  // Calculate container height from measured tile heights
212
- useEffect(() => {
239
+ useLayoutEffect(() => {
213
240
  if (!parentRef.current) return
214
241
 
215
242
  const measuredHeights = Object.values(tileHeights)
@@ -223,14 +250,21 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
223
250
  const totalHeight = numberOfRows * maxTileHeight + totalGapsHeight
224
251
 
225
252
  parentRef.current.style.height = `${totalHeight}px`
226
- }, [tileHeights, numberOfRows, isMobile, parentRef, tilesPerRow])
253
+ }, [tileHeights, numberOfRows, isMobile])
227
254
 
228
255
  if (tileItems.length === 0) {
229
256
  return null
230
257
  }
231
258
 
259
+ const hasTopYAxisTitle =
260
+ config.yAxis?.titlePlacement === 'top' && !config.hideYAxisLabel && Boolean(config.runtime?.yAxis?.label)
261
+
232
262
  return (
233
- <div className='small-multiples-container'>
263
+ <div
264
+ className={`small-multiples-container${
265
+ hasTopYAxisTitle ? ' small-multiples-container--with-top-y-axis-title' : ''
266
+ }`}
267
+ >
234
268
  <div className='small-multiples-grid' style={gridStyle}>
235
269
  {tileItems.map((item, index) => {
236
270
  const customColorScale = createTileColorScale(item, config, colorScale, index, tileItems.length)
@@ -251,6 +285,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
251
285
  parentHeight={parentHeight}
252
286
  globalYAxisMax={globalYAxisValues?.max}
253
287
  globalYAxisMin={globalYAxisValues?.min}
288
+ globalYAxisTickValues={globalYAxisValues?.tickValues}
254
289
  isFirstInRow={index % tilesPerRow === 0}
255
290
  onHeightChange={handleTileHeightChange}
256
291
  onChartRef={ref => {
@@ -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
+ })
@@ -0,0 +1,170 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import ConfigContext from '../../../ConfigContext'
5
+ import SmallMultiples from '../SmallMultiples'
6
+ import { createMockChartContext, createMockConfig } from '../../LinearChart/tests/mockConfigContext'
7
+
8
+ const tileProps = vi.hoisted(() => [] as any[])
9
+ const mockUseReduceData = vi.hoisted(() => vi.fn())
10
+ const mockUseScales = vi.hoisted(() => vi.fn())
11
+
12
+ vi.mock('../SmallMultipleTile', () => ({
13
+ default: props => {
14
+ tileProps.push(props)
15
+ return <div data-testid={`tile-${props.tileKey}`} />
16
+ }
17
+ }))
18
+
19
+ vi.mock('../../../hooks/useReduceData', () => ({
20
+ default: (...args: any[]) => mockUseReduceData(...args)
21
+ }))
22
+
23
+ vi.mock('../../../hooks/useScales', () => ({
24
+ default: (...args: any[]) => mockUseScales(...args)
25
+ }))
26
+
27
+ describe('SmallMultiples', () => {
28
+ const createSmallMultiplesConfig = () =>
29
+ createMockConfig({
30
+ smallMultiples: {
31
+ mode: 'by-column',
32
+ tileColumn: 'Sex',
33
+ independentYAxis: false,
34
+ tilesPerRowDesktop: 2,
35
+ tilesPerRowMobile: 1
36
+ },
37
+ series: [{ dataKey: 'Cases', type: 'Bar' }] as any,
38
+ runtime: {
39
+ ...createMockConfig().runtime,
40
+ series: [{ dataKey: 'Cases', type: 'Bar' }],
41
+ seriesKeys: ['Cases'],
42
+ seriesLabelsAll: ['Cases']
43
+ } as any
44
+ })
45
+
46
+ beforeEach(() => {
47
+ tileProps.length = 0
48
+ mockUseReduceData.mockReset()
49
+ mockUseScales.mockReset()
50
+ mockUseReduceData.mockReturnValue({
51
+ minValue: 0,
52
+ maxValue: 100,
53
+ existPositiveValue: true,
54
+ isAllLine: false
55
+ })
56
+ mockUseScales.mockReturnValue({
57
+ min: 0,
58
+ max: 100,
59
+ yTickValues: [0, 50, 100]
60
+ })
61
+ })
62
+
63
+ it('uses stable domain data for the shared Y-axis while rendering filtered tile data', () => {
64
+ const filteredRows = [
65
+ { Sex: 'Male', Year: '2019', Cases: 10 },
66
+ { Sex: 'Female', Year: '2019', Cases: 20 }
67
+ ]
68
+ const stableDomainRows = [
69
+ ...filteredRows,
70
+ { Sex: 'Male', Year: '2020', Cases: 100 },
71
+ { Sex: 'Female', Year: '2020', Cases: 80 }
72
+ ]
73
+ const config = createSmallMultiplesConfig()
74
+ const context = createMockChartContext(config, {
75
+ colorScale: vi.fn(),
76
+ parentRef: { current: document.createElement('div') }
77
+ } as any)
78
+
79
+ render(
80
+ <ConfigContext.Provider value={context}>
81
+ <SmallMultiples
82
+ config={config}
83
+ data={filteredRows}
84
+ yAxisDomainData={stableDomainRows}
85
+ parentWidth={600}
86
+ parentHeight={300}
87
+ />
88
+ </ConfigContext.Provider>
89
+ )
90
+
91
+ const expectedSharedAxisRows = [
92
+ { Sex: 'Female', Year: '2019', Cases: 20 },
93
+ { Sex: 'Female', Year: '2020', Cases: 80 },
94
+ { Sex: 'Male', Year: '2019', Cases: 10 },
95
+ { Sex: 'Male', Year: '2020', Cases: 100 }
96
+ ]
97
+
98
+ expect(mockUseReduceData.mock.calls[0][1]).toEqual(expectedSharedAxisRows)
99
+ expect(mockUseScales.mock.calls[0][0].data).toEqual(expectedSharedAxisRows)
100
+ expect(mockUseScales.mock.calls[0][0].hasBoundarySuppression).toBe(false)
101
+ expect(tileProps).toHaveLength(2)
102
+ expect(tileProps[0].data).toBe(filteredRows)
103
+ expect(tileProps[0].globalYAxisMax).toBe(100)
104
+ expect(tileProps[0].globalYAxisTickValues).toEqual([0, 50, 100])
105
+ })
106
+
107
+ it('includes filtered-out tile values when calculating a stable shared Y-axis', () => {
108
+ const filteredRows = [{ Sex: 'Male', Year: '2019', Cases: 10 }]
109
+ const stableDomainRows = [...filteredRows, { Sex: 'Female', Year: '2019', Cases: 200 }]
110
+ const config = createSmallMultiplesConfig()
111
+ const context = createMockChartContext(config, {
112
+ colorScale: vi.fn(),
113
+ parentRef: { current: document.createElement('div') }
114
+ } as any)
115
+
116
+ render(
117
+ <ConfigContext.Provider value={context}>
118
+ <SmallMultiples
119
+ config={config}
120
+ data={filteredRows}
121
+ yAxisDomainData={stableDomainRows}
122
+ parentWidth={600}
123
+ parentHeight={300}
124
+ />
125
+ </ConfigContext.Provider>
126
+ )
127
+
128
+ expect(mockUseReduceData.mock.calls[0][1]).toEqual([
129
+ { Sex: 'Female', Year: '2019', Cases: 200 },
130
+ { Sex: 'Male', Year: '2019', Cases: 10 }
131
+ ])
132
+ expect(mockUseScales.mock.calls[0][0].data).toEqual([
133
+ { Sex: 'Female', Year: '2019', Cases: 200 },
134
+ { Sex: 'Male', Year: '2019', Cases: 10 }
135
+ ])
136
+ expect(mockUseScales.mock.calls[0][0].hasBoundarySuppression).toBe(false)
137
+ expect(tileProps).toHaveLength(1)
138
+ expect(tileProps[0].tileValue).toBe('Male')
139
+ expect(tileProps[0].data).toBe(filteredRows)
140
+ })
141
+
142
+ it('passes hasBoundarySuppression from shared stable-domain boundary rows', () => {
143
+ const filteredRows = [{ Sex: 'Male', Year: '2019', Cases: 10 }]
144
+ const stableDomainRows = [
145
+ { Sex: 'Female', Year: '2018', Cases: 'ABC' },
146
+ { Sex: 'Male', Year: '2019', Cases: 10 }
147
+ ]
148
+ const config = createSmallMultiplesConfig()
149
+ config.preliminaryData = [{ type: 'suppression', style: 'Dashed Small', column: '', value: 'ABC' }] as any
150
+ const context = createMockChartContext(config, {
151
+ colorScale: vi.fn(),
152
+ parentRef: { current: document.createElement('div') }
153
+ } as any)
154
+
155
+ render(
156
+ <ConfigContext.Provider value={context}>
157
+ <SmallMultiples
158
+ config={config}
159
+ data={filteredRows}
160
+ yAxisDomainData={stableDomainRows as any}
161
+ hasBoundarySuppression={true}
162
+ parentWidth={600}
163
+ parentHeight={300}
164
+ />
165
+ </ConfigContext.Provider>
166
+ )
167
+
168
+ expect(mockUseScales.mock.calls[0][0].hasBoundarySuppression).toBe(true)
169
+ })
170
+ })
@@ -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,8 +75,10 @@ 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,
81
+ autoMaxStrategy: 'clean-top-tick',
80
82
  anchors: [],
81
83
  shoMissingDataLabel: true,
82
84
  showMissingDataLine: true,
@@ -144,6 +146,7 @@ const createInitialState = () => {
144
146
  sortByRecentDate: false,
145
147
  brushActive: false,
146
148
  brushDefaultRecentDateCount: undefined,
149
+ brushDynamicYAxis: false,
147
150
  viewportNumTicks: {
148
151
  xs: 4,
149
152
  xxs: 4
@@ -159,6 +162,8 @@ const createInitialState = () => {
159
162
  downloadUrlLabel: '',
160
163
  showDataTableLink: true,
161
164
  showDownloadLinkBelow: true,
165
+ search: false,
166
+ searchPlaceholder: '',
162
167
  indexLabel: '',
163
168
  download: false,
164
169
  showVertical: true,
@@ -306,6 +311,13 @@ const createInitialState = () => {
306
311
  leftLabel: '',
307
312
  rightLabel: ''
308
313
  },
314
+ heatmap: {
315
+ cellPadding: 1,
316
+ rowLabelGap: 32,
317
+ columnLabelGap: 56,
318
+ xAxisPosition: 'top',
319
+ showCellValues: false
320
+ },
309
321
  area: {
310
322
  isStacked: false
311
323
  },
@@ -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
+ }