@cdc/chart 4.25.8 → 4.25.11
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/.claude/settings.local.json +9 -0
- package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +44236 -40355
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/d.json +382 -0
- package/examples/private/example-2.json +49784 -0
- package/examples/private/f2.json +1 -0
- package/examples/private/f4.json +1577 -0
- package/examples/private/forecast.json +1180 -0
- package/examples/private/lollipop.json +468 -0
- package/examples/private/na.json +913 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/private/test-data.csv +28 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +2 -133
- package/package.json +25 -7
- package/src/CdcChart.tsx +9 -13
- package/src/CdcChartComponent.tsx +403 -92
- package/src/_stories/Chart.Anchors.stories.tsx +2 -2
- package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
- package/src/_stories/Chart.CI.stories.tsx +1 -1
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
- package/src/_stories/Chart.Filters.stories.tsx +2 -2
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +20 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +7 -4
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +59 -60
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
- package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
- package/src/_stories/_mock/combo.json +451 -0
- package/src/_stories/_mock/editor-test-configs.json +376 -0
- package/src/_stories/_mock/editor-test-datasets.json +477 -0
- package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
- package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
- package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
- package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
- package/src/_stories/_mock/pie_config.json +257 -62
- package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
- package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
- package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
- package/src/_stories/_mock/stacked-pattern-test.json +520 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/Annotations/components/findNearestDatum.ts +6 -41
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +161 -22
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
- package/src/components/BarChart/components/BarChart.Vertical.tsx +155 -22
- package/src/components/BarChart/helpers/index.ts +43 -4
- package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
- package/src/components/BarChart/helpers/useBarChart.ts +25 -3
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +563 -229
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +461 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
- package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +175 -27
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +114 -14
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +38 -15
- package/src/components/LinearChart.tsx +96 -84
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
- package/src/components/SmallMultiples/index.ts +2 -0
- package/src/data/initial-state.js +327 -293
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +71 -0
- package/src/helpers/getColorScale.ts +82 -8
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +116 -29
- package/src/index.jsx +0 -2
- package/src/scss/main.scss +13 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +5 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +53 -11
- package/src/types/ChartContext.ts +4 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
- package/src/hooks/useColorPalette.js +0 -76
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, { useContext, useRef, useEffect } from 'react'
|
|
2
|
+
import LinearChart from '../LinearChart'
|
|
3
|
+
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
4
|
+
import ConfigContext from '../../ConfigContext'
|
|
5
|
+
import { ColorScale } from '../../types/ChartContext'
|
|
6
|
+
import cloneConfig from '@cdc/core/helpers/cloneConfig'
|
|
7
|
+
import { getTileDisplayTitle } from '../../helpers/smallMultiplesHelpers'
|
|
8
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
9
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
10
|
+
|
|
11
|
+
interface SmallMultipleTileProps {
|
|
12
|
+
mode: 'by-series' | 'by-column'
|
|
13
|
+
config: ChartConfig
|
|
14
|
+
data: object[]
|
|
15
|
+
tileKey: string
|
|
16
|
+
seriesKey?: string
|
|
17
|
+
tileValue?: string | number
|
|
18
|
+
tileColumn?: string
|
|
19
|
+
customColorScale?: ColorScale
|
|
20
|
+
svgRef?: React.RefObject<SVGAElement>
|
|
21
|
+
parentWidth?: number
|
|
22
|
+
parentHeight?: number
|
|
23
|
+
globalYAxisMax?: number
|
|
24
|
+
globalYAxisMin?: number
|
|
25
|
+
isFirstInRow?: boolean
|
|
26
|
+
onHeightChange?: (tileKey: string, height: number) => void
|
|
27
|
+
onChartRef?: (ref: any) => void
|
|
28
|
+
onHeaderRef?: (ref: HTMLDivElement | null) => void
|
|
29
|
+
onChartHover?: (xAxisValue: any, yCoordinate: number) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
33
|
+
mode,
|
|
34
|
+
config,
|
|
35
|
+
data,
|
|
36
|
+
tileKey,
|
|
37
|
+
seriesKey,
|
|
38
|
+
tileValue,
|
|
39
|
+
tileColumn,
|
|
40
|
+
customColorScale,
|
|
41
|
+
svgRef,
|
|
42
|
+
parentWidth,
|
|
43
|
+
globalYAxisMax,
|
|
44
|
+
globalYAxisMin,
|
|
45
|
+
isFirstInRow,
|
|
46
|
+
onHeightChange,
|
|
47
|
+
onChartRef,
|
|
48
|
+
onHeaderRef,
|
|
49
|
+
onChartHover
|
|
50
|
+
}) => {
|
|
51
|
+
let tileConfig = cloneConfig(config)
|
|
52
|
+
let tileData = data
|
|
53
|
+
|
|
54
|
+
if (mode === 'by-series') {
|
|
55
|
+
// BY-SERIES: One series per tile, all data
|
|
56
|
+
const singleSeries = tileConfig.series.find(s => s.dataKey === seriesKey)
|
|
57
|
+
tileConfig = {
|
|
58
|
+
...tileConfig,
|
|
59
|
+
series: [singleSeries], // Single series
|
|
60
|
+
runtime: {
|
|
61
|
+
...tileConfig.runtime,
|
|
62
|
+
series: tileConfig.runtime.series.filter(s => s.dataKey === seriesKey),
|
|
63
|
+
seriesKeys: [seriesKey],
|
|
64
|
+
seriesLabels: { [seriesKey]: tileConfig.runtime.seriesLabels?.[seriesKey] || seriesKey },
|
|
65
|
+
seriesLabelsAll: [tileConfig.runtime.seriesLabels?.[seriesKey] || seriesKey],
|
|
66
|
+
// Filter area chart specific series keys for proper rendering
|
|
67
|
+
...(tileConfig.runtime.areaSeriesKeys && {
|
|
68
|
+
areaSeriesKeys: tileConfig.runtime.areaSeriesKeys.filter(s => s.dataKey === seriesKey)
|
|
69
|
+
}),
|
|
70
|
+
// Filter line chart specific series keys for proper rendering
|
|
71
|
+
...(tileConfig.runtime.lineSeriesKeys && {
|
|
72
|
+
lineSeriesKeys: tileConfig.runtime.lineSeriesKeys.filter(key => key === seriesKey)
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
showTitle: false, // Individual tiles don't need the main title
|
|
76
|
+
smallMultiples: undefined // Remove smallMultiples to prevent infinite loop
|
|
77
|
+
}
|
|
78
|
+
tileData = data // All data, but only one series will render
|
|
79
|
+
} else if (mode === 'by-column') {
|
|
80
|
+
// BY-COLUMN: All series, filtered data by tile column value
|
|
81
|
+
tileConfig = {
|
|
82
|
+
...tileConfig,
|
|
83
|
+
showTitle: false, // Individual tiles don't need the main title
|
|
84
|
+
smallMultiples: undefined // Remove smallMultiples to prevent infinite loop
|
|
85
|
+
}
|
|
86
|
+
tileData = data.filter(row => row[tileColumn] === tileValue) // Filtered data
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Apply global Y-axis values for consistent scaling if provided
|
|
90
|
+
if (globalYAxisMax !== undefined) {
|
|
91
|
+
tileConfig = {
|
|
92
|
+
...tileConfig,
|
|
93
|
+
yAxis: {
|
|
94
|
+
...tileConfig.yAxis,
|
|
95
|
+
max: globalYAxisMax,
|
|
96
|
+
min: globalYAxisMin
|
|
97
|
+
},
|
|
98
|
+
// Also update runtime properties since LinearChart checks runtime.yAxis.max
|
|
99
|
+
runtime: {
|
|
100
|
+
...tileConfig.runtime,
|
|
101
|
+
yAxis: {
|
|
102
|
+
...tileConfig.runtime?.yAxis,
|
|
103
|
+
max: globalYAxisMax,
|
|
104
|
+
min: globalYAxisMin
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Small multiples-specific modifications
|
|
111
|
+
tileConfig = {
|
|
112
|
+
...tileConfig,
|
|
113
|
+
hideXAxisLabel: !isFirstInRow,
|
|
114
|
+
hideYAxisLabel: !isFirstInRow,
|
|
115
|
+
legend: {
|
|
116
|
+
...tileConfig.legend,
|
|
117
|
+
hide: true
|
|
118
|
+
},
|
|
119
|
+
showAreaUnderLine: config.smallMultiples?.showAreaUnderLine || false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const displayTitle = getTileDisplayTitle(mode, seriesKey, tileValue, tileKey, config)
|
|
123
|
+
|
|
124
|
+
// Get the original context values to merge with our filtered config
|
|
125
|
+
const originalContextValues = useContext(ConfigContext)
|
|
126
|
+
|
|
127
|
+
// Create a tile-specific parentRef for this tile's chart
|
|
128
|
+
const tileParentRef = useRef<HTMLDivElement>(null)
|
|
129
|
+
|
|
130
|
+
// Create a ref for the entire tile (including header) for height measurement
|
|
131
|
+
const fullTileRef = useRef<HTMLDivElement>(null)
|
|
132
|
+
|
|
133
|
+
// Create a ref for the LinearChart instance for tooltip coordination
|
|
134
|
+
const linearChartRef = useRef<any>(null)
|
|
135
|
+
|
|
136
|
+
// Create new context values with our filtered config
|
|
137
|
+
const tileContextValues = {
|
|
138
|
+
...originalContextValues,
|
|
139
|
+
config: tileConfig,
|
|
140
|
+
transformedData: tileData,
|
|
141
|
+
tableData: tileData, // Override with tile-specific filtered data (important for tooltip data lookup)
|
|
142
|
+
parentRef: tileParentRef, // Override with tile-specific parentRef
|
|
143
|
+
updateConfig: () => {}, // Prevent tile hooks from modifying global config
|
|
144
|
+
...(customColorScale && { colorScale: customColorScale }) // Override colorScale if provided
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Use ResizeObserver to capture actual full tile height changes (including header)
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!fullTileRef.current || !onHeightChange) return
|
|
150
|
+
|
|
151
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
152
|
+
entries.forEach(entry => {
|
|
153
|
+
const { height } = entry.contentRect
|
|
154
|
+
onHeightChange(tileKey, height)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
resizeObserver.observe(fullTileRef.current)
|
|
159
|
+
return () => resizeObserver.disconnect()
|
|
160
|
+
}, [tileKey, onHeightChange])
|
|
161
|
+
|
|
162
|
+
// Pass chart ref to parent SmallMultiples component for tooltip coordination
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (onChartRef && linearChartRef.current) {
|
|
165
|
+
onChartRef(linearChartRef.current)
|
|
166
|
+
}
|
|
167
|
+
}, [onChartRef])
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div ref={fullTileRef} className='small-multiple-tile'>
|
|
171
|
+
<div ref={onHeaderRef} className='tile-header'>
|
|
172
|
+
<div className='tile-title'>{displayTitle}</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div ref={tileParentRef} className='tile-chart'>
|
|
175
|
+
<ParentSize
|
|
176
|
+
key={`${mode}-${seriesKey || tileValue}-${config.smallMultiples?.tilesPerRowDesktop}-${
|
|
177
|
+
config.smallMultiples?.tilesPerRowMobile
|
|
178
|
+
}-${parentWidth}`}
|
|
179
|
+
>
|
|
180
|
+
{parent => (
|
|
181
|
+
<ConfigContext.Provider
|
|
182
|
+
value={{
|
|
183
|
+
...tileContextValues,
|
|
184
|
+
dimensions: [parent.width, parent.height], // Override with tile-specific dimensions
|
|
185
|
+
vizViewport: getViewport(parent.width), // Override with tile-specific viewport
|
|
186
|
+
handleSmallMultipleHover: onChartHover
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<LinearChart ref={linearChartRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
190
|
+
</ConfigContext.Provider>
|
|
191
|
+
)}
|
|
192
|
+
</ParentSize>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default SmallMultipleTile
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.small-multiples-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.small-multiples-grid {
|
|
8
|
+
display: grid;
|
|
9
|
+
width: 100%;
|
|
10
|
+
flex: 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.small-multiple-tile {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.tile-header {
|
|
19
|
+
margin-bottom: 0.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tile-title {
|
|
23
|
+
margin: 0;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
text-align: left;
|
|
26
|
+
line-height: 1.3;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.tile-chart {
|
|
30
|
+
width: 100%;
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'
|
|
2
|
+
import SmallMultipleTile from './SmallMultipleTile'
|
|
3
|
+
import ConfigContext from '../../ConfigContext'
|
|
4
|
+
import useReduceData from '../../hooks/useReduceData'
|
|
5
|
+
import useScales from '../../hooks/useScales'
|
|
6
|
+
import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
|
|
7
|
+
import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
|
|
8
|
+
import './SmallMultiples.css'
|
|
9
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
10
|
+
|
|
11
|
+
interface SmallMultiplesProps {
|
|
12
|
+
config: ChartConfig
|
|
13
|
+
data: object[]
|
|
14
|
+
svgRef?: React.RefObject<SVGAElement>
|
|
15
|
+
parentWidth?: number
|
|
16
|
+
parentHeight?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type TileItem = {
|
|
20
|
+
key: string | number
|
|
21
|
+
mode: 'by-series' | 'by-column'
|
|
22
|
+
seriesKey?: string
|
|
23
|
+
tileValue?: any
|
|
24
|
+
tileColumn?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ChartRefWithTooltipMethods = {
|
|
28
|
+
triggerTooltipAtDataValue?: (xAxisValue: any, yCoordinate: number) => void
|
|
29
|
+
hideTooltip?: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TileHeaderRows = Array<Array<HTMLDivElement>>
|
|
33
|
+
|
|
34
|
+
type TileHeaderEntries = Array<[string, HTMLDivElement]>
|
|
35
|
+
|
|
36
|
+
const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, parentWidth, parentHeight }) => {
|
|
37
|
+
const { currentViewport, colorScale, parentRef } = useContext(ConfigContext)
|
|
38
|
+
const { mode, tileColumn, tilesPerRowDesktop, tilesPerRowMobile } = config.smallMultiples || {}
|
|
39
|
+
|
|
40
|
+
const isMobile = isMobileSmallMultiplesViewport(currentViewport)
|
|
41
|
+
const tilesPerRow = isMobile ? tilesPerRowMobile || 1 : tilesPerRowDesktop || 3
|
|
42
|
+
|
|
43
|
+
// Figure out what objects to iterate over based on mode - memoized to prevent recalculation
|
|
44
|
+
const tileItems = useMemo<Array<TileItem>>(() => {
|
|
45
|
+
let items: Array<TileItem> = []
|
|
46
|
+
|
|
47
|
+
if (mode === 'by-series') {
|
|
48
|
+
items = config.series.map(series => ({
|
|
49
|
+
key: series.dataKey,
|
|
50
|
+
mode: 'by-series' as const,
|
|
51
|
+
seriesKey: series.dataKey
|
|
52
|
+
}))
|
|
53
|
+
} else if (mode === 'by-column') {
|
|
54
|
+
const uniqueValues = Array.from(new Set(data.map(row => row[tileColumn])))
|
|
55
|
+
.filter(val => val != null)
|
|
56
|
+
.sort()
|
|
57
|
+
items = uniqueValues.map(value => ({
|
|
58
|
+
key: value,
|
|
59
|
+
mode: 'by-column' as const,
|
|
60
|
+
tileValue: value,
|
|
61
|
+
tileColumn: tileColumn
|
|
62
|
+
}))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Apply tile ordering based on user preference
|
|
66
|
+
return applyTileOrder(
|
|
67
|
+
items,
|
|
68
|
+
config.smallMultiples?.tileOrderType || 'asc',
|
|
69
|
+
config.smallMultiples?.tileOrder,
|
|
70
|
+
config
|
|
71
|
+
)
|
|
72
|
+
}, [
|
|
73
|
+
mode,
|
|
74
|
+
config.series,
|
|
75
|
+
data,
|
|
76
|
+
tileColumn,
|
|
77
|
+
config.smallMultiples?.tileOrderType,
|
|
78
|
+
config.smallMultiples?.tileOrder,
|
|
79
|
+
config.smallMultiples?.tileTitles
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
// Calculate the grid styling based on tiles per row
|
|
83
|
+
const gridGap = isMobile ? '1rem' : '2rem'
|
|
84
|
+
const gridStyle = {
|
|
85
|
+
gridTemplateColumns: `repeat(${tilesPerRow}, 1fr)`,
|
|
86
|
+
gap: gridGap
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [tileHeights, setTileHeights] = useState<Record<string, number>>({})
|
|
90
|
+
|
|
91
|
+
// Refs to all LinearChart instances for tooltip coordination
|
|
92
|
+
const chartRefs = useRef<Record<string, ChartRefWithTooltipMethods>>({})
|
|
93
|
+
|
|
94
|
+
// Refs to all tile header elements for height alignment
|
|
95
|
+
const headerRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
|
96
|
+
|
|
97
|
+
// Create combined data and config for consistent Y-axis calculation
|
|
98
|
+
const combinedDataForYAxis = useMemo(
|
|
99
|
+
() => createCombinedDataForYAxis(config, data, tileItems),
|
|
100
|
+
[config, data, tileItems]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(
|
|
104
|
+
combinedDataForYAxis.config,
|
|
105
|
+
combinedDataForYAxis.data
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const inlineLabel = config.yAxis?.inlineLabel
|
|
109
|
+
const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
|
|
110
|
+
const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
|
|
111
|
+
|
|
112
|
+
const { min, max } = useScales({
|
|
113
|
+
config: combinedDataForYAxis.config,
|
|
114
|
+
data: combinedDataForYAxis.data,
|
|
115
|
+
tableData: combinedDataForYAxis.data,
|
|
116
|
+
minValue,
|
|
117
|
+
maxValue,
|
|
118
|
+
existPositiveValue,
|
|
119
|
+
isAllLine,
|
|
120
|
+
xAxisDataMapped: [],
|
|
121
|
+
xMax: parentWidth,
|
|
122
|
+
yMax: parentHeight,
|
|
123
|
+
needsYAxisAutoPadding,
|
|
124
|
+
currentViewport
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Use consistent Y-axis if the feature is enabled and we have valid values
|
|
128
|
+
const globalYAxisValues = useMemo(() => {
|
|
129
|
+
if (config.smallMultiples?.independentYAxis) return null
|
|
130
|
+
if (typeof min !== 'number' || typeof max !== 'number') return null
|
|
131
|
+
if (combinedDataForYAxis.data.length === 0) return null
|
|
132
|
+
|
|
133
|
+
return { min, max }
|
|
134
|
+
}, [config.smallMultiples?.independentYAxis, min, max, combinedDataForYAxis.data.length])
|
|
135
|
+
|
|
136
|
+
const numberOfRows = useMemo(() => Math.ceil(tileItems.length / tilesPerRow), [tileItems.length, tilesPerRow])
|
|
137
|
+
|
|
138
|
+
// Handle tile height changes from ResizeObserver
|
|
139
|
+
const handleTileHeightChange = useCallback((tileKey: string, height: number) => {
|
|
140
|
+
setTileHeights(prev => ({ ...prev, [tileKey]: height }))
|
|
141
|
+
}, [])
|
|
142
|
+
|
|
143
|
+
// Handle tooltip synchronization across small multiple tiles
|
|
144
|
+
const handleChartHover = useCallback(
|
|
145
|
+
(sourceTileKey: string, xAxisValue: any, yCoordinate: number) => {
|
|
146
|
+
if (!config.smallMultiples?.synchronizedTooltips) return
|
|
147
|
+
|
|
148
|
+
// If xAxisValue is null, it means mouse left the chart - hide all tooltips
|
|
149
|
+
if (xAxisValue === null) {
|
|
150
|
+
Object.entries(chartRefs.current).forEach(([tileKey, chartRef]) => {
|
|
151
|
+
if (tileKey !== sourceTileKey && chartRef?.hideTooltip) {
|
|
152
|
+
chartRef.hideTooltip()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For each OTHER chart in the grid, trigger tooltip at same X-axis data value and exact Y coordinate
|
|
159
|
+
Object.entries(chartRefs.current).forEach(([tileKey, chartRef]) => {
|
|
160
|
+
if (tileKey === sourceTileKey || !chartRef) return
|
|
161
|
+
|
|
162
|
+
if (chartRef.triggerTooltipAtDataValue) {
|
|
163
|
+
chartRef.triggerTooltipAtDataValue(xAxisValue, yCoordinate)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
[config.smallMultiples?.synchronizedTooltips]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Align tile header heights per row
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const headerEntries = Object.entries(headerRefs.current).filter(([_, ref]) => ref) as TileHeaderEntries
|
|
173
|
+
if (headerEntries.length === 0) return
|
|
174
|
+
|
|
175
|
+
// Group headers by row based on their index in tileItems
|
|
176
|
+
const headersByRow: TileHeaderRows = []
|
|
177
|
+
|
|
178
|
+
tileItems.forEach((item, index) => {
|
|
179
|
+
const rowIndex = Math.floor(index / tilesPerRow)
|
|
180
|
+
const header = headerRefs.current[String(item.key)]
|
|
181
|
+
|
|
182
|
+
headersByRow[rowIndex] ||= []
|
|
183
|
+
headersByRow[rowIndex].push(header)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// For each row, find the header with longest text and align others to it
|
|
187
|
+
headersByRow.forEach(rowHeaders => {
|
|
188
|
+
let longestHeader: HTMLDivElement | null = null
|
|
189
|
+
let maxTextLength = 0
|
|
190
|
+
|
|
191
|
+
rowHeaders.forEach(header => {
|
|
192
|
+
const textLength = header.textContent?.length || 0
|
|
193
|
+
if (textLength > maxTextLength) {
|
|
194
|
+
maxTextLength = textLength
|
|
195
|
+
longestHeader = header
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if (!longestHeader) return
|
|
200
|
+
|
|
201
|
+
// Get the height of the longest header in this row
|
|
202
|
+
const targetHeight = longestHeader.offsetHeight
|
|
203
|
+
|
|
204
|
+
// Apply that height to all other headers in this row
|
|
205
|
+
rowHeaders.forEach(header => {
|
|
206
|
+
header.style.minHeight = header !== longestHeader ? `${targetHeight}px` : 'auto'
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
}, [tileItems, tilesPerRow])
|
|
210
|
+
|
|
211
|
+
// Calculate container height from measured tile heights
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (!parentRef.current) return
|
|
214
|
+
|
|
215
|
+
const measuredHeights = Object.values(tileHeights)
|
|
216
|
+
if (measuredHeights.length === 0) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const maxTileHeight = Math.max(...measuredHeights)
|
|
221
|
+
const gapSize = isMobile ? 18 : 36
|
|
222
|
+
const totalGapsHeight = (numberOfRows - 1) * gapSize
|
|
223
|
+
const totalHeight = numberOfRows * maxTileHeight + totalGapsHeight
|
|
224
|
+
|
|
225
|
+
parentRef.current.style.height = `${totalHeight}px`
|
|
226
|
+
}, [tileHeights, numberOfRows, isMobile, parentRef, tilesPerRow])
|
|
227
|
+
|
|
228
|
+
if (tileItems.length === 0) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className='small-multiples-container'>
|
|
234
|
+
<div className='small-multiples-grid' style={gridStyle}>
|
|
235
|
+
{tileItems.map((item, index) => {
|
|
236
|
+
const customColorScale = createTileColorScale(item, config, colorScale, index, tileItems.length)
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<SmallMultipleTile
|
|
240
|
+
key={item.key}
|
|
241
|
+
tileKey={String(item.key)}
|
|
242
|
+
mode={item.mode}
|
|
243
|
+
seriesKey={item.seriesKey}
|
|
244
|
+
tileValue={item.tileValue}
|
|
245
|
+
tileColumn={item.tileColumn}
|
|
246
|
+
customColorScale={customColorScale}
|
|
247
|
+
config={config}
|
|
248
|
+
data={data}
|
|
249
|
+
svgRef={svgRef}
|
|
250
|
+
parentWidth={parentWidth}
|
|
251
|
+
parentHeight={parentHeight}
|
|
252
|
+
globalYAxisMax={globalYAxisValues?.max}
|
|
253
|
+
globalYAxisMin={globalYAxisValues?.min}
|
|
254
|
+
isFirstInRow={index % tilesPerRow === 0}
|
|
255
|
+
onHeightChange={handleTileHeightChange}
|
|
256
|
+
onChartRef={ref => {
|
|
257
|
+
chartRefs.current[String(item.key)] = ref
|
|
258
|
+
}}
|
|
259
|
+
onHeaderRef={ref => {
|
|
260
|
+
headerRefs.current[String(item.key)] = ref
|
|
261
|
+
}}
|
|
262
|
+
onChartHover={(xAxisValue, yCoordinate) => handleChartHover(String(item.key), xAxisValue, yCoordinate)}
|
|
263
|
+
/>
|
|
264
|
+
)
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export default SmallMultiples
|