@cdc/chart 4.25.10 → 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/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +36258 -34658
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/na.json +913 -0
- package/examples/private/test-data.csv +28 -0
- package/index.html +2 -121
- package/package.json +4 -4
- package/src/CdcChart.tsx +8 -11
- package/src/CdcChartComponent.tsx +256 -87
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Patterns.stories.tsx +2 -1
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/ChartAnnotation.stories.tsx +6 -3
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +1 -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/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 +4 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +199 -190
- 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 +102 -55
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +139 -21
- package/src/components/Legend/Legend.Component.tsx +16 -9
- package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +36 -13
- package/src/components/LinearChart.tsx +75 -80
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/types/index.ts +1 -1
- 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 +13 -1
- package/src/helpers/buildForecastPaletteOptions.ts +0 -38
- package/src/helpers/getColorScale.ts +10 -0
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +60 -15
- package/src/scss/main.scss +1 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +4 -0
- package/src/types/ChartConfig.ts +24 -6
- package/src/types/ChartContext.ts +3 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { scaleOrdinal } from '@visx/scale'
|
|
2
|
+
import { getColorScale } from './getColorScale'
|
|
3
|
+
import { ColorScale } from '../types/ChartContext'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get filtered data for a specific tile based on its mode
|
|
7
|
+
*/
|
|
8
|
+
export const getTileData = (tileItem, data) => {
|
|
9
|
+
if (tileItem.mode === 'by-series') {
|
|
10
|
+
// BY-SERIES: All data, but will be filtered to single series by config
|
|
11
|
+
return data
|
|
12
|
+
} else if (tileItem.mode === 'by-column') {
|
|
13
|
+
// BY-COLUMN: Filter data by tile column value
|
|
14
|
+
return data.filter(row => row[tileItem.tileColumn] === tileItem.tileValue)
|
|
15
|
+
}
|
|
16
|
+
return data
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get tile-specific config based on its mode
|
|
21
|
+
*/
|
|
22
|
+
export const getTileConfig = (tileItem, config) => {
|
|
23
|
+
if (tileItem.mode === 'by-series') {
|
|
24
|
+
// BY-SERIES: Single series config
|
|
25
|
+
const singleSeries = config.series.find(s => s.dataKey === tileItem.seriesKey)
|
|
26
|
+
return {
|
|
27
|
+
...config,
|
|
28
|
+
series: [singleSeries],
|
|
29
|
+
runtime: {
|
|
30
|
+
...config.runtime,
|
|
31
|
+
series: config.runtime.series.filter(s => s.dataKey === tileItem.seriesKey),
|
|
32
|
+
seriesKeys: [tileItem.seriesKey],
|
|
33
|
+
seriesLabels: {
|
|
34
|
+
[tileItem.seriesKey]: config.runtime.seriesLabels?.[tileItem.seriesKey] || tileItem.seriesKey
|
|
35
|
+
},
|
|
36
|
+
seriesLabelsAll: [config.runtime.seriesLabels?.[tileItem.seriesKey] || tileItem.seriesKey],
|
|
37
|
+
// Filter area chart specific series keys for proper rendering
|
|
38
|
+
...(config.runtime.areaSeriesKeys && {
|
|
39
|
+
areaSeriesKeys: config.runtime.areaSeriesKeys.filter(s => s.dataKey === tileItem.seriesKey)
|
|
40
|
+
}),
|
|
41
|
+
// Filter line chart specific series keys for proper rendering
|
|
42
|
+
...(config.runtime.lineSeriesKeys && {
|
|
43
|
+
lineSeriesKeys: config.runtime.lineSeriesKeys.filter(key => key === tileItem.seriesKey)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (tileItem.mode === 'by-column') {
|
|
48
|
+
// BY-COLUMN: Same config, data will be filtered
|
|
49
|
+
return config
|
|
50
|
+
}
|
|
51
|
+
return config
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create combined data and config for consistent Y-axis calculation across all tiles
|
|
56
|
+
* This combines all tile data into one dataset and creates a unified config
|
|
57
|
+
*/
|
|
58
|
+
export const createCombinedDataForYAxis = (config, data, tileItems) => {
|
|
59
|
+
if (config.smallMultiples?.independentYAxis) {
|
|
60
|
+
return { data: [], config: config }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Combine all tile data into one dataset for Y-axis calculation
|
|
64
|
+
let allTileData = []
|
|
65
|
+
let allSeriesKeys = new Set()
|
|
66
|
+
|
|
67
|
+
tileItems.forEach(item => {
|
|
68
|
+
const tileData = getTileData(item, data)
|
|
69
|
+
const tileConfig = getTileConfig(item, config)
|
|
70
|
+
|
|
71
|
+
if (tileData.length === 0) return
|
|
72
|
+
|
|
73
|
+
// Add this tile's data to the combined dataset
|
|
74
|
+
allTileData = allTileData.concat(tileData)
|
|
75
|
+
|
|
76
|
+
// Collect all series keys
|
|
77
|
+
if (tileConfig.runtime?.seriesKeys) {
|
|
78
|
+
tileConfig.runtime.seriesKeys.forEach(key => allSeriesKeys.add(key))
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Create combined config with all series for Y-axis calculation
|
|
83
|
+
const combinedConfig = {
|
|
84
|
+
...config,
|
|
85
|
+
runtime: {
|
|
86
|
+
...config.runtime,
|
|
87
|
+
seriesKeys: Array.from(allSeriesKeys)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { data: allTileData, config: combinedConfig }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Apply tile ordering to tile items array
|
|
96
|
+
* Sorts tiles according to the user's order preference (ascending, descending, or custom)
|
|
97
|
+
*/
|
|
98
|
+
export const applyTileOrder = (tileItems, orderType, customOrder, config) => {
|
|
99
|
+
if (!orderType) {
|
|
100
|
+
return tileItems
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const getTileKey = item => (item.mode === 'by-series' ? item.seriesKey : item.tileValue)
|
|
104
|
+
|
|
105
|
+
switch (orderType) {
|
|
106
|
+
case 'asc':
|
|
107
|
+
return [...tileItems].sort((a, b) => {
|
|
108
|
+
const titleA = String(getTileDisplayTitle(a.mode, a.seriesKey, a.tileValue, a.key, config)).toLowerCase()
|
|
109
|
+
const titleB = String(getTileDisplayTitle(b.mode, b.seriesKey, b.tileValue, b.key, config)).toLowerCase()
|
|
110
|
+
return titleA.localeCompare(titleB)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
case 'desc':
|
|
114
|
+
return [...tileItems].sort((a, b) => {
|
|
115
|
+
const titleA = String(getTileDisplayTitle(a.mode, a.seriesKey, a.tileValue, a.key, config)).toLowerCase()
|
|
116
|
+
const titleB = String(getTileDisplayTitle(b.mode, b.seriesKey, b.tileValue, b.key, config)).toLowerCase()
|
|
117
|
+
return titleB.localeCompare(titleA)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
case 'custom':
|
|
121
|
+
if (!customOrder || customOrder.length === 0) {
|
|
122
|
+
return tileItems
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sort tiles based on custom order, with unordered items at the end
|
|
126
|
+
return [...tileItems].sort((a, b) => {
|
|
127
|
+
const keyA = getTileKey(a)
|
|
128
|
+
const keyB = getTileKey(b)
|
|
129
|
+
|
|
130
|
+
const orderA = customOrder.indexOf(keyA)
|
|
131
|
+
const orderB = customOrder.indexOf(keyB)
|
|
132
|
+
|
|
133
|
+
// Items not in customOrder go to the end
|
|
134
|
+
const finalOrderA = orderA === -1 ? 999999 : orderA
|
|
135
|
+
const finalOrderB = orderB === -1 ? 999999 : orderB
|
|
136
|
+
|
|
137
|
+
return finalOrderA - finalOrderB
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
return tileItems
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the available tile keys that can be reordered
|
|
147
|
+
* Returns series keys for by-series mode, unique column values for by-column mode
|
|
148
|
+
*/
|
|
149
|
+
export const getTileKeys = (config, data) => {
|
|
150
|
+
if (config.smallMultiples?.mode === 'by-series') {
|
|
151
|
+
return config.series.map(series => series.dataKey)
|
|
152
|
+
} else if (config.smallMultiples?.mode === 'by-column') {
|
|
153
|
+
const tileColumn = config.smallMultiples.tileColumn
|
|
154
|
+
if (!tileColumn) return []
|
|
155
|
+
|
|
156
|
+
return Array.from(new Set(data.map(row => row[tileColumn])))
|
|
157
|
+
.filter(val => val != null)
|
|
158
|
+
.sort()
|
|
159
|
+
}
|
|
160
|
+
return []
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get the display title for a tile based on its mode
|
|
165
|
+
* For by-series: uses series.name or seriesKey
|
|
166
|
+
* For by-column: uses custom title or tileValue
|
|
167
|
+
*/
|
|
168
|
+
export const getTileDisplayTitle = (mode, seriesKey, tileValue, tileKey, config) => {
|
|
169
|
+
if (mode === 'by-series') {
|
|
170
|
+
// For by-series mode: use configured series name, fall back to seriesKey
|
|
171
|
+
const series = config.series?.find(s => s.dataKey === seriesKey)
|
|
172
|
+
return series?.name || seriesKey
|
|
173
|
+
} else if (mode === 'by-column') {
|
|
174
|
+
// For by-column mode: use custom title from editor, fall back to column value
|
|
175
|
+
return config.smallMultiples?.tileTitles?.[tileValue] || tileValue
|
|
176
|
+
}
|
|
177
|
+
return tileKey
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the full color palette from config with exactly the number of colors needed
|
|
182
|
+
* This creates a temporary colorScale with the right number of series to get the needed colors
|
|
183
|
+
*/
|
|
184
|
+
const getFullColorPalette = (config, numberOfTiles) => {
|
|
185
|
+
// Create fake series keys for exactly the number of tiles needed
|
|
186
|
+
const tempSeriesKeys = Array(numberOfTiles)
|
|
187
|
+
.fill(null)
|
|
188
|
+
.map((_, i) => `temp${i + 1}`)
|
|
189
|
+
const tempConfig = {
|
|
190
|
+
...config,
|
|
191
|
+
runtime: {
|
|
192
|
+
...config.runtime,
|
|
193
|
+
seriesKeys: tempSeriesKeys,
|
|
194
|
+
seriesLabelsAll: tempSeriesKeys
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tempColorScale = getColorScale(tempConfig) as ColorScale
|
|
199
|
+
return tempColorScale.range()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create a custom colorScale for a tile based on the color mode
|
|
204
|
+
* This reuses the existing colorScale and extracts/manipulates its palette as needed
|
|
205
|
+
*/
|
|
206
|
+
export const createTileColorScale = (tileItem, config, originalColorScale, tileIndex, numberOfTiles) => {
|
|
207
|
+
const colorMode = config.smallMultiples?.colorMode || 'same'
|
|
208
|
+
|
|
209
|
+
if (colorMode === 'same') {
|
|
210
|
+
if (tileItem.mode === 'by-series') {
|
|
211
|
+
// Same mode + by-series: All tiles use the same color
|
|
212
|
+
const palette = originalColorScale.range() // Extract palette from existing colorScale
|
|
213
|
+
const baseColor = palette[0]
|
|
214
|
+
// Create a ScaleOrdinal that always returns the same color
|
|
215
|
+
return scaleOrdinal({
|
|
216
|
+
domain: originalColorScale.domain(), // Reuse existing domain
|
|
217
|
+
range: [baseColor],
|
|
218
|
+
unknown: baseColor
|
|
219
|
+
})
|
|
220
|
+
} else {
|
|
221
|
+
// Same mode + by-column: Use original colorScale
|
|
222
|
+
return originalColorScale
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
if (tileItem.mode === 'by-series') {
|
|
226
|
+
// Different mode + by-series: Each series gets different color
|
|
227
|
+
return originalColorScale
|
|
228
|
+
} else {
|
|
229
|
+
// Different mode + by-column: Each tile gets a different base color
|
|
230
|
+
// Get exactly the right number of colors for the tiles
|
|
231
|
+
const fullPalette = getFullColorPalette(config, numberOfTiles)
|
|
232
|
+
const baseColor = fullPalette[tileIndex]
|
|
233
|
+
// Create a ScaleOrdinal that returns the tile-specific color for all series
|
|
234
|
+
return scaleOrdinal({
|
|
235
|
+
domain: originalColorScale.domain(), // Reuse existing domain
|
|
236
|
+
range: [baseColor],
|
|
237
|
+
unknown: baseColor
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Pivot data from long format to wide format for DataTable display
|
|
245
|
+
* Transforms data so each unique tileColumn value becomes its own column
|
|
246
|
+
* Works for both regular data and runtimeData
|
|
247
|
+
*
|
|
248
|
+
* Example:
|
|
249
|
+
* From: [{index: "1", value: 100, smell: "sweet"}, {index: "1", value: 50, smell: "bitter"}]
|
|
250
|
+
* To: [{index: "1", sweet: 100, bitter: 50}]
|
|
251
|
+
*
|
|
252
|
+
* @param data - Original data in long format
|
|
253
|
+
* @param tileColumn - Column to pivot on (e.g., "smell")
|
|
254
|
+
* @param valueColumn - Column containing values to pivot (e.g., "value")
|
|
255
|
+
* @param xAxisColumn - X-axis column name (e.g., "index")
|
|
256
|
+
* @param tileValues - Ordered array of tile values (determines column order)
|
|
257
|
+
* @returns Pivoted data in wide format
|
|
258
|
+
*/
|
|
259
|
+
export const pivotDataForDataTable = (data, tileColumn, valueColumn, xAxisColumn, tileValues) => {
|
|
260
|
+
if (!data || !tileColumn || !valueColumn || !xAxisColumn) return []
|
|
261
|
+
|
|
262
|
+
// Group data by x-axis value
|
|
263
|
+
const xAxisGroups = new Map()
|
|
264
|
+
|
|
265
|
+
data.forEach(row => {
|
|
266
|
+
const xAxisKey = String(row[xAxisColumn])
|
|
267
|
+
if (!xAxisGroups.has(xAxisKey)) {
|
|
268
|
+
xAxisGroups.set(xAxisKey, [])
|
|
269
|
+
}
|
|
270
|
+
xAxisGroups.get(xAxisKey).push(row)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Create pivoted rows
|
|
274
|
+
const pivotedData = []
|
|
275
|
+
|
|
276
|
+
xAxisGroups.forEach((rows, xAxisKey) => {
|
|
277
|
+
const pivotedRow = {
|
|
278
|
+
[xAxisColumn]: xAxisKey
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Add tile value columns in the correct order
|
|
282
|
+
tileValues.forEach(tileValue => {
|
|
283
|
+
const columnKey = String(tileValue)
|
|
284
|
+
const matchingRow = rows.find(row => String(row[tileColumn]) === columnKey)
|
|
285
|
+
if (matchingRow) {
|
|
286
|
+
pivotedRow[columnKey] = matchingRow[valueColumn]
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Copy non-value, non-tile columns from first row
|
|
291
|
+
const firstRow = rows[0]
|
|
292
|
+
Object.keys(firstRow).forEach(key => {
|
|
293
|
+
if (key !== tileColumn && key !== valueColumn && key !== xAxisColumn) {
|
|
294
|
+
pivotedRow[key] = firstRow[key]
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
pivotedData.push(pivotedRow)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
return pivotedData
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create a single column config for a tile value
|
|
306
|
+
* Helper function to avoid duplication
|
|
307
|
+
*/
|
|
308
|
+
const createTileColumnConfig = (tileValue, valueColumnConfig, tileTitles) => {
|
|
309
|
+
const columnKey = String(tileValue)
|
|
310
|
+
return {
|
|
311
|
+
...valueColumnConfig,
|
|
312
|
+
name: columnKey,
|
|
313
|
+
label: tileTitles?.[tileValue] || String(tileValue),
|
|
314
|
+
dataTable: true
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create column configurations for pivoted data table
|
|
320
|
+
* Generates one column config for each tile value, copying formatting from the original value column
|
|
321
|
+
* Preserves column order by inserting new columns where the value column was
|
|
322
|
+
*
|
|
323
|
+
* @param originalColumns - Original columns configuration
|
|
324
|
+
* @param valueColumnName - Name of the value column to clone config from
|
|
325
|
+
* @param tileColumnName - Name of the tile column to remove
|
|
326
|
+
* @param tileValues - Array of tile values (becomes new column names)
|
|
327
|
+
* @param tileTitles - Custom titles for columns
|
|
328
|
+
* @returns New columns configuration with xAxis column + one column per tile value
|
|
329
|
+
*/
|
|
330
|
+
export const createPivotedColumns = (originalColumns, valueColumnName, tileColumnName, tileValues, tileTitles) => {
|
|
331
|
+
const newColumns = {}
|
|
332
|
+
let valueColumnConfig = {}
|
|
333
|
+
let addedPivotedColumns = false
|
|
334
|
+
|
|
335
|
+
const hasOriginalColumns = originalColumns && Object.keys(originalColumns).length > 0
|
|
336
|
+
|
|
337
|
+
const addPivotedColumns = () => {
|
|
338
|
+
if (addedPivotedColumns) return
|
|
339
|
+
tileValues.forEach(tileValue => {
|
|
340
|
+
const columnKey = String(tileValue)
|
|
341
|
+
newColumns[columnKey] = createTileColumnConfig(tileValue, valueColumnConfig, tileTitles)
|
|
342
|
+
})
|
|
343
|
+
addedPivotedColumns = true
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (hasOriginalColumns) {
|
|
347
|
+
Object.keys(originalColumns).forEach(key => {
|
|
348
|
+
const column = originalColumns[key]
|
|
349
|
+
|
|
350
|
+
if (column.name === valueColumnName) {
|
|
351
|
+
// Found value column - save its config and replace with pivoted columns
|
|
352
|
+
valueColumnConfig = column
|
|
353
|
+
addPivotedColumns()
|
|
354
|
+
} else if (column.name !== tileColumnName) {
|
|
355
|
+
newColumns[key] = column
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Only added if not previously added
|
|
361
|
+
addPivotedColumns()
|
|
362
|
+
|
|
363
|
+
return newColumns
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get ordered tile values/keys for data table display
|
|
368
|
+
* Works for both by-series and by-column modes
|
|
369
|
+
*/
|
|
370
|
+
const getOrderedTileValues = (config, rawValues, mode) => {
|
|
371
|
+
const { tileOrderType, tileOrder } = config.smallMultiples || {}
|
|
372
|
+
|
|
373
|
+
if (!tileOrderType) {
|
|
374
|
+
return rawValues
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Create minimal tile items for ordering
|
|
378
|
+
const tileItems = rawValues.map(value => ({
|
|
379
|
+
mode,
|
|
380
|
+
...(mode === 'by-series' ? { seriesKey: value } : { tileValue: value }),
|
|
381
|
+
key: value
|
|
382
|
+
}))
|
|
383
|
+
|
|
384
|
+
// Apply ordering
|
|
385
|
+
const orderedItems = applyTileOrder(tileItems, tileOrderType, tileOrder, config)
|
|
386
|
+
|
|
387
|
+
// Extract values back
|
|
388
|
+
return orderedItems.map(item => (mode === 'by-series' ? item.seriesKey : item.tileValue))
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Create runtime series objects for pivoted data
|
|
393
|
+
* Each tile value becomes a series in the data table
|
|
394
|
+
*/
|
|
395
|
+
const createPivotedRuntimeSeries = (tileValues, originalSeries) => {
|
|
396
|
+
return tileValues.map(tileValue => ({
|
|
397
|
+
dataKey: String(tileValue),
|
|
398
|
+
type: originalSeries[0]?.type || 'Bar',
|
|
399
|
+
axis: originalSeries[0]?.axis || 'Left',
|
|
400
|
+
tooltip: true
|
|
401
|
+
}))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Handle by-series mode for data table
|
|
406
|
+
* Reorders series based on tile ordering settings
|
|
407
|
+
*/
|
|
408
|
+
const handleBySeriesMode = (config, columns, runtimeData) => {
|
|
409
|
+
const rawSeriesKeys = config.series?.map(s => s.dataKey) || []
|
|
410
|
+
const orderedSeriesKeys = getOrderedTileValues(config, rawSeriesKeys, 'by-series')
|
|
411
|
+
|
|
412
|
+
if (!config.runtime?.series) {
|
|
413
|
+
return { config, columns, runtimeData }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Reorder runtime series
|
|
417
|
+
const reorderedRuntimeSeries = orderedSeriesKeys
|
|
418
|
+
.map(seriesKey => config.runtime.series.find(s => s.dataKey === seriesKey))
|
|
419
|
+
.filter(Boolean)
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
config: {
|
|
423
|
+
...config,
|
|
424
|
+
runtime: {
|
|
425
|
+
...config.runtime,
|
|
426
|
+
series: reorderedRuntimeSeries
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
columns,
|
|
430
|
+
runtimeData
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Handle by-column mode for data table
|
|
436
|
+
* Two scenarios:
|
|
437
|
+
* 1. Single-series: Pivot data and create columns per tile value
|
|
438
|
+
* 2. Multi-series (stacked/grouped): Add tileColumn to data table, no pivoting
|
|
439
|
+
*/
|
|
440
|
+
const handleByColumnMode = (config, columns, runtimeData) => {
|
|
441
|
+
const xAxisColumn = config.xAxis?.dataKey
|
|
442
|
+
const { tileColumn, tileTitles } = config.smallMultiples
|
|
443
|
+
|
|
444
|
+
// Validate required columns
|
|
445
|
+
if (!xAxisColumn || !tileColumn) {
|
|
446
|
+
return { config, columns, runtimeData }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (config.series && config.series.length > 1) {
|
|
450
|
+
const updatedColumns = {
|
|
451
|
+
...columns
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const tileColumnKey = tileColumn
|
|
455
|
+
if (!updatedColumns[tileColumnKey]) {
|
|
456
|
+
updatedColumns[tileColumnKey] = {
|
|
457
|
+
name: tileColumn,
|
|
458
|
+
label: tileColumn,
|
|
459
|
+
dataTable: true,
|
|
460
|
+
order: 2
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
config: {
|
|
466
|
+
...config,
|
|
467
|
+
columns: updatedColumns
|
|
468
|
+
},
|
|
469
|
+
columns: updatedColumns,
|
|
470
|
+
runtimeData
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const valueColumn = config.series?.[0]?.dataKey
|
|
475
|
+
|
|
476
|
+
if (!valueColumn) {
|
|
477
|
+
return { config, columns, runtimeData }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Get ordered tile values
|
|
481
|
+
const rawTileValues = getTileKeys(config, config.data)
|
|
482
|
+
const orderedTileValues = getOrderedTileValues(config, rawTileValues, 'by-column')
|
|
483
|
+
|
|
484
|
+
// Pivot data and runtimeData
|
|
485
|
+
const pivotedData = pivotDataForDataTable(config.data, tileColumn, valueColumn, xAxisColumn, orderedTileValues)
|
|
486
|
+
const pivotedRuntimeData = Array.isArray(runtimeData)
|
|
487
|
+
? pivotDataForDataTable(runtimeData, tileColumn, valueColumn, xAxisColumn, orderedTileValues)
|
|
488
|
+
: runtimeData
|
|
489
|
+
|
|
490
|
+
// Create pivoted columns and runtime series
|
|
491
|
+
const pivotedColumns = createPivotedColumns(columns, valueColumn, tileColumn, orderedTileValues, tileTitles)
|
|
492
|
+
const pivotedRuntimeSeries = createPivotedRuntimeSeries(orderedTileValues, config.series)
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
config: {
|
|
496
|
+
...config,
|
|
497
|
+
data: pivotedData,
|
|
498
|
+
columns: pivotedColumns,
|
|
499
|
+
runtime: {
|
|
500
|
+
...config.runtime,
|
|
501
|
+
series: pivotedRuntimeSeries,
|
|
502
|
+
seriesKeys: orderedTileValues.map(String)
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
columns: pivotedColumns,
|
|
506
|
+
runtimeData: pivotedRuntimeData
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Prepare data table props for small multiples display
|
|
512
|
+
* Handles both by-column and by-series modes
|
|
513
|
+
*
|
|
514
|
+
* @param config - Chart configuration
|
|
515
|
+
* @param columns - Original columns configuration
|
|
516
|
+
* @param runtimeData - Original runtime data
|
|
517
|
+
* @returns Object with modified config, columns, and runtimeData (or originals if not applicable)
|
|
518
|
+
*/
|
|
519
|
+
export const prepareSmallMultiplesDataTable = (config, columns, runtimeData) => {
|
|
520
|
+
const mode = config.smallMultiples?.mode
|
|
521
|
+
|
|
522
|
+
if (!mode) {
|
|
523
|
+
return { config, columns, runtimeData }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return mode === 'by-series'
|
|
527
|
+
? handleBySeriesMode(config, columns, runtimeData)
|
|
528
|
+
: handleByColumnMode(config, columns, runtimeData)
|
|
529
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useRef, useImperativeHandle, ForwardedRef } from 'react'
|
|
2
|
+
|
|
3
|
+
interface UseProgrammaticTooltipProps {
|
|
4
|
+
svgRef: ForwardedRef<SVGAElement>
|
|
5
|
+
getCoordinateFromXValue: (xAxisValue: any) => number
|
|
6
|
+
config: any
|
|
7
|
+
setPoint: (point: { x: number; y: number }) => void
|
|
8
|
+
setShowHoverLine: (show: boolean) => void
|
|
9
|
+
handleTooltipMouseOver: (event: MouseEvent, additionalChartData?: any) => void
|
|
10
|
+
hideTooltip: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom hook to provide programmatic tooltip control for small multiples synchronization
|
|
15
|
+
* Handles SVG ref management and exposes methods to trigger tooltips from external sources
|
|
16
|
+
*/
|
|
17
|
+
export const useProgrammaticTooltip = ({
|
|
18
|
+
svgRef,
|
|
19
|
+
getCoordinateFromXValue,
|
|
20
|
+
config,
|
|
21
|
+
setPoint,
|
|
22
|
+
setShowHoverLine,
|
|
23
|
+
handleTooltipMouseOver,
|
|
24
|
+
hideTooltip
|
|
25
|
+
}: UseProgrammaticTooltipProps) => {
|
|
26
|
+
// Internal SVG ref for DOM manipulation
|
|
27
|
+
const internalSvgRef = useRef<SVGSVGElement>(null)
|
|
28
|
+
|
|
29
|
+
// Expose programmatic tooltip methods via ref
|
|
30
|
+
useImperativeHandle(
|
|
31
|
+
svgRef,
|
|
32
|
+
() => {
|
|
33
|
+
// Return a proxy that combines SVG element access with our custom methods
|
|
34
|
+
const svgElement = internalSvgRef.current
|
|
35
|
+
|
|
36
|
+
// If no SVG element yet, return minimal interface
|
|
37
|
+
if (!svgElement) {
|
|
38
|
+
return {
|
|
39
|
+
triggerTooltipAtDataValue: () => {},
|
|
40
|
+
hideTooltip: () => {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create object that has both SVG methods and our custom methods
|
|
45
|
+
return Object.assign(svgElement, {
|
|
46
|
+
/**
|
|
47
|
+
* Trigger tooltip at specific data value and Y coordinate (data-centric approach)
|
|
48
|
+
* This ensures pixel-perfect alignment across small multiple tiles
|
|
49
|
+
* @param {any} xAxisValue - X-axis data value (date, number, or category)
|
|
50
|
+
* @param {number} yCoordinate - Exact Y coordinate to use
|
|
51
|
+
*/
|
|
52
|
+
triggerTooltipAtDataValue: (xAxisValue: any, yCoordinate: number) => {
|
|
53
|
+
const pixelX = getCoordinateFromXValue(xAxisValue)
|
|
54
|
+
const adjustedX = pixelX + Number(config.yAxis.size || 0)
|
|
55
|
+
|
|
56
|
+
const svgRect = internalSvgRef.current!.getBoundingClientRect()
|
|
57
|
+
|
|
58
|
+
const syntheticEvent = new MouseEvent('mousemove', {
|
|
59
|
+
bubbles: true,
|
|
60
|
+
cancelable: true,
|
|
61
|
+
clientX: svgRect.left + adjustedX,
|
|
62
|
+
clientY: svgRect.top + yCoordinate,
|
|
63
|
+
screenX: window.screenX + svgRect.left + adjustedX,
|
|
64
|
+
screenY: window.screenY + svgRect.top + yCoordinate,
|
|
65
|
+
view: window
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(syntheticEvent, 'currentTarget', {
|
|
69
|
+
value: internalSvgRef.current,
|
|
70
|
+
writable: false
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
Object.defineProperty(syntheticEvent, 'target', {
|
|
74
|
+
value: internalSvgRef.current,
|
|
75
|
+
writable: false
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
setPoint({ x: adjustedX, y: yCoordinate })
|
|
79
|
+
setShowHoverLine(true)
|
|
80
|
+
handleTooltipMouseOver(syntheticEvent, null)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hide any currently displayed tooltip and hover line
|
|
85
|
+
*/
|
|
86
|
+
hideTooltip: () => {
|
|
87
|
+
hideTooltip()
|
|
88
|
+
setShowHoverLine(false)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
},
|
|
92
|
+
[getCoordinateFromXValue, config.yAxis.size, setPoint, setShowHoverLine, handleTooltipMouseOver, hideTooltip]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return internalSvgRef
|
|
96
|
+
}
|