@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.
- package/CONFIG.md +353 -29
- package/LICENSE +201 -0
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +37885 -35117
- package/examples/feature/heatmap/average-age-categorical.json +313 -0
- package/examples/feature/heatmap/calendar-heatmap.json +134 -0
- package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
- package/examples/feature/heatmap/sparse-aggregation.json +158 -0
- package/examples/private/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/examples/private/bump-chart.json +1116 -0
- package/examples/private/test-123.json +624 -0
- package/package.json +4 -3
- package/src/CdcChart.tsx +5 -4
- package/src/CdcChartComponent.tsx +166 -62
- package/src/_stories/Chart.AltText.stories.tsx +136 -0
- package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
- package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
- package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
- package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
- package/src/_stories/_mock/alt_text_metadata.json +38 -0
- package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
- package/src/_stories/_mock/heatmap.json +156 -0
- package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
- package/src/_stories/_mock/sankey_mock.json +78 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
- package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +17 -11
- package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- package/src/components/Brush/BrushSelector.tsx +88 -187
- package/src/components/Brush/brushInteraction.ts +22 -0
- package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
- package/src/components/EditorPanel/EditorPanel.tsx +677 -185
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
- package/src/components/HeatMap/components/HeatMap.tsx +767 -0
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
- package/src/components/HeatMap/heatmap.constants.ts +13 -0
- package/src/components/HeatMap/heatmap.css +85 -0
- package/src/components/HeatMap/helpers/index.ts +261 -0
- package/src/components/HeatMap/index.tsx +5 -0
- package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
- package/src/components/HeatMap/tests/helpers.test.ts +167 -0
- package/src/components/Legend/Legend.Component.tsx +2 -2
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
- package/src/components/LineChart/helpers.test.ts +574 -0
- package/src/components/LineChart/helpers.ts +39 -16
- package/src/components/LineChart/index.tsx +28 -5
- package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
- package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
- package/src/components/LinearChart.tsx +103 -47
- package/src/components/PieChart/PieChart.tsx +4 -1
- package/src/components/RadarChart/RadarChart.tsx +3 -1
- package/src/components/Regions/components/Regions.tsx +195 -41
- package/src/components/Sankey/components/Sankey.tsx +7 -3
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
- package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
- package/src/components/SmallMultiples/SmallMultiples.css +12 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
- package/src/components/Sparkline/components/SparkLine.tsx +4 -9
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
- package/src/data/initial-state.js +12 -0
- package/src/helpers/axisLabelFontSize.ts +8 -0
- package/src/helpers/bumpChartTooltip.ts +54 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getHasBoundarySuppression.ts +26 -0
- package/src/helpers/getMinMax.ts +26 -25
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/handleChartAriaLabels.ts +23 -10
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/seriesColumnSettings.ts +5 -4
- package/src/helpers/seriesHighlight.ts +26 -0
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
- package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
- package/src/helpers/tests/seriesHighlight.test.ts +61 -0
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +127 -0
- package/src/hooks/tests/useScales.test.tsx +383 -0
- package/src/hooks/useRightAxis.ts +30 -13
- package/src/hooks/useScales.ts +48 -21
- package/src/hooks/useTooltip.tsx +3 -1
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +32 -49
- package/src/test/CdcChart.dataTable.test.tsx +122 -0
- package/src/types/ChartConfig.ts +24 -1
- package/src/types/ChartContext.ts +5 -0
- package/src/helpers/getYAxisAutoPadding.ts +0 -53
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useContext,
|
|
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> = ({
|
|
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,
|
|
100
|
-
[config,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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='
|
|
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='
|
|
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
|
+
}
|