@cdc/chart 4.25.10 → 4.26.1
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 +44003 -43518
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/feature/pie/planet-pie-example-config.json +48 -2
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/DEV-12100.json +1303 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/data-points.json +228 -0
- package/examples/private/height.json +3915 -0
- package/examples/private/links.json +569 -0
- package/examples/private/na.json +913 -0
- package/examples/private/quadrant.txt +30 -0
- package/examples/private/test-data.csv +28 -0
- package/examples/private/test-forecast.json +5510 -0
- package/examples/private/warming-stripe-test.json +2578 -0
- package/examples/private/warming-stripes.json +4763 -0
- package/examples/tech-adoption-with-links.json +560 -0
- package/index.html +16 -140
- package/package.json +6 -5
- package/preview.html +1616 -0
- package/src/CdcChart.tsx +8 -11
- package/src/CdcChartComponent.tsx +329 -124
- 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.Regions.Categorical.stories.tsx +148 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/Chart.stories.tsx +8 -0
- package/src/_stories/ChartAnnotation.stories.tsx +6 -3
- package/src/_stories/ChartBar.Editor.stories.tsx +3585 -0
- package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
- package/src/_stories/ChartBrush.stories.tsx +50 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +656 -0
- package/src/_stories/ChartEditor.stories.tsx +1 -2
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
- package/src/_stories/_mock/brush_enabled.json +326 -0
- package/src/_stories/_mock/brush_mock.json +2 -69
- 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/horizontal-bars-dynamic-y-axis.json +413 -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 -7
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/Axis/Categorical.Axis.tsx +6 -7
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +181 -27
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
- package/src/components/BarChart/components/BarChart.Vertical.tsx +8 -9
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BarChart/helpers/useBarChart.ts +14 -2
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushSelector.tsx +1258 -0
- package/src/components/Brush/MiniChartPreview.tsx +283 -0
- package/src/components/DeviationBar.jsx +9 -7
- package/src/components/EditorPanel/EditorPanel.tsx +2720 -2586
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +76 -31
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +104 -55
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +427 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +96 -48
- 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 +36 -31
- package/src/components/Forecasting/Forecasting.tsx +139 -21
- package/src/components/Legend/Legend.Component.tsx +16 -9
- package/src/components/Legend/Legend.tsx +3 -2
- package/src/components/Legend/helpers/createFormatLabels.tsx +325 -176
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/Legend/helpers/index.ts +10 -6
- 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 +559 -499
- package/src/components/PairedBarChart.jsx +20 -3
- package/src/components/Regions/components/Regions.tsx +366 -144
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +202 -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/components/WarmingStripes/WarmingStripes.tsx +160 -0
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
- package/src/components/WarmingStripes/index.tsx +3 -0
- package/src/data/initial-state.js +16 -2
- package/src/helpers/buildForecastPaletteOptions.ts +0 -38
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
- package/src/helpers/getColorScale.ts +10 -0
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +26 -14
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/sizeHelpers.ts +0 -20
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useChartHoverAnalytics.tsx +10 -9
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useScales.ts +98 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +91 -25
- package/src/scss/DataTable.scss +0 -4
- package/src/scss/main.scss +18 -83
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +4 -0
- package/src/test/CdcChart.test.jsx +1 -1
- package/src/types/ChartConfig.ts +27 -6
- package/src/types/ChartContext.ts +3 -0
- package/src/types/Label.ts +1 -0
- package/src/utils/analyticsTracking.ts +19 -0
- package/LICENSE +0 -201
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/components/Brush/BrushChart.tsx +0 -128
- package/src/components/Brush/BrushController.tsx +0 -71
- package/src/components/Brush/types.tsx +0 -8
- package/src/components/BrushChart.tsx +0 -223
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
|
@@ -21,21 +21,8 @@ export const useEditorPermissions = () => {
|
|
|
21
21
|
'Pie',
|
|
22
22
|
'Scatter Plot',
|
|
23
23
|
'Spark Line',
|
|
24
|
-
'Sankey'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const headerColors = [
|
|
28
|
-
'theme-blue',
|
|
29
|
-
'theme-purple',
|
|
30
|
-
'theme-brown',
|
|
31
|
-
'theme-teal',
|
|
32
|
-
'theme-pink',
|
|
33
|
-
'theme-orange',
|
|
34
|
-
'theme-slate',
|
|
35
|
-
'theme-indigo',
|
|
36
|
-
'theme-cyan',
|
|
37
|
-
'theme-green',
|
|
38
|
-
'theme-amber'
|
|
24
|
+
'Sankey',
|
|
25
|
+
'Warming Stripes'
|
|
39
26
|
]
|
|
40
27
|
|
|
41
28
|
const visSupportsDateCategoryAxis = () => {
|
|
@@ -77,7 +64,8 @@ export const useEditorPermissions = () => {
|
|
|
77
64
|
'Forest Plot',
|
|
78
65
|
'Spark Line',
|
|
79
66
|
'Sankey',
|
|
80
|
-
'Bump Chart'
|
|
67
|
+
'Bump Chart',
|
|
68
|
+
'Warming Stripes'
|
|
81
69
|
]
|
|
82
70
|
if (disabledCharts.includes(visualizationType)) return false
|
|
83
71
|
return true
|
|
@@ -91,7 +79,8 @@ export const useEditorPermissions = () => {
|
|
|
91
79
|
'Forest Plot',
|
|
92
80
|
'Spark Line',
|
|
93
81
|
'Sankey',
|
|
94
|
-
'Bump Chart'
|
|
82
|
+
'Bump Chart',
|
|
83
|
+
'Warming Stripes'
|
|
95
84
|
]
|
|
96
85
|
if (disabledCharts.includes(visualizationType)) return false
|
|
97
86
|
return true
|
|
@@ -107,6 +96,8 @@ export const useEditorPermissions = () => {
|
|
|
107
96
|
return false
|
|
108
97
|
case 'Sankey':
|
|
109
98
|
return false
|
|
99
|
+
case 'Warming Stripes':
|
|
100
|
+
return true
|
|
110
101
|
default:
|
|
111
102
|
return true
|
|
112
103
|
}
|
|
@@ -154,7 +145,11 @@ export const useEditorPermissions = () => {
|
|
|
154
145
|
}
|
|
155
146
|
const visHasBrushChart = () => {
|
|
156
147
|
if (config.xAxis.type === 'categorical') return false
|
|
157
|
-
|
|
148
|
+
// Allow Line charts, vertical Bar charts (both stacked and grouped), and vertical Area charts
|
|
149
|
+
if (visualizationType === 'Line' && orientation === 'vertical') return true
|
|
150
|
+
if (visualizationType === 'Bar' && orientation === 'vertical') return true
|
|
151
|
+
if (visualizationType === 'Area Chart' && orientation === 'vertical') return true
|
|
152
|
+
return false
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
const visHasBarBorders = () => {
|
|
@@ -167,6 +162,8 @@ export const useEditorPermissions = () => {
|
|
|
167
162
|
|
|
168
163
|
const visHasDataCutoff = () => {
|
|
169
164
|
switch (visualizationType) {
|
|
165
|
+
case 'Warming Stripes':
|
|
166
|
+
return false
|
|
170
167
|
case 'Sankey':
|
|
171
168
|
return false
|
|
172
169
|
case 'Forest Plot':
|
|
@@ -182,7 +179,9 @@ export const useEditorPermissions = () => {
|
|
|
182
179
|
}
|
|
183
180
|
}
|
|
184
181
|
|
|
185
|
-
const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line'].includes(
|
|
182
|
+
const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line', 'Warming Stripes'].includes(
|
|
183
|
+
visualizationType
|
|
184
|
+
)
|
|
186
185
|
const visHasLegendAxisAlign = () => {
|
|
187
186
|
return visualizationType === 'Bar' && visualizationSubType === 'stacked' && config.legend.behavior === 'isolate'
|
|
188
187
|
}
|
|
@@ -191,7 +190,7 @@ export const useEditorPermissions = () => {
|
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
const visSupportsTooltipOpacity = () => {
|
|
194
|
-
const disabledCharts = ['Spark Line', 'Sankey']
|
|
193
|
+
const disabledCharts = ['Spark Line', 'Sankey', 'Warming Stripes']
|
|
195
194
|
if (disabledCharts.includes(visualizationType)) return false
|
|
196
195
|
return true
|
|
197
196
|
}
|
|
@@ -203,7 +202,7 @@ export const useEditorPermissions = () => {
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
const visSupportsSequentialPallete = () => {
|
|
206
|
-
const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
|
|
205
|
+
const disabledCharts = ['Line', 'Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
|
|
207
206
|
if (disabledCharts.includes(visualizationType)) return false
|
|
208
207
|
return true
|
|
209
208
|
}
|
|
@@ -221,19 +220,19 @@ export const useEditorPermissions = () => {
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
const visSupportsDateCategoryAxisLabel = () => {
|
|
224
|
-
const disabledCharts = ['Forest Plot', 'Spark Line', 'Bump Chart']
|
|
223
|
+
const disabledCharts = ['Forest Plot', 'Spark Line', 'Bump Chart', 'Warming Stripes']
|
|
225
224
|
if (disabledCharts.includes(visualizationType)) return false
|
|
226
225
|
return true
|
|
227
226
|
}
|
|
228
227
|
|
|
229
228
|
const visSupportsDateCategoryAxisLine = () => {
|
|
230
|
-
const disabledCharts = ['Forest Plot', 'Spark Line']
|
|
229
|
+
const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
|
|
231
230
|
if (disabledCharts.includes(visualizationType)) return false
|
|
232
231
|
return true
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
const visSupportsDateCategoryAxisTicks = () => {
|
|
236
|
-
const disabledCharts = ['Forest Plot', 'Spark Line']
|
|
235
|
+
const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
|
|
237
236
|
if (disabledCharts.includes(visualizationType)) return false
|
|
238
237
|
return true
|
|
239
238
|
}
|
|
@@ -257,7 +256,7 @@ export const useEditorPermissions = () => {
|
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
const visSupportsRegions = () => {
|
|
260
|
-
const disabledCharts = ['Forest Plot', 'Pie', 'Paired Bar', 'Spark Line', 'Sankey']
|
|
259
|
+
const disabledCharts = ['Forest Plot', 'Pie', 'Paired Bar', 'Spark Line', 'Sankey', 'Warming Stripes']
|
|
261
260
|
if (disabledCharts.includes(visualizationType)) return false
|
|
262
261
|
return true
|
|
263
262
|
}
|
|
@@ -275,14 +274,13 @@ export const useEditorPermissions = () => {
|
|
|
275
274
|
}
|
|
276
275
|
|
|
277
276
|
const visSupportsFilters = () => {
|
|
278
|
-
const disabledCharts = ['Forest Plot', 'Sankey']
|
|
277
|
+
const disabledCharts = ['Forest Plot', 'Sankey', 'Warming Stripes']
|
|
279
278
|
if (disabledCharts.includes(visualizationType)) return false
|
|
280
279
|
return true
|
|
281
280
|
}
|
|
282
281
|
|
|
283
282
|
const visSupportsValueAxisGridLines = () => {
|
|
284
283
|
const disabledCharts = ['Forest Plot']
|
|
285
|
-
if (orientation === 'horizontal') return false
|
|
286
284
|
if (disabledCharts.includes(visualizationType)) return false
|
|
287
285
|
return true
|
|
288
286
|
}
|
|
@@ -316,13 +314,13 @@ export const useEditorPermissions = () => {
|
|
|
316
314
|
}
|
|
317
315
|
|
|
318
316
|
const visSupportsBarThickness = () => {
|
|
319
|
-
const disabledCharts = ['Forest Plot']
|
|
317
|
+
const disabledCharts = ['Forest Plot', 'Warming Stripes']
|
|
320
318
|
if (disabledCharts.includes(visualizationType)) return false
|
|
321
319
|
return true
|
|
322
320
|
}
|
|
323
321
|
|
|
324
322
|
const visSupportsChartHeight = () => {
|
|
325
|
-
const disabledCharts = ['Spark Line']
|
|
323
|
+
const disabledCharts = ['Spark Line', 'Warming Stripes']
|
|
326
324
|
if (disabledCharts.includes(visualizationType)) return false
|
|
327
325
|
return true
|
|
328
326
|
}
|
|
@@ -334,7 +332,7 @@ export const useEditorPermissions = () => {
|
|
|
334
332
|
}
|
|
335
333
|
|
|
336
334
|
const visSupportsLeftValueAxis = () => {
|
|
337
|
-
const disabledCharts = ['Spark Line', 'Sankey']
|
|
335
|
+
const disabledCharts = ['Spark Line', 'Sankey', 'Warming Stripes']
|
|
338
336
|
if (disabledCharts.includes(visualizationType)) return false
|
|
339
337
|
return true
|
|
340
338
|
}
|
|
@@ -349,6 +347,7 @@ export const useEditorPermissions = () => {
|
|
|
349
347
|
const disabledCharts = ['Spark Line', 'Sankey', 'Bump Chart']
|
|
350
348
|
if (disabledCharts.includes(visualizationType)) return false
|
|
351
349
|
if (config.orientation !== 'horizontal') return false
|
|
350
|
+
if (config.orientation === 'horizontal' && visualizationType === 'Bar' && !config.isLollipopChart) return false
|
|
352
351
|
return true
|
|
353
352
|
}
|
|
354
353
|
|
|
@@ -383,6 +382,12 @@ export const useEditorPermissions = () => {
|
|
|
383
382
|
)
|
|
384
383
|
}
|
|
385
384
|
|
|
385
|
+
const visSupportsSmallMultiples = () => {
|
|
386
|
+
const enabledCharts = ['Line', 'Bar', 'Area Chart', 'Combo', 'Box Plot', 'Scatter Plot', 'Warming Stripes']
|
|
387
|
+
if (enabledCharts.includes(visualizationType) && config.orientation !== 'horizontal') return true
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
|
|
386
391
|
const visSupportsYPadding = () => {
|
|
387
392
|
return !config.yAxis.inlineLabel || !config.yAxis.inlineLabel?.includes(' ')
|
|
388
393
|
}
|
|
@@ -411,7 +416,6 @@ export const useEditorPermissions = () => {
|
|
|
411
416
|
|
|
412
417
|
return {
|
|
413
418
|
enabledChartTypes,
|
|
414
|
-
headerColors,
|
|
415
419
|
visCanAnimate,
|
|
416
420
|
visHasAnchors,
|
|
417
421
|
visHasBarBorders,
|
|
@@ -460,6 +464,7 @@ export const useEditorPermissions = () => {
|
|
|
460
464
|
visSupportsValueAxisMax,
|
|
461
465
|
visSupportsValueAxisMin,
|
|
462
466
|
visSupportsDynamicSeries,
|
|
467
|
+
visSupportsSmallMultiples,
|
|
463
468
|
visSupportsYPadding,
|
|
464
469
|
visHasSingleSeriesTooltip,
|
|
465
470
|
visHasCategoricalAxis
|
|
@@ -14,6 +14,102 @@ import { curveMonotoneX } from '@visx/curve'
|
|
|
14
14
|
import { Bar, Area, LinePath } from '@visx/shape'
|
|
15
15
|
import { Group } from '@visx/group'
|
|
16
16
|
|
|
17
|
+
// Helper function to check if a value is numeric/calculable
|
|
18
|
+
const isCalculable = (value: any): boolean => {
|
|
19
|
+
if (value === null || value === undefined || value === '' || value === 'NA') return false
|
|
20
|
+
const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : Number(value)
|
|
21
|
+
return !isNaN(num) && isFinite(num)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Helper function to filter and sort forecast data, splitting into segments at gaps
|
|
25
|
+
const prepareForecastData = (
|
|
26
|
+
data: Record<string, any>[],
|
|
27
|
+
xAxisKey: string,
|
|
28
|
+
highKey: string,
|
|
29
|
+
lowKey: string
|
|
30
|
+
): Record<string, any>[][] => {
|
|
31
|
+
if (!data || data.length === 0) return []
|
|
32
|
+
|
|
33
|
+
// Filter out invalid data points (where confidence intervals are not calculable)
|
|
34
|
+
const validData = data.filter(d => {
|
|
35
|
+
const high = d[highKey]
|
|
36
|
+
const low = d[lowKey]
|
|
37
|
+
return isCalculable(high) && isCalculable(low) && d[xAxisKey]
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (validData.length === 0) return []
|
|
41
|
+
|
|
42
|
+
// Sort by date
|
|
43
|
+
const sortedData = [...validData].sort((a, b) => {
|
|
44
|
+
const dateA = Date.parse(a[xAxisKey])
|
|
45
|
+
const dateB = Date.parse(b[xAxisKey])
|
|
46
|
+
if (isNaN(dateA) || isNaN(dateB)) return 0
|
|
47
|
+
return dateA - dateB
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Split into segments when there are gaps
|
|
51
|
+
// Calculate intervals between consecutive points to detect gaps
|
|
52
|
+
const intervals: number[] = []
|
|
53
|
+
for (let i = 1; i < sortedData.length; i++) {
|
|
54
|
+
const currentDate = Date.parse(sortedData[i][xAxisKey])
|
|
55
|
+
const prevDate = Date.parse(sortedData[i - 1][xAxisKey])
|
|
56
|
+
if (!isNaN(currentDate) && !isNaN(prevDate)) {
|
|
57
|
+
intervals.push(currentDate - prevDate)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Calculate median interval (more robust than average for detecting gaps)
|
|
62
|
+
const medianInterval =
|
|
63
|
+
intervals.length > 0
|
|
64
|
+
? [...intervals].sort((a, b) => a - b)[Math.floor(intervals.length / 2)]
|
|
65
|
+
: 7 * 24 * 60 * 60 * 1000 // Default to 7 days if no intervals
|
|
66
|
+
|
|
67
|
+
// Threshold: gap is more than 2x the median interval, or more than 30 days
|
|
68
|
+
const gapThreshold = Math.max(medianInterval * 2, 30 * 24 * 60 * 60 * 1000)
|
|
69
|
+
|
|
70
|
+
const segments: Record<string, any>[][] = []
|
|
71
|
+
let currentSegment: Record<string, any>[] = []
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < sortedData.length; i++) {
|
|
74
|
+
const current = sortedData[i]
|
|
75
|
+
const prev = sortedData[i - 1]
|
|
76
|
+
|
|
77
|
+
if (i === 0) {
|
|
78
|
+
// First data point starts a new segment
|
|
79
|
+
currentSegment = [current]
|
|
80
|
+
} else {
|
|
81
|
+
const currentDate = Date.parse(current[xAxisKey])
|
|
82
|
+
const prevDate = Date.parse(prev[xAxisKey])
|
|
83
|
+
|
|
84
|
+
if (isNaN(currentDate) || isNaN(prevDate)) {
|
|
85
|
+
// If dates are invalid, continue current segment
|
|
86
|
+
currentSegment.push(current)
|
|
87
|
+
} else {
|
|
88
|
+
const interval = currentDate - prevDate
|
|
89
|
+
const hasGap = interval > gapThreshold
|
|
90
|
+
|
|
91
|
+
if (hasGap) {
|
|
92
|
+
// Save current segment and start a new one
|
|
93
|
+
if (currentSegment.length > 0) {
|
|
94
|
+
segments.push(currentSegment)
|
|
95
|
+
}
|
|
96
|
+
currentSegment = [current]
|
|
97
|
+
} else {
|
|
98
|
+
// Continue current segment
|
|
99
|
+
currentSegment.push(current)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add the last segment
|
|
106
|
+
if (currentSegment.length > 0) {
|
|
107
|
+
segments.push(currentSegment)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return segments.length > 0 ? segments : [sortedData]
|
|
111
|
+
}
|
|
112
|
+
|
|
17
113
|
const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, handleTooltipMouseOff }) => {
|
|
18
114
|
const { transformedData: data, rawData, config, seriesHighlight, parseDate } = useContext(ConfigContext)
|
|
19
115
|
const { xAxis, yAxis, legend, runtime } = config
|
|
@@ -61,7 +157,7 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
61
157
|
return (
|
|
62
158
|
<Group
|
|
63
159
|
className={`forecasting-areas-combo-${index}`}
|
|
64
|
-
key={`forecasting-areas--stage-${replace(stage.key, /
|
|
160
|
+
key={`forecasting-areas--stage-${replace(stage.key, / /g, '—')}-${index}`}
|
|
65
161
|
>
|
|
66
162
|
{group.confidenceIntervals?.map((ciGroup, ciGroupIndex) => {
|
|
67
163
|
const palette = forecastingPalettes[stage.color] || false
|
|
@@ -78,35 +174,57 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
78
174
|
return isReversed ? palette?.[1] || 'transparent' : palette?.[4] || 'transparent'
|
|
79
175
|
}
|
|
80
176
|
|
|
81
|
-
if (ciGroup.high === '' || ciGroup.low === '') return
|
|
177
|
+
if (ciGroup.high === '' || ciGroup.low === '') return null
|
|
178
|
+
|
|
179
|
+
// Prepare data: filter invalid values, sort by date, and split into segments at gaps
|
|
180
|
+
const dataSegments = prepareForecastData(bridgedData, xAxis.dataKey, ciGroup.high, ciGroup.low)
|
|
181
|
+
|
|
82
182
|
return (
|
|
83
183
|
<Group
|
|
84
184
|
key={`forecasting-areas--stage-${replace(
|
|
85
185
|
stage.key,
|
|
86
|
-
/
|
|
186
|
+
/ /g,
|
|
87
187
|
'—'
|
|
88
188
|
)}--group-${stageIndex}-${ciGroupIndex}`}
|
|
89
189
|
>
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
curve={curveMonotoneX}
|
|
93
|
-
data={bridgedData}
|
|
94
|
-
fill={getFill()}
|
|
95
|
-
opacity={transparentArea? 0.1 : 0.5 }
|
|
96
|
-
x={d => xScale(Date.parse(d[xAxis.dataKey]))}
|
|
97
|
-
y0={d => yScale(d[ciGroup.low])}
|
|
98
|
-
y1={d => yScale(d[ciGroup.high])}
|
|
99
|
-
/>
|
|
100
|
-
|
|
101
|
-
{ciGroupIndex === 0 && (
|
|
102
|
-
<>
|
|
190
|
+
{dataSegments.map((segment, segmentIndex) => (
|
|
191
|
+
<Group key={`segment-${segmentIndex}`}>
|
|
103
192
|
{/* prettier-ignore */}
|
|
104
|
-
<
|
|
193
|
+
<Area
|
|
194
|
+
curve={curveMonotoneX}
|
|
195
|
+
data={segment}
|
|
196
|
+
fill={getFill()}
|
|
197
|
+
opacity={transparentArea ? 0.1 : 0.5}
|
|
198
|
+
x={d => xScale(Date.parse(d[xAxis.dataKey]))}
|
|
199
|
+
y0={d => yScale(d[ciGroup.low])}
|
|
200
|
+
y1={d => yScale(d[ciGroup.high])}
|
|
201
|
+
/>
|
|
105
202
|
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
203
|
+
{ciGroupIndex === 0 && (
|
|
204
|
+
<>
|
|
205
|
+
<LinePath
|
|
206
|
+
data={segment}
|
|
207
|
+
x={d => Number(xScale(Date.parse(d[xAxis.dataKey])))}
|
|
208
|
+
y={d => Number(yScale(d[ciGroup.high]))}
|
|
209
|
+
curve={curveMonotoneX}
|
|
210
|
+
stroke={getStroke()}
|
|
211
|
+
strokeWidth={1}
|
|
212
|
+
strokeOpacity={1}
|
|
213
|
+
/>
|
|
214
|
+
|
|
215
|
+
<LinePath
|
|
216
|
+
data={segment}
|
|
217
|
+
x={d => Number(xScale(Date.parse(d[xAxis.dataKey])))}
|
|
218
|
+
y={d => Number(yScale(d[ciGroup.low]))}
|
|
219
|
+
curve={curveMonotoneX}
|
|
220
|
+
stroke={getStroke()}
|
|
221
|
+
strokeWidth={1}
|
|
222
|
+
strokeOpacity={1}
|
|
223
|
+
/>
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</Group>
|
|
227
|
+
))}
|
|
110
228
|
</Group>
|
|
111
229
|
)
|
|
112
230
|
})}
|
|
@@ -23,7 +23,7 @@ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
|
23
23
|
|
|
24
24
|
const LEGEND_PADDING = 36
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface LegendProps {
|
|
27
27
|
colorScale: ColorScale
|
|
28
28
|
config: ChartConfig
|
|
29
29
|
currentViewport: ViewportSize
|
|
@@ -149,9 +149,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
149
149
|
eventType: `chart_legend_item_toggled` as any,
|
|
150
150
|
eventAction: 'keydown',
|
|
151
151
|
eventLabel: interactionLabel,
|
|
152
|
-
specifics:
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
specifics:
|
|
153
|
+
config.visualizationType === 'Bar'
|
|
154
|
+
? `label: ${label.text}, orientation: ${
|
|
155
|
+
config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
|
|
156
|
+
}, mode: ${legend.behavior}`
|
|
157
|
+
: `label: ${label.text}, mode: ${legend.behavior}`
|
|
155
158
|
})
|
|
156
159
|
highlight(label)
|
|
157
160
|
}
|
|
@@ -164,9 +167,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
164
167
|
eventType: `chart_legend_item_toggled` as any,
|
|
165
168
|
eventAction: 'click',
|
|
166
169
|
eventLabel: interactionLabel,
|
|
167
|
-
specifics:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
specifics:
|
|
171
|
+
config.visualizationType === 'Bar'
|
|
172
|
+
? `label: ${label.text}, orientation: ${
|
|
173
|
+
config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
|
|
174
|
+
}, mode: ${legend.behavior}`
|
|
175
|
+
: `label: ${label.text}, mode: ${legend.behavior}`,
|
|
170
176
|
|
|
171
177
|
vizTitle: getVizTitle(config)
|
|
172
178
|
})
|
|
@@ -238,8 +244,9 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
238
244
|
{/* Pattern Legend Items */}
|
|
239
245
|
{config.legend.patterns && Object.keys(config.legend.patterns).length > 0 && (
|
|
240
246
|
<div
|
|
241
|
-
className={`legend-patterns d-flex ${
|
|
242
|
-
|
|
247
|
+
className={`legend-patterns d-flex ${
|
|
248
|
+
['top', 'bottom'].includes(config.legend.position) ? 'flex-row flex-wrap' : 'flex-column'
|
|
249
|
+
}`}
|
|
243
250
|
>
|
|
244
251
|
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
245
252
|
const patternId = `legend-pattern-${key}`
|
|
@@ -18,13 +18,14 @@ const Legend = forwardRef((props, ref) => {
|
|
|
18
18
|
currentViewport,
|
|
19
19
|
dimensions,
|
|
20
20
|
getTextWidth,
|
|
21
|
-
transformedData
|
|
21
|
+
transformedData,
|
|
22
|
+
formatNumber
|
|
22
23
|
} = useContext(ConfigContext)
|
|
23
24
|
if (!config.legend) return null
|
|
24
25
|
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default
|
|
25
26
|
const { interactionLabel } = props
|
|
26
27
|
|
|
27
|
-
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
|
|
28
|
+
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale, formatNumber)
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
31
|
<Fragment>
|