@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,9 +1,11 @@
|
|
|
1
1
|
import React, { useContext } from 'react'
|
|
2
2
|
import ConfigContext from '../../../ConfigContext'
|
|
3
3
|
import { ChartContext } from '../../../types/ChartContext'
|
|
4
|
-
import { Text } from '@visx/text'
|
|
4
|
+
import { Text, useText } from '@visx/text'
|
|
5
5
|
import { Group } from '@visx/group'
|
|
6
|
-
import { formatDate
|
|
6
|
+
import { formatDate } from '@cdc/core/helpers/cove/date.js'
|
|
7
|
+
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
8
|
+
import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
7
9
|
|
|
8
10
|
// Constants for visualization types
|
|
9
11
|
const VIZ_TYPES = {
|
|
@@ -14,6 +16,16 @@ const VIZ_TYPES = {
|
|
|
14
16
|
HORIZON: 'Horizon Chart'
|
|
15
17
|
} as const
|
|
16
18
|
|
|
19
|
+
const DEFAULT_REGION_BACKGROUND = 'var(--cool-gray-50, #71767a)'
|
|
20
|
+
const TICK_LABEL_FONT_SIZE = 16
|
|
21
|
+
const TICK_LABEL_FONT_SIZE_SMALL = 13
|
|
22
|
+
const REGION_LABEL_HORIZONTAL_PADDING = 8
|
|
23
|
+
const REGION_LABEL_TOP_PADDING = 6
|
|
24
|
+
const REGION_LABEL_MAX_PLOT_WIDTH_RATIO = 1 / 3
|
|
25
|
+
const REGION_LABEL_COMFORTABLE_MIN_WIDTH = 120
|
|
26
|
+
const REGION_LABEL_COMFORTABLE_MIN_WIDTH_SMALL = 96
|
|
27
|
+
const REGION_LABEL_FONT_FAMILY = 'Nunito, sans-serif'
|
|
28
|
+
|
|
17
29
|
type Region = {
|
|
18
30
|
from: string
|
|
19
31
|
to: string
|
|
@@ -36,7 +48,6 @@ type RegionsProps = {
|
|
|
36
48
|
barWidth?: number
|
|
37
49
|
totalBarsInGroup?: number
|
|
38
50
|
xMax?: number
|
|
39
|
-
yAxisWidth?: number
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
type HighlightedAreaProps = {
|
|
@@ -46,10 +57,111 @@ type HighlightedAreaProps = {
|
|
|
46
57
|
background: string
|
|
47
58
|
}
|
|
48
59
|
|
|
60
|
+
type RegionLabelLayout = {
|
|
61
|
+
x: number
|
|
62
|
+
width: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type RegionLabelProps = {
|
|
66
|
+
label: string
|
|
67
|
+
color: string
|
|
68
|
+
clippedFrom: number
|
|
69
|
+
regionWidth: number
|
|
70
|
+
plotWidth: number
|
|
71
|
+
fontSize: number
|
|
72
|
+
isMobileViewport: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
const HighlightedArea: React.FC<HighlightedAreaProps> = ({ x, width, yMax, background }) => (
|
|
50
|
-
<rect x={x} y={0} width={width} height={yMax} fill={background} opacity={0.3} />
|
|
76
|
+
<rect x={x} y={0} width={width} height={yMax} fill={background || DEFAULT_REGION_BACKGROUND} opacity={0.3} />
|
|
51
77
|
)
|
|
52
78
|
|
|
79
|
+
const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max)
|
|
80
|
+
|
|
81
|
+
const getRegionLabelY = (fontSize: number): number => REGION_LABEL_TOP_PADDING + fontSize * 0.25
|
|
82
|
+
|
|
83
|
+
const getComfortableRegionLabelWidth = (plotWidth: number, isMobileViewport: boolean): number => {
|
|
84
|
+
const maxPlotLabelWidth = Math.max(1, plotWidth * REGION_LABEL_MAX_PLOT_WIDTH_RATIO)
|
|
85
|
+
const comfortableMinWidth = isMobileViewport
|
|
86
|
+
? REGION_LABEL_COMFORTABLE_MIN_WIDTH_SMALL
|
|
87
|
+
: REGION_LABEL_COMFORTABLE_MIN_WIDTH
|
|
88
|
+
return Math.min(comfortableMinWidth, maxPlotLabelWidth, plotWidth)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Region labels prefer to sit centered inside their highlighted region when the measured text
|
|
92
|
+
// fits with padding. If the visible region is too narrow, keep a readable fixed wrapping width
|
|
93
|
+
// and pin the measured text near the region's right edge, clamped so the text stays inside the plot.
|
|
94
|
+
const getRegionLabelLayout = (
|
|
95
|
+
clippedFrom: number,
|
|
96
|
+
regionWidth: number,
|
|
97
|
+
plotWidth: number,
|
|
98
|
+
comfortableLabelWidth: number,
|
|
99
|
+
estimatedWrappedWidth: number
|
|
100
|
+
): RegionLabelLayout => {
|
|
101
|
+
const maxPlotLabelWidth = Math.max(1, plotWidth * REGION_LABEL_MAX_PLOT_WIDTH_RATIO)
|
|
102
|
+
const regionInnerWidth = Math.max(0, regionWidth - REGION_LABEL_HORIZONTAL_PADDING * 2)
|
|
103
|
+
const insideThreshold = Math.min(comfortableLabelWidth, estimatedWrappedWidth || comfortableLabelWidth)
|
|
104
|
+
|
|
105
|
+
const isInsideRegion = regionInnerWidth >= insideThreshold
|
|
106
|
+
const labelWidth = isInsideRegion ? Math.min(regionInnerWidth, maxPlotLabelWidth) : comfortableLabelWidth
|
|
107
|
+
|
|
108
|
+
const regionCenter = clippedFrom + regionWidth / 2
|
|
109
|
+
const halfLabelWidth = labelWidth / 2
|
|
110
|
+
const measuredTextWidth = Math.min(estimatedWrappedWidth || labelWidth, labelWidth)
|
|
111
|
+
const halfMeasuredTextWidth = measuredTextWidth / 2
|
|
112
|
+
const overflowLabelX = clamp(
|
|
113
|
+
clippedFrom + regionWidth - REGION_LABEL_HORIZONTAL_PADDING - halfMeasuredTextWidth,
|
|
114
|
+
halfMeasuredTextWidth,
|
|
115
|
+
plotWidth - halfMeasuredTextWidth
|
|
116
|
+
)
|
|
117
|
+
const insideLabelX =
|
|
118
|
+
labelWidth >= plotWidth ? plotWidth / 2 : clamp(regionCenter, halfLabelWidth, plotWidth - halfLabelWidth)
|
|
119
|
+
|
|
120
|
+
return { x: isInsideRegion ? insideLabelX : overflowLabelX, width: labelWidth }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const RegionLabel: React.FC<RegionLabelProps> = ({
|
|
124
|
+
label,
|
|
125
|
+
color,
|
|
126
|
+
clippedFrom,
|
|
127
|
+
regionWidth,
|
|
128
|
+
plotWidth,
|
|
129
|
+
fontSize,
|
|
130
|
+
isMobileViewport
|
|
131
|
+
}) => {
|
|
132
|
+
const style = { fontFamily: REGION_LABEL_FONT_FAMILY, fontSize }
|
|
133
|
+
const comfortableLabelWidth = getComfortableRegionLabelWidth(plotWidth, isMobileViewport)
|
|
134
|
+
const { wordsByLines } = useText({
|
|
135
|
+
children: label,
|
|
136
|
+
width: comfortableLabelWidth,
|
|
137
|
+
style
|
|
138
|
+
})
|
|
139
|
+
const estimatedWrappedWidth = Math.max(...wordsByLines.map(line => line.width || 0), 0)
|
|
140
|
+
const labelLayout = getRegionLabelLayout(
|
|
141
|
+
clippedFrom,
|
|
142
|
+
regionWidth,
|
|
143
|
+
plotWidth,
|
|
144
|
+
comfortableLabelWidth,
|
|
145
|
+
estimatedWrappedWidth
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Text
|
|
150
|
+
x={labelLayout.x}
|
|
151
|
+
y={getRegionLabelY(fontSize)}
|
|
152
|
+
width={labelLayout.width}
|
|
153
|
+
fill={color || APP_FONT_COLOR}
|
|
154
|
+
fontSize={fontSize}
|
|
155
|
+
style={style}
|
|
156
|
+
lineHeight='1.1em'
|
|
157
|
+
verticalAnchor='start'
|
|
158
|
+
textAnchor='middle'
|
|
159
|
+
>
|
|
160
|
+
{label}
|
|
161
|
+
</Text>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
53
165
|
/** Find the closest date in domain to a target date */
|
|
54
166
|
const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) => number): T => {
|
|
55
167
|
let closest = domain[0]
|
|
@@ -73,17 +185,12 @@ const isLineLike = (type: string): boolean =>
|
|
|
73
185
|
const isBarLike = (type: string): boolean => type === VIZ_TYPES.BAR || type === VIZ_TYPES.COMBO
|
|
74
186
|
|
|
75
187
|
// TODO: should regions be removed on categorical axis?
|
|
76
|
-
const Regions: React.FC<RegionsProps> = ({
|
|
77
|
-
|
|
78
|
-
barWidth = 0,
|
|
79
|
-
totalBarsInGroup = 1,
|
|
80
|
-
yMax,
|
|
81
|
-
xMax,
|
|
82
|
-
yAxisWidth = 0
|
|
83
|
-
}) => {
|
|
84
|
-
const { parseDate, config } = useContext<ChartContext>(ConfigContext)
|
|
188
|
+
const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => {
|
|
189
|
+
const { parseDate, config, tableData, vizViewport } = useContext<ChartContext>(ConfigContext)
|
|
85
190
|
|
|
86
191
|
const { regions, visualizationType, orientation, xAxis } = config
|
|
192
|
+
const isMobileViewport = isMobileFontViewport(vizViewport || 'lg')
|
|
193
|
+
const regionLabelFontSize = isMobileViewport ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
|
|
87
194
|
|
|
88
195
|
const getBarOffset = (): number => (barWidth * totalBarsInGroup) / 2
|
|
89
196
|
|
|
@@ -91,15 +198,32 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
91
198
|
// HELPER FUNCTIONS FOR PREVIOUS DAYS
|
|
92
199
|
// ============================================
|
|
93
200
|
|
|
201
|
+
const getDataLastTime = (): number => {
|
|
202
|
+
const dataKey = config.xAxis.dataKey
|
|
203
|
+
if (!Array.isArray(tableData) || tableData.length === 0 || !dataKey) return NaN
|
|
204
|
+
let last = NaN
|
|
205
|
+
for (const row of tableData) {
|
|
206
|
+
const value = (row as Record<string, unknown>)[dataKey]
|
|
207
|
+
const t = parseDate(String(value))?.getTime()
|
|
208
|
+
if (typeof t === 'number' && !isNaN(t) && (isNaN(last) || t > last)) {
|
|
209
|
+
last = t
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return last
|
|
213
|
+
}
|
|
214
|
+
|
|
94
215
|
const calculatePreviousDaysFrom = (region: Region, axisType: string): number => {
|
|
95
216
|
const previousDays = Number(region.from) || 0
|
|
96
217
|
const domain = xScale.domain()
|
|
97
218
|
|
|
98
219
|
// Determine the "to" reference date
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
220
|
+
let toRefDate: number | Date
|
|
221
|
+
if (region.toType === 'Last Date') {
|
|
222
|
+
const dataLastTime = getDataLastTime()
|
|
223
|
+
toRefDate = !isNaN(dataLastTime) ? dataLastTime : new Date(domain[domain.length - 1] as string | number).getTime()
|
|
224
|
+
} else {
|
|
225
|
+
toRefDate = new Date(region.to)
|
|
226
|
+
}
|
|
103
227
|
|
|
104
228
|
const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate, config.locale)
|
|
105
229
|
const toDate = new Date(toFormatted)
|
|
@@ -132,8 +256,8 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
132
256
|
} else {
|
|
133
257
|
from = xScale(region.from)
|
|
134
258
|
}
|
|
135
|
-
// Add
|
|
136
|
-
let scalePadding =
|
|
259
|
+
// Add half bandwidth to center on the category
|
|
260
|
+
let scalePadding = 0
|
|
137
261
|
if (xScale.bandwidth) {
|
|
138
262
|
scalePadding += xScale.bandwidth() / 2
|
|
139
263
|
}
|
|
@@ -145,8 +269,8 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
145
269
|
return calculateLineLastDatePosition_Categorical()
|
|
146
270
|
}
|
|
147
271
|
let to = xScale(region.to)
|
|
148
|
-
// Add
|
|
149
|
-
let scalePadding =
|
|
272
|
+
// Add half bandwidth to center on the category
|
|
273
|
+
let scalePadding = 0
|
|
150
274
|
if (xScale.bandwidth) {
|
|
151
275
|
scalePadding += xScale.bandwidth() / 2
|
|
152
276
|
}
|
|
@@ -168,8 +292,8 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
168
292
|
const closestDate = findClosestDate(parsedDate, domain, d => d)
|
|
169
293
|
from = xScale(closestDate)
|
|
170
294
|
}
|
|
171
|
-
// Add
|
|
172
|
-
let scalePadding =
|
|
295
|
+
// Add half bandwidth to center on the date band
|
|
296
|
+
let scalePadding = 0
|
|
173
297
|
if (xScale.bandwidth) {
|
|
174
298
|
scalePadding += xScale.bandwidth() / 2
|
|
175
299
|
}
|
|
@@ -189,8 +313,8 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
189
313
|
const closestDate = findClosestDate(parsedDate, domain, d => d)
|
|
190
314
|
let to = xScale(closestDate)
|
|
191
315
|
|
|
192
|
-
// Add
|
|
193
|
-
let scalePadding =
|
|
316
|
+
// Add half bandwidth to center on the date band
|
|
317
|
+
let scalePadding = 0
|
|
194
318
|
if (xScale.bandwidth) {
|
|
195
319
|
scalePadding += xScale.bandwidth() / 2
|
|
196
320
|
}
|
|
@@ -200,13 +324,13 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
200
324
|
const getLineFromValue_DateTime = (region: Region): number => {
|
|
201
325
|
if (region.fromType === 'Previous Days') {
|
|
202
326
|
const from = calculatePreviousDaysFrom(region, 'date-time')
|
|
203
|
-
return from
|
|
327
|
+
return from
|
|
204
328
|
}
|
|
205
329
|
const date = new Date(region.from)
|
|
206
330
|
const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
|
|
207
331
|
let from = xScale(parsedDate)
|
|
208
|
-
// For date-time, xScale returns
|
|
209
|
-
return from
|
|
332
|
+
// For date-time, xScale returns the plot-local position (no bandwidth)
|
|
333
|
+
return from
|
|
210
334
|
}
|
|
211
335
|
|
|
212
336
|
const getLineToValue_DateTime = (region: Region): number => {
|
|
@@ -214,27 +338,24 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
214
338
|
return calculateLineLastDatePosition_DateTime()
|
|
215
339
|
}
|
|
216
340
|
let to = xScale(parseDate(region.to).getTime())
|
|
217
|
-
return to
|
|
341
|
+
return to
|
|
218
342
|
}
|
|
219
343
|
|
|
220
344
|
const calculateLineLastDatePosition_Categorical = (): number => {
|
|
221
|
-
const chartStart = yAxisWidth
|
|
222
345
|
// Extend to the right edge of the chart
|
|
223
|
-
return
|
|
346
|
+
return xMax || 0
|
|
224
347
|
}
|
|
225
348
|
|
|
226
349
|
const calculateLineLastDatePosition_Date = (): number => {
|
|
227
|
-
const chartStart = yAxisWidth
|
|
228
350
|
// For date scale line charts with Last Date, extend to the right edge of the chart
|
|
229
|
-
return
|
|
351
|
+
return xMax || 0
|
|
230
352
|
}
|
|
231
353
|
|
|
232
354
|
const calculateLineLastDatePosition_DateTime = (): number => {
|
|
233
355
|
const domain = xScale.domain()
|
|
234
356
|
const lastDate = domain[domain.length - 1]
|
|
235
357
|
const lastDatePosition = xScale(lastDate)
|
|
236
|
-
|
|
237
|
-
return Number(lastDatePosition + yAxisWidth)
|
|
358
|
+
return Number(lastDatePosition)
|
|
238
359
|
}
|
|
239
360
|
|
|
240
361
|
// ============================================
|
|
@@ -333,6 +454,31 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
333
454
|
return Number(lastDatePosition)
|
|
334
455
|
}
|
|
335
456
|
|
|
457
|
+
const isRegionInVisibleDomain = (region: Region): boolean => {
|
|
458
|
+
if (region.toType !== 'Last Date') return true
|
|
459
|
+
if (xAxis.type !== 'date' && xAxis.type !== 'date-time') return true
|
|
460
|
+
|
|
461
|
+
const domain = xScale.domain()
|
|
462
|
+
if (!domain || domain.length === 0) return true
|
|
463
|
+
const visEnd = Number(domain[domain.length - 1])
|
|
464
|
+
if (isNaN(visEnd)) return true
|
|
465
|
+
|
|
466
|
+
let fromTime: number | undefined
|
|
467
|
+
if (region.fromType === 'Previous Days') {
|
|
468
|
+
const dataLastTime = getDataLastTime()
|
|
469
|
+
if (isNaN(dataLastTime)) return true
|
|
470
|
+
const previousDays = Number(region.from) || 0
|
|
471
|
+
const fromDate = new Date(dataLastTime)
|
|
472
|
+
fromDate.setDate(fromDate.getDate() - previousDays)
|
|
473
|
+
fromTime = fromDate.getTime()
|
|
474
|
+
} else {
|
|
475
|
+
fromTime = parseDate(formatDate(config.xAxis.dateParseFormat, new Date(region.from), config.locale))?.getTime()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (fromTime === undefined || isNaN(fromTime)) return true
|
|
479
|
+
return fromTime < visEnd
|
|
480
|
+
}
|
|
481
|
+
|
|
336
482
|
// ============================================
|
|
337
483
|
// MAIN ROUTING FUNCTIONS
|
|
338
484
|
// ============================================
|
|
@@ -399,10 +545,12 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
399
545
|
|
|
400
546
|
if (!regions || orientation !== 'vertical') return null
|
|
401
547
|
|
|
402
|
-
const chartStart =
|
|
403
|
-
const chartEnd = xMax !== undefined ?
|
|
548
|
+
const chartStart = 0
|
|
549
|
+
const chartEnd = xMax !== undefined ? xMax : 1000
|
|
404
550
|
|
|
405
551
|
return regions.map((region: Region) => {
|
|
552
|
+
if (!isRegionInVisibleDomain(region)) return null
|
|
553
|
+
|
|
406
554
|
const from = getFromValue(region)
|
|
407
555
|
const to = getToValue(region)
|
|
408
556
|
|
|
@@ -419,11 +567,17 @@ const Regions: React.FC<RegionsProps> = ({
|
|
|
419
567
|
if (width <= 0) return null
|
|
420
568
|
|
|
421
569
|
return (
|
|
422
|
-
<Group height={100}
|
|
570
|
+
<Group height={100} className='regions regions-group--line' key={region.label} pointerEvents='none'>
|
|
423
571
|
<HighlightedArea x={clippedFrom} width={width} yMax={yMax} background={region.background} />
|
|
424
|
-
<
|
|
425
|
-
{region.label}
|
|
426
|
-
|
|
572
|
+
<RegionLabel
|
|
573
|
+
label={region.label}
|
|
574
|
+
color={region.color}
|
|
575
|
+
clippedFrom={clippedFrom}
|
|
576
|
+
regionWidth={width}
|
|
577
|
+
plotWidth={chartEnd}
|
|
578
|
+
fontSize={regionLabelFontSize}
|
|
579
|
+
isMobileViewport={isMobileViewport}
|
|
580
|
+
/>
|
|
427
581
|
</Group>
|
|
428
582
|
)
|
|
429
583
|
})
|
|
@@ -16,8 +16,10 @@ import useSankeyAlert from '../useSankeyAlert'
|
|
|
16
16
|
import { getSankeyTooltip } from '../helpers/getSankeyTooltip'
|
|
17
17
|
|
|
18
18
|
const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
19
|
-
const { config } = useContext<ChartContext>(ConfigContext)
|
|
19
|
+
const { config, handleChartAriaLabels } = useContext<ChartContext>(ConfigContext)
|
|
20
20
|
const { sankey: sankeyConfig } = config
|
|
21
|
+
|
|
22
|
+
const a11y = handleChartAriaLabels(config)
|
|
21
23
|
const [largestGroupWidth, setLargestGroupWidth] = useState(0)
|
|
22
24
|
const [tooltipID, setTooltipID] = useState<string>('')
|
|
23
25
|
const { showAlert, alert } = useSankeyAlert()
|
|
@@ -43,7 +45,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
43
45
|
useEffect(() => {
|
|
44
46
|
let largest = 0
|
|
45
47
|
groupRefs?.current?.map(g => {
|
|
46
|
-
const groupWidth = g?.getBoundingClientRect().width
|
|
48
|
+
const groupWidth = g?.getBBox?.().width || g?.getBoundingClientRect?.().width || 0
|
|
47
49
|
if (groupWidth > largest) {
|
|
48
50
|
largest = groupWidth
|
|
49
51
|
}
|
|
@@ -455,10 +457,12 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
455
457
|
width={width}
|
|
456
458
|
height={Number(config.heights.vertical)}
|
|
457
459
|
style={{ overflow: 'visible' }}
|
|
460
|
+
role='img'
|
|
461
|
+
aria-label={a11y}
|
|
458
462
|
>
|
|
459
463
|
<Group className='links'>{allLinks}</Group>
|
|
460
464
|
<Group className='nodes'>{allNodes}</Group>
|
|
461
|
-
<Group className='finalNodes' style={{
|
|
465
|
+
<Group className='finalNodes' style={{ visibility: 'hidden', pointerEvents: 'none' }} aria-hidden='true'>
|
|
462
466
|
{finalNodes}
|
|
463
467
|
</Group>
|
|
464
468
|
</svg>
|
|
@@ -2,12 +2,13 @@ import ReactDOMServer from 'react-dom/server'
|
|
|
2
2
|
import ColumnList from '../components/ColumnList'
|
|
3
3
|
|
|
4
4
|
export const getSankeyTooltip = (data: Object, tooltipID: string) => {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
5
|
+
const tooltips = Array.isArray(data?.tooltips) ? data.tooltips : []
|
|
6
|
+
const tooltipVal = `${(tooltips.find(item => item.node === tooltipID) || {}).value}`
|
|
7
|
+
const tooltipSummary = `${(tooltips.find(item => item.node === tooltipID) || {}).summary}`
|
|
8
|
+
const tooltipColumn1Label = (tooltips.find(item => item.node === tooltipID) || {}).column1Label
|
|
9
|
+
const tooltipColumn2Label = (tooltips.find(item => item.node === tooltipID) || {}).column2Label
|
|
10
|
+
const tooltipColumn1 = (tooltips.find(item => item.node === tooltipID) || {}).column1
|
|
11
|
+
const tooltipColumn2 = (tooltips.find(item => item.node === tooltipID) || {}).column2
|
|
11
12
|
|
|
12
13
|
const tooltipColumn1Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn1} />)
|
|
13
14
|
const tooltipColumn2Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn2} />)
|
|
@@ -6,7 +6,7 @@ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
|
6
6
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
7
7
|
import { buildSeriesTooltipListHtml } from '../../helpers/tooltipHelpers'
|
|
8
8
|
|
|
9
|
-
const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
9
|
+
const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
|
|
10
10
|
const {
|
|
11
11
|
transformedData: data,
|
|
12
12
|
config,
|
|
@@ -61,7 +61,7 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
|
61
61
|
return config.runtime.seriesKeys.map((s, index) => {
|
|
62
62
|
const transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s) === -1
|
|
63
63
|
const displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s) !== -1
|
|
64
|
-
const seriesColor =
|
|
64
|
+
const seriesColor = colorScale(config.runtime.seriesLabels?.[s] || s)
|
|
65
65
|
|
|
66
66
|
let pointStyles = {
|
|
67
67
|
filter: 'unset',
|
|
@@ -78,7 +78,7 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
|
78
78
|
<circle
|
|
79
79
|
key={`${dataIndex}-${index}`}
|
|
80
80
|
r={circleRadii}
|
|
81
|
-
cx={xScale(item
|
|
81
|
+
cx={xScale(getXAxisData(item))}
|
|
82
82
|
cy={yScale(item[s])}
|
|
83
83
|
fill={displayArea ? seriesColor : 'transparent'}
|
|
84
84
|
fillOpacity={transparentArea ? 0.25 : 1}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, useRef, useEffect } from 'react'
|
|
1
|
+
import React, { useContext, useRef, useEffect, useState } from 'react'
|
|
2
2
|
import LinearChart from '../LinearChart'
|
|
3
3
|
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
4
4
|
import ConfigContext from '../../ConfigContext'
|
|
@@ -7,6 +7,7 @@ import cloneConfig from '@cdc/core/helpers/cloneConfig'
|
|
|
7
7
|
import { getTileDisplayTitle } from '../../helpers/smallMultiplesHelpers'
|
|
8
8
|
import getViewport from '@cdc/core/helpers/getViewport'
|
|
9
9
|
import { ChartConfig } from '../../types/ChartConfig'
|
|
10
|
+
import { getAxisLabelFontSize } from '../../helpers/axisLabelFontSize'
|
|
10
11
|
|
|
11
12
|
interface SmallMultipleTileProps {
|
|
12
13
|
mode: 'by-series' | 'by-column'
|
|
@@ -22,6 +23,7 @@ interface SmallMultipleTileProps {
|
|
|
22
23
|
parentHeight?: number
|
|
23
24
|
globalYAxisMax?: number
|
|
24
25
|
globalYAxisMin?: number
|
|
26
|
+
globalYAxisTickValues?: number[]
|
|
25
27
|
isFirstInRow?: boolean
|
|
26
28
|
onHeightChange?: (tileKey: string, height: number) => void
|
|
27
29
|
onChartRef?: (ref: any) => void
|
|
@@ -42,6 +44,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
42
44
|
parentWidth,
|
|
43
45
|
globalYAxisMax,
|
|
44
46
|
globalYAxisMin,
|
|
47
|
+
globalYAxisTickValues,
|
|
45
48
|
isFirstInRow,
|
|
46
49
|
onHeightChange,
|
|
47
50
|
onChartRef,
|
|
@@ -107,11 +110,13 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
const titlePlacementIsTop = tileConfig.yAxis?.titlePlacement === 'top'
|
|
114
|
+
|
|
110
115
|
// Small multiples-specific modifications
|
|
111
116
|
tileConfig = {
|
|
112
117
|
...tileConfig,
|
|
113
118
|
hideXAxisLabel: !isFirstInRow,
|
|
114
|
-
hideYAxisLabel: !isFirstInRow,
|
|
119
|
+
hideYAxisLabel: config.hideYAxisLabel || (!isFirstInRow && !titlePlacementIsTop),
|
|
115
120
|
legend: {
|
|
116
121
|
...tileConfig.legend,
|
|
117
122
|
tooltipLegendVisible: !config.legend?.hide,
|
|
@@ -125,6 +130,8 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
const displayTitle = getTileDisplayTitle(mode, seriesKey, tileValue, tileKey, config)
|
|
133
|
+
const showTopYAxisTitle =
|
|
134
|
+
titlePlacementIsTop && isFirstInRow && !config.hideYAxisLabel && Boolean(tileConfig.runtime?.yAxis?.label)
|
|
128
135
|
|
|
129
136
|
// Get the original context values to merge with our filtered config
|
|
130
137
|
const originalContextValues = useContext(ConfigContext)
|
|
@@ -134,6 +141,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
134
141
|
|
|
135
142
|
// Create a ref for the entire tile (including header) for height measurement
|
|
136
143
|
const fullTileRef = useRef<HTMLDivElement>(null)
|
|
144
|
+
const [tileChartWidth, setTileChartWidth] = useState(0)
|
|
137
145
|
|
|
138
146
|
// Create a ref for the LinearChart instance for tooltip coordination
|
|
139
147
|
const linearChartRef = useRef<any>(null)
|
|
@@ -146,6 +154,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
146
154
|
tableData: tileData, // Override with tile-specific filtered data (important for tooltip data lookup)
|
|
147
155
|
parentRef: tileParentRef, // Override with tile-specific parentRef
|
|
148
156
|
updateConfig: () => {}, // Prevent tile hooks from modifying global config
|
|
157
|
+
yAxisTickValues: globalYAxisTickValues,
|
|
149
158
|
...(customColorScale && { colorScale: customColorScale }) // Override colorScale if provided
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -164,6 +173,22 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
164
173
|
return () => resizeObserver.disconnect()
|
|
165
174
|
}, [tileKey, onHeightChange])
|
|
166
175
|
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!tileParentRef.current) return
|
|
178
|
+
|
|
179
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
180
|
+
entries.forEach(entry => {
|
|
181
|
+
setTileChartWidth(entry.contentRect.width)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
resizeObserver.observe(tileParentRef.current)
|
|
186
|
+
return () => resizeObserver.disconnect()
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
const tileVizViewport = tileChartWidth > 0 ? getViewport(tileChartWidth) : originalContextValues.vizViewport || 'lg'
|
|
190
|
+
const topYAxisTitleFontSize = getAxisLabelFontSize(tileVizViewport)
|
|
191
|
+
|
|
167
192
|
// Pass chart ref to parent SmallMultiples component for tooltip coordination
|
|
168
193
|
useEffect(() => {
|
|
169
194
|
if (onChartRef && linearChartRef.current) {
|
|
@@ -173,8 +198,13 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
173
198
|
|
|
174
199
|
return (
|
|
175
200
|
<div ref={fullTileRef} className='small-multiple-tile'>
|
|
176
|
-
<div ref={onHeaderRef} className='tile-header'>
|
|
201
|
+
<div ref={onHeaderRef} className={`tile-header${showTopYAxisTitle ? ' tile-header--with-top-y-axis-title' : ''}`}>
|
|
177
202
|
<div className='tile-title'>{displayTitle}</div>
|
|
203
|
+
{showTopYAxisTitle && (
|
|
204
|
+
<div className='y-axis-top-title' style={{ fontSize: `${topYAxisTitleFontSize}px` }}>
|
|
205
|
+
{tileConfig.runtime.yAxis.label}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
178
208
|
</div>
|
|
179
209
|
<div ref={tileParentRef} className='tile-chart'>
|
|
180
210
|
<ParentSize
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
margin-bottom: 0.5rem;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
.small-multiples-container--with-top-y-axis-title .tile-header {
|
|
23
|
+
margin-bottom: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tile-header--with-top-y-axis-title {
|
|
27
|
+
margin-bottom: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tile-header--with-top-y-axis-title .tile-title {
|
|
31
|
+
margin-bottom: 0.5rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
.tile-title {
|
|
23
35
|
font-weight: 700;
|
|
24
36
|
line-height: 1.3;
|