@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.
- package/CONFIG.md +349 -29
- package/LICENSE +201 -0
- package/dist/cdcchart.js +34247 -32805
- 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/bump-chart.json +1116 -0
- package/examples/private/test-123.json +624 -0
- package/package.json +4 -3
- package/src/CdcChartComponent.tsx +135 -54
- 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.SmallMultiples.smoke.stories.tsx +29 -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/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.tsx +1 -1
- package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
- package/src/components/Axis/LeftAxis.tsx +14 -11
- package/src/components/Axis/LeftAxisGridlines.tsx +14 -11
- 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 +310 -49
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +13 -0
- 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.Visual.tsx +1 -1
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +42 -22
- package/src/components/HeatMap/components/HeatMap.tsx +762 -0
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
- package/src/components/HeatMap/heatmap.constants.ts +13 -0
- package/src/components/HeatMap/heatmap.css +71 -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 +1040 -0
- package/src/components/HeatMap/tests/helpers.test.ts +167 -0
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
- package/src/components/LineChart/helpers.test.ts +345 -0
- package/src/components/LineChart/helpers.ts +15 -5
- package/src/components/LineChart/index.tsx +7 -1
- package/src/components/LinearChart/tests/LinearChart.test.tsx +107 -33
- package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
- package/src/components/LinearChart.tsx +58 -37
- 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/SmallMultiples/SmallMultipleTile.tsx +30 -3
- package/src/components/SmallMultiples/SmallMultiples.css +12 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -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 +11 -0
- package/src/helpers/axisLabelFontSize.ts +8 -0
- package/src/helpers/bumpChartTooltip.ts +54 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +4 -0
- package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
- package/src/helpers/getMinMax.ts +2 -3
- package/src/helpers/getYAxisAutoPadding.ts +8 -6
- package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
- package/src/helpers/handleChartAriaLabels.ts +23 -10
- package/src/helpers/seriesHighlight.ts +26 -0
- package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
- package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
- package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
- package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
- package/src/helpers/tests/seriesHighlight.test.ts +61 -0
- package/src/hooks/tests/useScales.test.tsx +105 -0
- package/src/hooks/useScales.ts +19 -9
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +32 -49
- package/src/types/ChartConfig.ts +18 -0
- 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='
|
|
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,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
|
+
}
|
package/src/helpers/getMinMax.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
46
|
+
let calculatedPadding = (nextTick - maxValue) / divideBy
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
24
|
+
const title = getAutoLabel(state)
|
|
25
|
+
const description = resolveAltTextDescription(state.altText, state.dataMetadata)
|
|
13
26
|
|
|
14
|
-
return
|
|
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
|
+
})
|