@cdc/chart 4.26.3 → 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 +489 -0
- package/README.md +42 -17
- package/dist/cdcchart.js +39024 -36587
- package/examples/feature/bar/additional-column-tooltip.json +3 -3
- package/examples/feature/deviation/planet-deviation-config.json +2 -2
- 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/feature/paired-bar/paired-bar-example.json +2 -2
- package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
- package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
- package/examples/feature/tests-covid/covid-example-config.json +2 -2
- package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
- package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
- package/examples/metadata-variables.json +1 -1
- package/examples/minimal-example.json +24 -0
- package/examples/private/bump-chart.json +1116 -0
- package/examples/private/test-123.json +624 -0
- package/examples/private/test2.json +35012 -0
- package/package.json +5 -4
- package/src/CdcChartComponent.tsx +144 -60
- package/src/_stories/Chart.AltText.stories.tsx +136 -0
- package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
- package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
- package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
- package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
- package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
- package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
- package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
- package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
- package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
- package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
- package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
- package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
- 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/line_chart_dashed_dates.json +276 -0
- package/src/_stories/_mock/paired-bar-abbr.json +1 -1
- package/src/_stories/_mock/paired-bar.json +1 -1
- 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 +2 -2
- package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
- package/src/components/Axis/BottomAxis.tsx +1 -1
- package/src/components/Axis/LeftAxis.tsx +15 -14
- package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
- package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
- package/src/components/BarChart/components/BarChart.tsx +2 -6
- package/src/components/BarChart/helpers/useBarChart.ts +1 -1
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
- 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 +946 -960
- package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
- 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 +32 -21
- package/src/components/Forecasting/Forecasting.tsx +2 -2
- 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/HorizonChart/HorizonChart.tsx +10 -2
- package/src/components/HoverLine/HoverLine.tsx +6 -8
- package/src/components/Legend/Legend.Component.tsx +4 -4
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
- package/src/components/Legend/helpers/index.ts +0 -2
- package/src/components/LineChart/LineChartProps.ts +1 -0
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
- package/src/components/LineChart/helpers.test.ts +345 -0
- package/src/components/LineChart/helpers.ts +15 -5
- package/src/components/LineChart/index.tsx +18 -5
- package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
- package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
- package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
- package/src/components/LinearChart.tsx +155 -77
- package/src/components/PairedBarChart.jsx +2 -2
- package/src/components/PieChart/PieChart.tsx +15 -10
- package/src/components/RadarChart/RadarChart.tsx +3 -1
- package/src/components/Regions/components/Regions.tsx +199 -35
- package/src/components/Sankey/components/Sankey.tsx +7 -3
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
- package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -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 +5 -10
- package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
- 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 +197 -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/helpers/tests/tooltipHelpers.test.ts +151 -0
- package/src/helpers/tooltipHelpers.ts +176 -0
- package/src/hooks/tests/useScales.test.tsx +105 -0
- package/src/hooks/useHighlightedBars.ts +9 -16
- package/src/hooks/useProgrammaticTooltip.ts +16 -2
- package/src/hooks/useScales.ts +19 -9
- package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
- package/src/hooks/useTooltip.tsx +167 -38
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +32 -49
- package/src/test/CdcChart.test.jsx +24 -0
- package/src/types/ChartConfig.ts +19 -0
- package/src/types/ChartContext.ts +2 -0
- package/examples/chart-regression-2.json +0 -2360
- package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
- package/examples/feature/__data__/city-temperature.json +0 -2198
- package/examples/feature/bar/Barchart_with_negative.json +0 -34
- package/examples/feature/boxplot/box-plot-data.json +0 -71
- package/examples/feature/boxplot/boxplot-data.json +0 -156
- package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
- package/examples/feature/forest-plot/log.json +0 -26
- package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
- package/examples/region-issue.json +0 -2065
- package/examples/xaxis.json +0 -493
- package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
- /package/examples/{data → __data__}/data-with-metadata.json +0 -0
- /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
- /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
- /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
- /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
- /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
- /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
- /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
- /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
- /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
- /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
- /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
- /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
- /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
|
@@ -1,18 +1,31 @@
|
|
|
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 = {
|
|
10
12
|
BAR: 'Bar',
|
|
11
13
|
LINE: 'Line',
|
|
12
14
|
AREA: 'Area Chart',
|
|
13
|
-
COMBO: 'Combo'
|
|
15
|
+
COMBO: 'Combo',
|
|
16
|
+
HORIZON: 'Horizon Chart'
|
|
14
17
|
} as const
|
|
15
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
|
+
|
|
16
29
|
type Region = {
|
|
17
30
|
from: string
|
|
18
31
|
to: string
|
|
@@ -44,10 +57,111 @@ type HighlightedAreaProps = {
|
|
|
44
57
|
background: string
|
|
45
58
|
}
|
|
46
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
|
+
|
|
47
75
|
const HighlightedArea: React.FC<HighlightedAreaProps> = ({ x, width, yMax, background }) => (
|
|
48
|
-
<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} />
|
|
49
77
|
)
|
|
50
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
|
+
|
|
51
165
|
/** Find the closest date in domain to a target date */
|
|
52
166
|
const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) => number): T => {
|
|
53
167
|
let closest = domain[0]
|
|
@@ -63,17 +177,20 @@ const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) =>
|
|
|
63
177
|
return closest
|
|
64
178
|
}
|
|
65
179
|
|
|
66
|
-
/** Check if visualization type is line-like (Line or
|
|
67
|
-
const isLineLike = (type: string): boolean =>
|
|
180
|
+
/** Check if visualization type is line-like (Line, Area Chart, or Horizon Chart) */
|
|
181
|
+
const isLineLike = (type: string): boolean =>
|
|
182
|
+
type === VIZ_TYPES.LINE || type === VIZ_TYPES.AREA || type === VIZ_TYPES.HORIZON
|
|
68
183
|
|
|
69
184
|
/** Check if visualization type is bar-like (Bar or Combo) */
|
|
70
185
|
const isBarLike = (type: string): boolean => type === VIZ_TYPES.BAR || type === VIZ_TYPES.COMBO
|
|
71
186
|
|
|
72
187
|
// TODO: should regions be removed on categorical axis?
|
|
73
188
|
const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => {
|
|
74
|
-
const { parseDate, config } = useContext<ChartContext>(ConfigContext)
|
|
189
|
+
const { parseDate, config, tableData, vizViewport } = useContext<ChartContext>(ConfigContext)
|
|
75
190
|
|
|
76
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
|
|
77
194
|
|
|
78
195
|
const getBarOffset = (): number => (barWidth * totalBarsInGroup) / 2
|
|
79
196
|
|
|
@@ -81,15 +198,32 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
81
198
|
// HELPER FUNCTIONS FOR PREVIOUS DAYS
|
|
82
199
|
// ============================================
|
|
83
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
|
+
|
|
84
215
|
const calculatePreviousDaysFrom = (region: Region, axisType: string): number => {
|
|
85
216
|
const previousDays = Number(region.from) || 0
|
|
86
217
|
const domain = xScale.domain()
|
|
87
218
|
|
|
88
219
|
// Determine the "to" reference date
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
227
|
|
|
94
228
|
const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate, config.locale)
|
|
95
229
|
const toDate = new Date(toFormatted)
|
|
@@ -122,8 +256,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
122
256
|
} else {
|
|
123
257
|
from = xScale(region.from)
|
|
124
258
|
}
|
|
125
|
-
// Add
|
|
126
|
-
let scalePadding =
|
|
259
|
+
// Add half bandwidth to center on the category
|
|
260
|
+
let scalePadding = 0
|
|
127
261
|
if (xScale.bandwidth) {
|
|
128
262
|
scalePadding += xScale.bandwidth() / 2
|
|
129
263
|
}
|
|
@@ -135,8 +269,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
135
269
|
return calculateLineLastDatePosition_Categorical()
|
|
136
270
|
}
|
|
137
271
|
let to = xScale(region.to)
|
|
138
|
-
// Add
|
|
139
|
-
let scalePadding =
|
|
272
|
+
// Add half bandwidth to center on the category
|
|
273
|
+
let scalePadding = 0
|
|
140
274
|
if (xScale.bandwidth) {
|
|
141
275
|
scalePadding += xScale.bandwidth() / 2
|
|
142
276
|
}
|
|
@@ -158,8 +292,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
158
292
|
const closestDate = findClosestDate(parsedDate, domain, d => d)
|
|
159
293
|
from = xScale(closestDate)
|
|
160
294
|
}
|
|
161
|
-
// Add
|
|
162
|
-
let scalePadding =
|
|
295
|
+
// Add half bandwidth to center on the date band
|
|
296
|
+
let scalePadding = 0
|
|
163
297
|
if (xScale.bandwidth) {
|
|
164
298
|
scalePadding += xScale.bandwidth() / 2
|
|
165
299
|
}
|
|
@@ -179,8 +313,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
179
313
|
const closestDate = findClosestDate(parsedDate, domain, d => d)
|
|
180
314
|
let to = xScale(closestDate)
|
|
181
315
|
|
|
182
|
-
// Add
|
|
183
|
-
let scalePadding =
|
|
316
|
+
// Add half bandwidth to center on the date band
|
|
317
|
+
let scalePadding = 0
|
|
184
318
|
if (xScale.bandwidth) {
|
|
185
319
|
scalePadding += xScale.bandwidth() / 2
|
|
186
320
|
}
|
|
@@ -190,13 +324,13 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
190
324
|
const getLineFromValue_DateTime = (region: Region): number => {
|
|
191
325
|
if (region.fromType === 'Previous Days') {
|
|
192
326
|
const from = calculatePreviousDaysFrom(region, 'date-time')
|
|
193
|
-
return from
|
|
327
|
+
return from
|
|
194
328
|
}
|
|
195
329
|
const date = new Date(region.from)
|
|
196
330
|
const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
|
|
197
331
|
let from = xScale(parsedDate)
|
|
198
|
-
// For date-time, xScale returns
|
|
199
|
-
return from
|
|
332
|
+
// For date-time, xScale returns the plot-local position (no bandwidth)
|
|
333
|
+
return from
|
|
200
334
|
}
|
|
201
335
|
|
|
202
336
|
const getLineToValue_DateTime = (region: Region): number => {
|
|
@@ -204,27 +338,24 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
204
338
|
return calculateLineLastDatePosition_DateTime()
|
|
205
339
|
}
|
|
206
340
|
let to = xScale(parseDate(region.to).getTime())
|
|
207
|
-
return to
|
|
341
|
+
return to
|
|
208
342
|
}
|
|
209
343
|
|
|
210
344
|
const calculateLineLastDatePosition_Categorical = (): number => {
|
|
211
|
-
const chartStart = Number(config.yAxis.size || 0)
|
|
212
345
|
// Extend to the right edge of the chart
|
|
213
|
-
return
|
|
346
|
+
return xMax || 0
|
|
214
347
|
}
|
|
215
348
|
|
|
216
349
|
const calculateLineLastDatePosition_Date = (): number => {
|
|
217
|
-
const chartStart = Number(config.yAxis.size || 0)
|
|
218
350
|
// For date scale line charts with Last Date, extend to the right edge of the chart
|
|
219
|
-
return
|
|
351
|
+
return xMax || 0
|
|
220
352
|
}
|
|
221
353
|
|
|
222
354
|
const calculateLineLastDatePosition_DateTime = (): number => {
|
|
223
355
|
const domain = xScale.domain()
|
|
224
356
|
const lastDate = domain[domain.length - 1]
|
|
225
357
|
const lastDatePosition = xScale(lastDate)
|
|
226
|
-
|
|
227
|
-
return Number(lastDatePosition + Number(config.yAxis.size))
|
|
358
|
+
return Number(lastDatePosition)
|
|
228
359
|
}
|
|
229
360
|
|
|
230
361
|
// ============================================
|
|
@@ -323,6 +454,31 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
323
454
|
return Number(lastDatePosition)
|
|
324
455
|
}
|
|
325
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
|
+
|
|
326
482
|
// ============================================
|
|
327
483
|
// MAIN ROUTING FUNCTIONS
|
|
328
484
|
// ============================================
|
|
@@ -389,10 +545,12 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
389
545
|
|
|
390
546
|
if (!regions || orientation !== 'vertical') return null
|
|
391
547
|
|
|
392
|
-
const chartStart =
|
|
393
|
-
const chartEnd = xMax !== undefined ?
|
|
548
|
+
const chartStart = 0
|
|
549
|
+
const chartEnd = xMax !== undefined ? xMax : 1000
|
|
394
550
|
|
|
395
551
|
return regions.map((region: Region) => {
|
|
552
|
+
if (!isRegionInVisibleDomain(region)) return null
|
|
553
|
+
|
|
396
554
|
const from = getFromValue(region)
|
|
397
555
|
const to = getToValue(region)
|
|
398
556
|
|
|
@@ -409,11 +567,17 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
409
567
|
if (width <= 0) return null
|
|
410
568
|
|
|
411
569
|
return (
|
|
412
|
-
<Group height={100}
|
|
570
|
+
<Group height={100} className='regions regions-group--line' key={region.label} pointerEvents='none'>
|
|
413
571
|
<HighlightedArea x={clippedFrom} width={width} yMax={yMax} background={region.background} />
|
|
414
|
-
<
|
|
415
|
-
{region.label}
|
|
416
|
-
|
|
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
|
+
/>
|
|
417
581
|
</Group>
|
|
418
582
|
)
|
|
419
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} />)
|
|
@@ -4,22 +4,21 @@ import { Group } from '@visx/group'
|
|
|
4
4
|
import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
|
|
5
5
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
6
6
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
7
|
+
import { buildSeriesTooltipListHtml } from '../../helpers/tooltipHelpers'
|
|
7
8
|
|
|
8
|
-
const ScatterPlot = ({ xScale, yScale }) => {
|
|
9
|
+
const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
9
10
|
const {
|
|
10
11
|
transformedData: data,
|
|
11
12
|
config,
|
|
12
13
|
tableData,
|
|
13
14
|
formatNumber,
|
|
14
15
|
seriesHighlight,
|
|
15
|
-
colorPalettes,
|
|
16
16
|
colorScale,
|
|
17
17
|
interactionLabel
|
|
18
18
|
} = useContext(ConfigContext)
|
|
19
19
|
|
|
20
20
|
// TODO: copied from line chart should probably be a constant somewhere.
|
|
21
21
|
const circleRadii = 4.5
|
|
22
|
-
const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
|
|
23
22
|
|
|
24
23
|
// Track current hover for analytics
|
|
25
24
|
const [currentHover, setCurrentHover] = useState({ dataIndex: null, seriesKey: null })
|
|
@@ -37,23 +36,26 @@ const ScatterPlot = ({ xScale, yScale }) => {
|
|
|
37
36
|
}
|
|
38
37
|
])
|
|
39
38
|
const handleTooltip = (item, s, dataIndex) => `<div>
|
|
40
|
-
${
|
|
41
|
-
config
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
${buildSeriesTooltipListHtml({
|
|
40
|
+
config,
|
|
41
|
+
colorScale,
|
|
42
|
+
heading: `${config.runtime?.xAxis?.label || config.xAxis.label}: ${formatNumber(
|
|
43
|
+
item[config.xAxis.dataKey],
|
|
44
|
+
'bottom'
|
|
45
|
+
)}`,
|
|
46
|
+
seriesKey: s,
|
|
47
|
+
seriesText: `${config.runtime.seriesLabels[s] || s}: ${formatNumber(item[s], 'left')}`,
|
|
48
|
+
extraRows: additionalColumns
|
|
49
|
+
.map(
|
|
50
|
+
([label, name, options]) =>
|
|
51
|
+
`${label} : ${formatColNumber(tableData[dataIndex][name], 'left', false, config, options)}`
|
|
52
|
+
)
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
})}
|
|
53
55
|
</div>`
|
|
54
56
|
|
|
55
57
|
return (
|
|
56
|
-
<Group className='scatter-plot' left={
|
|
58
|
+
<Group className='scatter-plot' left={yAxisWidth}>
|
|
57
59
|
{data.map((item, dataIndex) => {
|
|
58
60
|
// prettier-ignore
|
|
59
61
|
return config.runtime.seriesKeys.map((s, index) => {
|
|
@@ -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'
|
|
@@ -107,13 +108,16 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
const titlePlacementIsTop = tileConfig.yAxis?.titlePlacement === 'top'
|
|
112
|
+
|
|
110
113
|
// Small multiples-specific modifications
|
|
111
114
|
tileConfig = {
|
|
112
115
|
...tileConfig,
|
|
113
116
|
hideXAxisLabel: !isFirstInRow,
|
|
114
|
-
hideYAxisLabel: !isFirstInRow,
|
|
117
|
+
hideYAxisLabel: config.hideYAxisLabel || (!isFirstInRow && !titlePlacementIsTop),
|
|
115
118
|
legend: {
|
|
116
119
|
...tileConfig.legend,
|
|
120
|
+
tooltipLegendVisible: !config.legend?.hide,
|
|
117
121
|
hide: true // Hide legends for all small multiple tiles
|
|
118
122
|
},
|
|
119
123
|
xAxis: {
|
|
@@ -124,6 +128,8 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
const displayTitle = getTileDisplayTitle(mode, seriesKey, tileValue, tileKey, config)
|
|
131
|
+
const showTopYAxisTitle =
|
|
132
|
+
titlePlacementIsTop && isFirstInRow && !config.hideYAxisLabel && Boolean(tileConfig.runtime?.yAxis?.label)
|
|
127
133
|
|
|
128
134
|
// Get the original context values to merge with our filtered config
|
|
129
135
|
const originalContextValues = useContext(ConfigContext)
|
|
@@ -133,6 +139,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
133
139
|
|
|
134
140
|
// Create a ref for the entire tile (including header) for height measurement
|
|
135
141
|
const fullTileRef = useRef<HTMLDivElement>(null)
|
|
142
|
+
const [tileChartWidth, setTileChartWidth] = useState(0)
|
|
136
143
|
|
|
137
144
|
// Create a ref for the LinearChart instance for tooltip coordination
|
|
138
145
|
const linearChartRef = useRef<any>(null)
|
|
@@ -163,6 +170,22 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
163
170
|
return () => resizeObserver.disconnect()
|
|
164
171
|
}, [tileKey, onHeightChange])
|
|
165
172
|
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!tileParentRef.current) return
|
|
175
|
+
|
|
176
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
177
|
+
entries.forEach(entry => {
|
|
178
|
+
setTileChartWidth(entry.contentRect.width)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
resizeObserver.observe(tileParentRef.current)
|
|
183
|
+
return () => resizeObserver.disconnect()
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
const tileVizViewport = tileChartWidth > 0 ? getViewport(tileChartWidth) : originalContextValues.vizViewport || 'lg'
|
|
187
|
+
const topYAxisTitleFontSize = getAxisLabelFontSize(tileVizViewport)
|
|
188
|
+
|
|
166
189
|
// Pass chart ref to parent SmallMultiples component for tooltip coordination
|
|
167
190
|
useEffect(() => {
|
|
168
191
|
if (onChartRef && linearChartRef.current) {
|
|
@@ -172,8 +195,13 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
172
195
|
|
|
173
196
|
return (
|
|
174
197
|
<div ref={fullTileRef} className='small-multiple-tile'>
|
|
175
|
-
<div ref={onHeaderRef} className='tile-header'>
|
|
198
|
+
<div ref={onHeaderRef} className={`tile-header${showTopYAxisTitle ? ' tile-header--with-top-y-axis-title' : ''}`}>
|
|
176
199
|
<div className='tile-title'>{displayTitle}</div>
|
|
200
|
+
{showTopYAxisTitle && (
|
|
201
|
+
<div className='y-axis-top-title' style={{ fontSize: `${topYAxisTitleFontSize}px` }}>
|
|
202
|
+
{tileConfig.runtime.yAxis.label}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
177
205
|
</div>
|
|
178
206
|
<div ref={tileParentRef} className='tile-chart'>
|
|
179
207
|
<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;
|
|
@@ -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 { getYAxisAutoPaddingMode } from '../../helpers/getYAxisAutoPaddingMode'
|
|
6
7
|
import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
|
|
7
8
|
import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
|
|
8
9
|
import './SmallMultiples.css'
|
|
@@ -105,9 +106,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
105
106
|
combinedDataForYAxis.data
|
|
106
107
|
)
|
|
107
108
|
|
|
108
|
-
const
|
|
109
|
-
const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
|
|
110
|
-
const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
|
|
109
|
+
const yAxisAutoPaddingMode = getYAxisAutoPaddingMode(config)
|
|
111
110
|
|
|
112
111
|
const { min, max } = useScales({
|
|
113
112
|
config: combinedDataForYAxis.config,
|
|
@@ -120,7 +119,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
120
119
|
xAxisDataMapped: [],
|
|
121
120
|
xMax: parentWidth,
|
|
122
121
|
yMax: parentHeight,
|
|
123
|
-
|
|
122
|
+
yAxisAutoPaddingMode,
|
|
124
123
|
currentViewport
|
|
125
124
|
})
|
|
126
125
|
|
|
@@ -168,7 +167,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
168
167
|
)
|
|
169
168
|
|
|
170
169
|
// Align tile header heights per row
|
|
171
|
-
|
|
170
|
+
useLayoutEffect(() => {
|
|
172
171
|
const headerEntries = Object.entries(headerRefs.current).filter(([_, ref]) => ref) as TileHeaderEntries
|
|
173
172
|
if (headerEntries.length === 0) return
|
|
174
173
|
|
|
@@ -209,7 +208,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
209
208
|
}, [tileItems, tilesPerRow])
|
|
210
209
|
|
|
211
210
|
// Calculate container height from measured tile heights
|
|
212
|
-
|
|
211
|
+
useLayoutEffect(() => {
|
|
213
212
|
if (!parentRef.current) return
|
|
214
213
|
|
|
215
214
|
const measuredHeights = Object.values(tileHeights)
|
|
@@ -223,14 +222,21 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
223
222
|
const totalHeight = numberOfRows * maxTileHeight + totalGapsHeight
|
|
224
223
|
|
|
225
224
|
parentRef.current.style.height = `${totalHeight}px`
|
|
226
|
-
}, [tileHeights, numberOfRows, isMobile
|
|
225
|
+
}, [tileHeights, numberOfRows, isMobile])
|
|
227
226
|
|
|
228
227
|
if (tileItems.length === 0) {
|
|
229
228
|
return null
|
|
230
229
|
}
|
|
231
230
|
|
|
231
|
+
const hasTopYAxisTitle =
|
|
232
|
+
config.yAxis?.titlePlacement === 'top' && !config.hideYAxisLabel && Boolean(config.runtime?.yAxis?.label)
|
|
233
|
+
|
|
232
234
|
return (
|
|
233
|
-
<div
|
|
235
|
+
<div
|
|
236
|
+
className={`small-multiples-container${
|
|
237
|
+
hasTopYAxisTitle ? ' small-multiples-container--with-top-y-axis-title' : ''
|
|
238
|
+
}`}
|
|
239
|
+
>
|
|
234
240
|
<div className='small-multiples-grid' style={gridStyle}>
|
|
235
241
|
{tileItems.map((item, index) => {
|
|
236
242
|
const customColorScale = createTileColorScale(item, config, colorScale, index, tileItems.length)
|