@cdc/chart 4.25.11 → 4.26.2
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.local.md +79 -0
- package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
- package/dist/cdcchart.js +51401 -50814
- package/examples/default.json +378 -0
- package/examples/feature/__data__/horizon-chart-data.json +373 -0
- package/examples/feature/annotations/index.json +3 -6
- package/examples/feature/horizon/horizon-chart.json +395 -0
- package/examples/feature/pie/planet-pie-example-config.json +48 -2
- package/examples/line-chart-states.json +1085 -0
- package/examples/private/123.json +694 -0
- package/examples/private/DEV-12100.json +1303 -0
- package/examples/private/anchor-issue.json +4094 -0
- package/examples/private/backwards-slider.json +10430 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/data-points.json +228 -0
- package/examples/private/georgia.csv +160 -0
- package/examples/private/height.json +3915 -0
- package/examples/private/links.json +569 -0
- package/examples/private/quadrant.txt +30 -0
- package/examples/private/test-forecast.json +5510 -0
- package/examples/private/timeline-data.json +1 -0
- package/examples/private/timeline.json +389 -0
- package/examples/private/warming-stripe-test.json +2578 -0
- package/examples/private/warming-stripes.json +4763 -0
- package/examples/radar-chart-simple.json +133 -0
- package/examples/radar-chart.json +148 -0
- package/examples/tech-adoption-with-links.json +560 -0
- package/index.html +1 -36
- package/package.json +59 -60
- package/src/CdcChartComponent.tsx +206 -89
- package/src/_stories/Chart.Anchors.stories.tsx +10 -0
- package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
- package/src/_stories/Chart.CI.stories.tsx +13 -0
- package/src/_stories/Chart.Combo.stories.tsx +17 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
- package/src/_stories/Chart.Filters.stories.tsx +4 -0
- package/src/_stories/Chart.Forecast.stories.tsx +4 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
- package/src/_stories/Chart.Patterns.stories.tsx +4 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
- package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
- package/src/_stories/Chart.stories.tsx +45 -0
- package/src/_stories/Chart.tooltip.stories.tsx +7 -0
- package/src/_stories/ChartAnnotation.stories.tsx +10 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
- package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
- package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
- package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
- package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
- package/src/_stories/ChartBrush.stories.tsx +57 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
- package/src/_stories/ChartEditor.stories.tsx +7 -0
- package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
- package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
- package/src/_stories/_mock/brush_continuous.json +86 -0
- package/src/_stories/_mock/brush_date_large.json +176 -0
- package/src/_stories/_mock/brush_enabled.json +326 -0
- package/src/_stories/_mock/brush_mock.json +2 -69
- package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
- package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
- package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
- package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
- package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
- package/src/components/Annotations/components/AnnotationList.tsx +5 -4
- package/src/components/Annotations/components/findNearestDatum.ts +75 -85
- package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
- package/src/components/Axis/BottomAxis.tsx +270 -0
- package/src/components/Axis/Categorical.Axis.tsx +6 -7
- package/src/components/Axis/LeftAxis.tsx +404 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
- package/src/components/Axis/PairedBarAxis.tsx +186 -0
- package/src/components/Axis/README.md +94 -0
- package/src/components/Axis/RightAxis.tsx +108 -0
- package/src/components/Axis/axis.constants.ts +21 -0
- package/src/components/Axis/index.ts +7 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
- 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 +6 -8
- package/src/components/BarChart/components/BarChart.tsx +7 -1
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BarChart/helpers/useBarChart.ts +14 -2
- package/src/components/Brush/BrushSelector.tsx +1390 -0
- package/src/components/Brush/MiniChartPreview.tsx +400 -0
- package/src/components/DeviationBar.jsx +9 -7
- package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
- package/src/components/HorizonChart/HorizonChart.tsx +131 -0
- package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
- package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
- package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
- package/src/components/HorizonChart/index.tsx +3 -0
- package/src/components/Legend/Legend.Component.tsx +52 -4
- package/src/components/Legend/Legend.tsx +4 -3
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
- package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
- package/src/components/Legend/helpers/index.ts +10 -6
- package/src/components/LineChart/helpers/README.md +292 -0
- package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
- package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
- package/src/components/LineChart/index.tsx +44 -8
- package/src/components/LinearChart/README.md +109 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
- package/src/components/LinearChart/linearChart.constants.ts +84 -0
- package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
- package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
- package/src/components/LinearChart.tsx +338 -1082
- package/src/components/PairedBarChart.jsx +20 -3
- package/src/components/PieChart/PieChart.tsx +1 -1
- package/src/components/RadarChart/RadarAxis.tsx +78 -0
- package/src/components/RadarChart/RadarChart.tsx +298 -0
- package/src/components/RadarChart/RadarGrid.tsx +64 -0
- package/src/components/RadarChart/RadarPolygon.tsx +91 -0
- package/src/components/RadarChart/helpers.ts +83 -0
- package/src/components/RadarChart/index.tsx +3 -0
- package/src/components/Regions/components/Regions.tsx +365 -122
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
- package/src/components/WarmingStripes/WarmingStripes.tsx +230 -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 +17 -2
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/getMinMax.ts +12 -7
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/helpers/sizeHelpers.ts +0 -20
- package/src/helpers/smallMultiplesHelpers.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +10 -9
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useScales.ts +18 -1
- package/src/hooks/useTooltip.tsx +34 -10
- package/src/scss/DataTable.scss +0 -4
- package/src/scss/main.scss +22 -3
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/test/CdcChart.test.jsx +1 -1
- package/src/types/ChartConfig.ts +21 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/src/types/Label.ts +1 -0
- package/src/utils/analyticsTracking.ts +19 -0
- package/LICENSE +0 -201
- package/src/components/Annotations/components/helpers/index.tsx +0 -46
- 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
|
@@ -16,16 +16,19 @@ export const useEditorPermissions = () => {
|
|
|
16
16
|
'Deviation Bar',
|
|
17
17
|
'Forecasting',
|
|
18
18
|
// 'Forest Plot',
|
|
19
|
+
'Horizon Chart',
|
|
19
20
|
'Line',
|
|
20
21
|
'Paired Bar',
|
|
21
22
|
'Pie',
|
|
23
|
+
'Radar',
|
|
22
24
|
'Scatter Plot',
|
|
23
25
|
'Spark Line',
|
|
24
|
-
'Sankey'
|
|
26
|
+
'Sankey',
|
|
27
|
+
'Warming Stripes'
|
|
25
28
|
]
|
|
26
29
|
|
|
27
30
|
const visSupportsDateCategoryAxis = () => {
|
|
28
|
-
const disabledCharts = ['Forest Plot', 'Sankey']
|
|
31
|
+
const disabledCharts = ['Forest Plot', 'Radar', 'Sankey']
|
|
29
32
|
if (disabledCharts.includes(visualizationType)) return false
|
|
30
33
|
return true
|
|
31
34
|
}
|
|
@@ -54,16 +57,31 @@ export const useEditorPermissions = () => {
|
|
|
54
57
|
return true
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
const visSupportsClickingLegend = () => {
|
|
61
|
+
const disabledCharts = ['Horizon Chart']
|
|
62
|
+
if (disabledCharts.includes(visualizationType)) return false
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const visSupportsDataAnnotations = () => {
|
|
67
|
+
const enabledCharts = ['Line', 'Bar', 'Combo', 'Area Chart', 'Forecasting']
|
|
68
|
+
if (enabledCharts.includes(visualizationType) && config.orientation !== 'horizontal') return true
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
const visHasLabelOnData = () => {
|
|
58
73
|
const disabledCharts = [
|
|
59
74
|
'Area Chart',
|
|
60
75
|
'Box Plot',
|
|
76
|
+
'Bump Chart',
|
|
77
|
+
'Forest Plot',
|
|
78
|
+
'Horizon Chart',
|
|
61
79
|
'Pie',
|
|
80
|
+
'Radar',
|
|
81
|
+
'Sankey',
|
|
62
82
|
'Scatter Plot',
|
|
63
|
-
'Forest Plot',
|
|
64
83
|
'Spark Line',
|
|
65
|
-
'
|
|
66
|
-
'Bump Chart'
|
|
84
|
+
'Warming Stripes'
|
|
67
85
|
]
|
|
68
86
|
if (disabledCharts.includes(visualizationType)) return false
|
|
69
87
|
return true
|
|
@@ -72,12 +90,14 @@ export const useEditorPermissions = () => {
|
|
|
72
90
|
const visCanAnimate = () => {
|
|
73
91
|
const disabledCharts = [
|
|
74
92
|
'Area Chart',
|
|
75
|
-
'Scatter Plot',
|
|
76
93
|
'Box Plot',
|
|
94
|
+
'Bump Chart',
|
|
77
95
|
'Forest Plot',
|
|
78
|
-
'
|
|
96
|
+
'Radar',
|
|
79
97
|
'Sankey',
|
|
80
|
-
'
|
|
98
|
+
'Scatter Plot',
|
|
99
|
+
'Spark Line',
|
|
100
|
+
'Warming Stripes'
|
|
81
101
|
]
|
|
82
102
|
if (disabledCharts.includes(visualizationType)) return false
|
|
83
103
|
return true
|
|
@@ -93,6 +113,8 @@ export const useEditorPermissions = () => {
|
|
|
93
113
|
return false
|
|
94
114
|
case 'Sankey':
|
|
95
115
|
return false
|
|
116
|
+
case 'Warming Stripes':
|
|
117
|
+
return true
|
|
96
118
|
default:
|
|
97
119
|
return true
|
|
98
120
|
}
|
|
@@ -140,7 +162,12 @@ export const useEditorPermissions = () => {
|
|
|
140
162
|
}
|
|
141
163
|
const visHasBrushChart = () => {
|
|
142
164
|
if (config.xAxis.type === 'categorical') return false
|
|
143
|
-
|
|
165
|
+
// Allow Line charts, vertical Bar charts (both stacked and grouped), vertical Area charts, and Combo charts
|
|
166
|
+
if (visualizationType === 'Line' && orientation === 'vertical') return true
|
|
167
|
+
if (visualizationType === 'Bar' && orientation === 'vertical') return true
|
|
168
|
+
if (visualizationType === 'Area Chart' && orientation === 'vertical') return true
|
|
169
|
+
if (visualizationType === 'Combo' && orientation === 'vertical') return true
|
|
170
|
+
return false
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
const visHasBarBorders = () => {
|
|
@@ -153,6 +180,8 @@ export const useEditorPermissions = () => {
|
|
|
153
180
|
|
|
154
181
|
const visHasDataCutoff = () => {
|
|
155
182
|
switch (visualizationType) {
|
|
183
|
+
case 'Warming Stripes':
|
|
184
|
+
return false
|
|
156
185
|
case 'Sankey':
|
|
157
186
|
return false
|
|
158
187
|
case 'Forest Plot':
|
|
@@ -168,7 +197,9 @@ export const useEditorPermissions = () => {
|
|
|
168
197
|
}
|
|
169
198
|
}
|
|
170
199
|
|
|
171
|
-
const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line'].includes(
|
|
200
|
+
const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line', 'Warming Stripes'].includes(
|
|
201
|
+
visualizationType
|
|
202
|
+
)
|
|
172
203
|
const visHasLegendAxisAlign = () => {
|
|
173
204
|
return visualizationType === 'Bar' && visualizationSubType === 'stacked' && config.legend.behavior === 'isolate'
|
|
174
205
|
}
|
|
@@ -177,7 +208,7 @@ export const useEditorPermissions = () => {
|
|
|
177
208
|
}
|
|
178
209
|
|
|
179
210
|
const visSupportsTooltipOpacity = () => {
|
|
180
|
-
const disabledCharts = ['Spark Line', 'Sankey']
|
|
211
|
+
const disabledCharts = ['Spark Line', 'Sankey', 'Warming Stripes']
|
|
181
212
|
if (disabledCharts.includes(visualizationType)) return false
|
|
182
213
|
return true
|
|
183
214
|
}
|
|
@@ -189,7 +220,7 @@ export const useEditorPermissions = () => {
|
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
const visSupportsSequentialPallete = () => {
|
|
192
|
-
const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
|
|
223
|
+
const disabledCharts = ['Line', 'Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
|
|
193
224
|
if (disabledCharts.includes(visualizationType)) return false
|
|
194
225
|
return true
|
|
195
226
|
}
|
|
@@ -207,19 +238,19 @@ export const useEditorPermissions = () => {
|
|
|
207
238
|
}
|
|
208
239
|
|
|
209
240
|
const visSupportsDateCategoryAxisLabel = () => {
|
|
210
|
-
const disabledCharts = ['Forest Plot', 'Spark Line', 'Bump Chart']
|
|
241
|
+
const disabledCharts = ['Forest Plot', 'Spark Line', 'Bump Chart', 'Warming Stripes']
|
|
211
242
|
if (disabledCharts.includes(visualizationType)) return false
|
|
212
243
|
return true
|
|
213
244
|
}
|
|
214
245
|
|
|
215
246
|
const visSupportsDateCategoryAxisLine = () => {
|
|
216
|
-
const disabledCharts = ['Forest Plot', 'Spark Line']
|
|
247
|
+
const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
|
|
217
248
|
if (disabledCharts.includes(visualizationType)) return false
|
|
218
249
|
return true
|
|
219
250
|
}
|
|
220
251
|
|
|
221
252
|
const visSupportsDateCategoryAxisTicks = () => {
|
|
222
|
-
const disabledCharts = ['Forest Plot', 'Spark Line']
|
|
253
|
+
const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
|
|
223
254
|
if (disabledCharts.includes(visualizationType)) return false
|
|
224
255
|
return true
|
|
225
256
|
}
|
|
@@ -243,7 +274,16 @@ export const useEditorPermissions = () => {
|
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
const visSupportsRegions = () => {
|
|
246
|
-
const disabledCharts = [
|
|
277
|
+
const disabledCharts = [
|
|
278
|
+
'Forest Plot',
|
|
279
|
+
'Horizon Chart',
|
|
280
|
+
'Pie',
|
|
281
|
+
'Paired Bar',
|
|
282
|
+
'Radar',
|
|
283
|
+
'Spark Line',
|
|
284
|
+
'Sankey',
|
|
285
|
+
'Warming Stripes'
|
|
286
|
+
]
|
|
247
287
|
if (disabledCharts.includes(visualizationType)) return false
|
|
248
288
|
return true
|
|
249
289
|
}
|
|
@@ -261,14 +301,13 @@ export const useEditorPermissions = () => {
|
|
|
261
301
|
}
|
|
262
302
|
|
|
263
303
|
const visSupportsFilters = () => {
|
|
264
|
-
const disabledCharts = ['Forest Plot', 'Sankey']
|
|
304
|
+
const disabledCharts = ['Forest Plot', 'Sankey', 'Warming Stripes']
|
|
265
305
|
if (disabledCharts.includes(visualizationType)) return false
|
|
266
306
|
return true
|
|
267
307
|
}
|
|
268
308
|
|
|
269
309
|
const visSupportsValueAxisGridLines = () => {
|
|
270
310
|
const disabledCharts = ['Forest Plot']
|
|
271
|
-
if (orientation === 'horizontal') return false
|
|
272
311
|
if (disabledCharts.includes(visualizationType)) return false
|
|
273
312
|
return true
|
|
274
313
|
}
|
|
@@ -302,25 +341,25 @@ export const useEditorPermissions = () => {
|
|
|
302
341
|
}
|
|
303
342
|
|
|
304
343
|
const visSupportsBarThickness = () => {
|
|
305
|
-
const disabledCharts = ['Forest Plot']
|
|
344
|
+
const disabledCharts = ['Forest Plot', 'Warming Stripes']
|
|
306
345
|
if (disabledCharts.includes(visualizationType)) return false
|
|
307
346
|
return true
|
|
308
347
|
}
|
|
309
348
|
|
|
310
349
|
const visSupportsChartHeight = () => {
|
|
311
|
-
const disabledCharts = ['Spark Line']
|
|
350
|
+
const disabledCharts = ['Spark Line', 'Warming Stripes']
|
|
312
351
|
if (disabledCharts.includes(visualizationType)) return false
|
|
313
352
|
return true
|
|
314
353
|
}
|
|
315
354
|
const visSupportsMobileChartHeight = () => {
|
|
316
355
|
// TODO: this is a soft release. Support should eventually match visSupportsChartHeight
|
|
317
|
-
const enabledCharts = ['Bar', 'Line', 'Combo', 'Area Chart']
|
|
356
|
+
const enabledCharts = ['Bar', 'Line', 'Combo', 'Area Chart', 'Radar']
|
|
318
357
|
if (enabledCharts.includes(visualizationType)) return true
|
|
319
358
|
return false
|
|
320
359
|
}
|
|
321
360
|
|
|
322
361
|
const visSupportsLeftValueAxis = () => {
|
|
323
|
-
const disabledCharts = ['Spark Line', 'Sankey']
|
|
362
|
+
const disabledCharts = ['Radar', 'Spark Line', 'Sankey', 'Warming Stripes']
|
|
324
363
|
if (disabledCharts.includes(visualizationType)) return false
|
|
325
364
|
return true
|
|
326
365
|
}
|
|
@@ -335,6 +374,7 @@ export const useEditorPermissions = () => {
|
|
|
335
374
|
const disabledCharts = ['Spark Line', 'Sankey', 'Bump Chart']
|
|
336
375
|
if (disabledCharts.includes(visualizationType)) return false
|
|
337
376
|
if (config.orientation !== 'horizontal') return false
|
|
377
|
+
if (config.orientation === 'horizontal' && visualizationType === 'Bar' && !config.isLollipopChart) return false
|
|
338
378
|
return true
|
|
339
379
|
}
|
|
340
380
|
|
|
@@ -370,8 +410,8 @@ export const useEditorPermissions = () => {
|
|
|
370
410
|
}
|
|
371
411
|
|
|
372
412
|
const visSupportsSmallMultiples = () => {
|
|
373
|
-
const enabledCharts = ['Line', 'Bar', 'Area Chart', 'Combo', 'Box Plot', 'Scatter Plot']
|
|
374
|
-
if (enabledCharts.includes(visualizationType)) return true
|
|
413
|
+
const enabledCharts = ['Line', 'Bar', 'Area Chart', 'Combo', 'Box Plot', 'Scatter Plot', 'Warming Stripes']
|
|
414
|
+
if (enabledCharts.includes(visualizationType) && config.orientation !== 'horizontal') return true
|
|
375
415
|
return false
|
|
376
416
|
}
|
|
377
417
|
|
|
@@ -404,56 +444,58 @@ export const useEditorPermissions = () => {
|
|
|
404
444
|
return {
|
|
405
445
|
enabledChartTypes,
|
|
406
446
|
visCanAnimate,
|
|
447
|
+
visHasaAdditionalLabelsOnBars,
|
|
407
448
|
visHasAnchors,
|
|
408
449
|
visHasBarBorders,
|
|
450
|
+
visHasBrushChart,
|
|
451
|
+
visHasCategoricalAxis,
|
|
409
452
|
visHasDataCutoff,
|
|
410
|
-
visHasLabelOnData,
|
|
411
453
|
visHasDataSuppression,
|
|
454
|
+
visHasLabelOnData,
|
|
412
455
|
visHasLegend,
|
|
413
456
|
visHasLegendAxisAlign,
|
|
414
457
|
visHasLegendColorCategory,
|
|
415
|
-
visHasBrushChart,
|
|
416
458
|
visHasNumbersOnBars,
|
|
417
|
-
|
|
459
|
+
visHasSelectableLegendValues,
|
|
460
|
+
visHasSingleSeriesTooltip,
|
|
418
461
|
visSupportsBarSpace,
|
|
419
462
|
visSupportsBarThickness,
|
|
420
463
|
visSupportsChartHeight,
|
|
421
|
-
|
|
464
|
+
visSupportsClickingLegend,
|
|
465
|
+
visSupportsDataAnnotations,
|
|
422
466
|
visSupportsDateCategoryAxis,
|
|
423
|
-
visSupportsDateCategoryAxisMin,
|
|
424
|
-
visSupportsDateCategoryAxisMax,
|
|
425
467
|
visSupportsDateCategoryAxisLabel,
|
|
426
468
|
visSupportsDateCategoryAxisLine,
|
|
469
|
+
visSupportsDateCategoryAxisMax,
|
|
470
|
+
visSupportsDateCategoryAxisMin,
|
|
471
|
+
visSupportsDateCategoryAxisPadding,
|
|
427
472
|
visSupportsDateCategoryAxisTicks,
|
|
428
473
|
visSupportsDateCategoryHeight,
|
|
429
474
|
visSupportsDateCategoryNumTicks,
|
|
430
475
|
visSupportsDateCategoryTickRotation,
|
|
431
|
-
|
|
476
|
+
visSupportsDynamicSeries,
|
|
432
477
|
visSupportsFilters,
|
|
433
478
|
visSupportsFootnotes,
|
|
434
479
|
visSupportsLeftValueAxis,
|
|
480
|
+
visSupportsMobileChartHeight,
|
|
435
481
|
visSupportsNonSequentialPallete,
|
|
436
482
|
visSupportsPreliminaryData,
|
|
437
483
|
visSupportsRankByValue,
|
|
484
|
+
visSupportsReactTooltip,
|
|
438
485
|
visSupportsRegions,
|
|
439
486
|
visSupportsResponsiveTicks,
|
|
440
487
|
visSupportsReverseColorPalette,
|
|
441
488
|
visSupportsSequentialPallete,
|
|
489
|
+
visSupportsSmallMultiples,
|
|
442
490
|
visSupportsSuperTitle,
|
|
443
491
|
visSupportsTooltipLines,
|
|
444
|
-
visHasSelectableLegendValues,
|
|
445
492
|
visSupportsTooltipOpacity,
|
|
446
493
|
visSupportsValueAxisGridLines,
|
|
447
494
|
visSupportsValueAxisLabels,
|
|
448
495
|
visSupportsValueAxisLine,
|
|
449
|
-
visSupportsValueAxisTicks,
|
|
450
|
-
visSupportsReactTooltip,
|
|
451
496
|
visSupportsValueAxisMax,
|
|
452
497
|
visSupportsValueAxisMin,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
visSupportsYPadding,
|
|
456
|
-
visHasSingleSeriesTooltip,
|
|
457
|
-
visHasCategoricalAxis
|
|
498
|
+
visSupportsValueAxisTicks,
|
|
499
|
+
visSupportsYPadding
|
|
458
500
|
}
|
|
459
501
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useContext, memo, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
// cdc
|
|
4
|
+
import ConfigContext from '../../ConfigContext'
|
|
5
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
6
|
+
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
7
|
+
|
|
8
|
+
// visx
|
|
9
|
+
import { Bar } from '@visx/shape'
|
|
10
|
+
import { Group } from '@visx/group'
|
|
11
|
+
|
|
12
|
+
// components
|
|
13
|
+
import HorizonBand from './components/HorizonBand'
|
|
14
|
+
|
|
15
|
+
// helpers
|
|
16
|
+
import { calculateHorizonBands } from './helpers/calculateHorizonBands'
|
|
17
|
+
|
|
18
|
+
// types
|
|
19
|
+
import { HORIZON_DEFAULTS } from '../../types/Horizon'
|
|
20
|
+
|
|
21
|
+
type HorizonChartProps = {
|
|
22
|
+
xScale: any
|
|
23
|
+
yScale: any
|
|
24
|
+
xMax: number
|
|
25
|
+
yMax: number
|
|
26
|
+
handleTooltipMouseOver: (e: any, additionalData?: any) => void
|
|
27
|
+
handleTooltipMouseOff: () => void
|
|
28
|
+
tooltipData?: any
|
|
29
|
+
showTooltip?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const HorizonChart = ({ xScale, xMax, yMax, handleTooltipMouseOver, handleTooltipMouseOff }: HorizonChartProps) => {
|
|
33
|
+
// Get data and config from context
|
|
34
|
+
const { transformedData: data, config, colorScale, rawData, parseDate } = useContext(ConfigContext)
|
|
35
|
+
|
|
36
|
+
const horizonConfig = {
|
|
37
|
+
...HORIZON_DEFAULTS,
|
|
38
|
+
...config.horizon
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get series keys for rendering rows
|
|
42
|
+
const seriesKeys =
|
|
43
|
+
(config.runtime?.seriesKeys?.length ? config.runtime.seriesKeys : config.series?.map(s => s.dataKey)) || []
|
|
44
|
+
|
|
45
|
+
// Calculate value range across all horizon series (for consistent scaling)
|
|
46
|
+
// Must be called before early returns to satisfy React hooks rules
|
|
47
|
+
const valueRange = useMemo(() => {
|
|
48
|
+
if (!data || data.length === 0 || seriesKeys.length === 0) {
|
|
49
|
+
return { min: 0, max: 0 }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let min = Infinity
|
|
53
|
+
let max = -Infinity
|
|
54
|
+
|
|
55
|
+
data.forEach((row: any) => {
|
|
56
|
+
seriesKeys.forEach((key: string) => {
|
|
57
|
+
const value = Math.abs(Number(row[key]) || 0)
|
|
58
|
+
if (value > 0) {
|
|
59
|
+
min = Math.min(min, value)
|
|
60
|
+
max = Math.max(max, value)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
min: min === Infinity ? 0 : min,
|
|
67
|
+
max: max === -Infinity ? 0 : max
|
|
68
|
+
}
|
|
69
|
+
}, [data, seriesKeys])
|
|
70
|
+
|
|
71
|
+
// Early returns after all hooks
|
|
72
|
+
if (!data || data.length === 0) return null
|
|
73
|
+
if (seriesKeys.length === 0) return null
|
|
74
|
+
if (xMax <= 0 || yMax <= 0) return null
|
|
75
|
+
|
|
76
|
+
// Calculate band dimensions to fill available space
|
|
77
|
+
const { bandHeight, getRowY } = calculateHorizonBands(
|
|
78
|
+
seriesKeys.length,
|
|
79
|
+
yMax,
|
|
80
|
+
horizonConfig.bandGap,
|
|
81
|
+
horizonConfig.bottomPadding
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const getXPosition = value => {
|
|
85
|
+
if (config.xAxis.type === 'categorical') {
|
|
86
|
+
return xScale(value) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
87
|
+
}
|
|
88
|
+
if (isDateScale(config.xAxis)) {
|
|
89
|
+
const scaledValue = xScale(parseDate(value, false))
|
|
90
|
+
return scaledValue + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
91
|
+
}
|
|
92
|
+
return xScale(value)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ErrorBoundary component='HorizonChart'>
|
|
97
|
+
<Group className='horizon-chart' key='horizon-wrapper' left={Number(config.yAxis.size)} height={Number(yMax)}>
|
|
98
|
+
{seriesKeys.map((seriesKey, index) => {
|
|
99
|
+
const rowY = getRowY(index)
|
|
100
|
+
return (
|
|
101
|
+
<Group key={seriesKey} top={rowY} className='horizon-band-row'>
|
|
102
|
+
{/* Horizon band for this series */}
|
|
103
|
+
<HorizonBand
|
|
104
|
+
data={data}
|
|
105
|
+
seriesKey={seriesKey}
|
|
106
|
+
xAxisKey={config.xAxis.dataKey}
|
|
107
|
+
getXPosition={getXPosition}
|
|
108
|
+
bandHeight={bandHeight}
|
|
109
|
+
xMax={xMax}
|
|
110
|
+
numLayers={horizonConfig.numLayers}
|
|
111
|
+
colorScale={colorScale}
|
|
112
|
+
config={config}
|
|
113
|
+
globalMax={valueRange.max}
|
|
114
|
+
/>
|
|
115
|
+
</Group>
|
|
116
|
+
)
|
|
117
|
+
})}
|
|
118
|
+
{/* Transparent bar for tooltip interaction */}
|
|
119
|
+
<Bar
|
|
120
|
+
width={Number(xMax)}
|
|
121
|
+
height={Number(yMax)}
|
|
122
|
+
fill='transparent'
|
|
123
|
+
onMouseMove={e => handleTooltipMouseOver(e, rawData)}
|
|
124
|
+
onMouseLeave={handleTooltipMouseOff}
|
|
125
|
+
/>
|
|
126
|
+
</Group>
|
|
127
|
+
</ErrorBoundary>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default memo(HorizonChart)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React, { memo, useId, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
// visx
|
|
4
|
+
import { AreaClosed } from '@visx/shape'
|
|
5
|
+
import { Group } from '@visx/group'
|
|
6
|
+
import { scaleLinear } from '@visx/scale'
|
|
7
|
+
import * as allCurves from '@visx/curve'
|
|
8
|
+
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
|
|
9
|
+
import { getHorizonLayerColors } from '../helpers/getHorizonLayerColors'
|
|
10
|
+
|
|
11
|
+
type HorizonBandProps = {
|
|
12
|
+
data: any[]
|
|
13
|
+
seriesKey: string
|
|
14
|
+
xAxisKey: string
|
|
15
|
+
getXPosition: (value: any) => number
|
|
16
|
+
bandHeight: number
|
|
17
|
+
xMax: number
|
|
18
|
+
numLayers: number
|
|
19
|
+
colorScale: any
|
|
20
|
+
config: any
|
|
21
|
+
globalMax: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* HorizonBand renders a single series as a horizon chart
|
|
26
|
+
*
|
|
27
|
+
* Horizon charts work by:
|
|
28
|
+
* 1. Dividing the value range into N layers
|
|
29
|
+
* 2. Each layer shows values within its threshold range
|
|
30
|
+
* 3. Layers are stacked/overlapped to create the horizon effect
|
|
31
|
+
* 4. Higher values appear overlapped, usually darker depending on color palette (achieved through layer stacking)
|
|
32
|
+
*/
|
|
33
|
+
const HorizonBand = ({
|
|
34
|
+
data,
|
|
35
|
+
seriesKey,
|
|
36
|
+
xAxisKey,
|
|
37
|
+
getXPosition,
|
|
38
|
+
bandHeight,
|
|
39
|
+
xMax,
|
|
40
|
+
numLayers,
|
|
41
|
+
config,
|
|
42
|
+
globalMax
|
|
43
|
+
}: HorizonBandProps) => {
|
|
44
|
+
// Create a unique, safe ID for clipPath (useId ensures uniqueness across instances)
|
|
45
|
+
// Must be called before any early returns to follow React's rules of hooks
|
|
46
|
+
const uniqueId = useId()
|
|
47
|
+
const safeSeriesKey = seriesKey.replace(/[^a-zA-Z0-9]/g, '-')
|
|
48
|
+
const clipId = `horizon-clip-${safeSeriesKey}-${uniqueId.replace(/:/g, '')}`
|
|
49
|
+
|
|
50
|
+
// Get the curve type from config (same as stacked area chart)
|
|
51
|
+
const curveType = allCurves[approvedCurveTypes[config.stackedAreaChartLineType || 'Linear']] || allCurves.curveLinear
|
|
52
|
+
|
|
53
|
+
// Process data: convert to absolute values and compute series max in single pass
|
|
54
|
+
const { processedData, seriesMax } = useMemo(() => {
|
|
55
|
+
let max = 0
|
|
56
|
+
const processed = data.map(d => {
|
|
57
|
+
const absValue = Math.abs(Number(d[seriesKey]) || 0)
|
|
58
|
+
if (absValue > max) max = absValue
|
|
59
|
+
return { ...d, [seriesKey]: absValue }
|
|
60
|
+
})
|
|
61
|
+
return { processedData: processed, seriesMax: max }
|
|
62
|
+
}, [data, seriesKey])
|
|
63
|
+
|
|
64
|
+
// Get layer colors using shared helper (memoized based on palette config and numLayers)
|
|
65
|
+
// Must be called before early returns to follow React's rules of hooks
|
|
66
|
+
const layerColors = useMemo(
|
|
67
|
+
() => getHorizonLayerColors(config, numLayers),
|
|
68
|
+
[
|
|
69
|
+
config.general?.palette?.name,
|
|
70
|
+
config.general?.palette?.isReversed,
|
|
71
|
+
config.general?.palette?.version,
|
|
72
|
+
config.general?.palette?.customColors,
|
|
73
|
+
numLayers
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Use global max for scaling (ensures all series bands are comparable)
|
|
78
|
+
const maxValue = globalMax
|
|
79
|
+
|
|
80
|
+
// If no data, max is 0, or dimensions are invalid, don't render
|
|
81
|
+
if (maxValue === 0) return null
|
|
82
|
+
if (xMax <= 0 || bandHeight <= 0) return null
|
|
83
|
+
|
|
84
|
+
// Calculate the threshold for each layer
|
|
85
|
+
// Each layer represents 1/numLayers of the max value
|
|
86
|
+
const layerThreshold = maxValue / numLayers
|
|
87
|
+
|
|
88
|
+
// Create a y-scale for positioning within the band
|
|
89
|
+
// The scale maps values 0-layerThreshold to the full bandHeight
|
|
90
|
+
// Each layer uses the full band height, creating overlay effect
|
|
91
|
+
const yScale = scaleLinear({
|
|
92
|
+
domain: [0, layerThreshold],
|
|
93
|
+
range: [bandHeight, 0],
|
|
94
|
+
clamp: true // Clamp values above threshold
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Render layers from bottom to top
|
|
98
|
+
// Each layer shows values from (layerIndex * threshold) to ((layerIndex + 1) * threshold)
|
|
99
|
+
const layers = []
|
|
100
|
+
|
|
101
|
+
for (let layerIndex = 0; layerIndex < numLayers; layerIndex++) {
|
|
102
|
+
const layerMin = layerIndex * layerThreshold
|
|
103
|
+
|
|
104
|
+
// Short-circuit: if this layer's minimum exceeds the series max,
|
|
105
|
+
// no remaining layers can have visible data
|
|
106
|
+
if (layerMin >= seriesMax) break
|
|
107
|
+
|
|
108
|
+
// Build layer data and track hasData in a single pass
|
|
109
|
+
let hasData = false
|
|
110
|
+
const layerData = processedData.map(d => {
|
|
111
|
+
const rawValue = d[seriesKey]
|
|
112
|
+
// Calculate the value relative to this layer's base
|
|
113
|
+
const layerValue = Math.max(0, rawValue - layerMin)
|
|
114
|
+
// Clamp to the layer threshold
|
|
115
|
+
const clampedValue = Math.min(layerValue, layerThreshold)
|
|
116
|
+
|
|
117
|
+
if (clampedValue > 0) hasData = true
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
x: d[xAxisKey],
|
|
121
|
+
y: clampedValue
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if (!hasData) continue
|
|
126
|
+
|
|
127
|
+
// Get color for this layer from the distributed layer colors
|
|
128
|
+
const layerColor = layerColors[layerIndex]
|
|
129
|
+
|
|
130
|
+
layers.push(
|
|
131
|
+
<Group key={`layer-${layerIndex}`} top={0}>
|
|
132
|
+
<AreaClosed
|
|
133
|
+
data={layerData}
|
|
134
|
+
x={d => getXPosition(d.x)}
|
|
135
|
+
y={d => yScale(d.y)}
|
|
136
|
+
yScale={yScale}
|
|
137
|
+
curve={curveType}
|
|
138
|
+
fill={layerColor}
|
|
139
|
+
fillOpacity={1}
|
|
140
|
+
stroke='none'
|
|
141
|
+
/>
|
|
142
|
+
</Group>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Group className='horizon-band'>
|
|
148
|
+
{/* Clip to band bounds */}
|
|
149
|
+
<defs>
|
|
150
|
+
<clipPath id={clipId}>
|
|
151
|
+
<rect x={0} y={0} width={xMax} height={bandHeight} />
|
|
152
|
+
</clipPath>
|
|
153
|
+
</defs>
|
|
154
|
+
|
|
155
|
+
<Group clipPath={`url(#${clipId})`}>{layers}</Group>
|
|
156
|
+
</Group>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default memo(HorizonBand)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the band dimensions for horizon chart rows
|
|
3
|
+
* Used by both HorizonChart (for rendering) and LeftAxis (for label positioning)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MIN_BAND_HEIGHT = 10
|
|
7
|
+
|
|
8
|
+
export type HorizonBandCalculation = {
|
|
9
|
+
bandHeight: number
|
|
10
|
+
getRowY: (index: number) => number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function calculateHorizonBands(
|
|
14
|
+
numSeries: number,
|
|
15
|
+
yMax: number,
|
|
16
|
+
bandGap: number | string,
|
|
17
|
+
bottomPadding: number | string = 15
|
|
18
|
+
): HorizonBandCalculation {
|
|
19
|
+
const gap = Number(bandGap) || 0
|
|
20
|
+
const padding = Number(bottomPadding) || 0
|
|
21
|
+
|
|
22
|
+
const totalGapSpace = (numSeries - 1) * gap + padding
|
|
23
|
+
const bandHeight = Math.max((yMax - totalGapSpace) / numSeries, MIN_BAND_HEIGHT)
|
|
24
|
+
const getRowY = (index: number) => index * (bandHeight + gap)
|
|
25
|
+
|
|
26
|
+
return { bandHeight, getRowY }
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
|
|
2
|
+
import { v2ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculates the layer colors for a horizon chart based on palette configuration.
|
|
6
|
+
* Shared between HorizonBand rendering and Legend display.
|
|
7
|
+
*/
|
|
8
|
+
export const getHorizonLayerColors = (config: any, numLayers: number): string[] => {
|
|
9
|
+
const paletteName = config.general?.palette?.name || 'sequential_blue'
|
|
10
|
+
const colorPalettes = filterChartColorPalettes(config)
|
|
11
|
+
const fullPalette = colorPalettes[paletteName] || Object.values(colorPalettes)[0] || ['#4292c6']
|
|
12
|
+
|
|
13
|
+
// Use v2ColorDistribution if we have a 9-color palette and numLayers <= 9
|
|
14
|
+
if (fullPalette.length === 9 && numLayers <= 9 && v2ColorDistribution[numLayers]) {
|
|
15
|
+
const indices = v2ColorDistribution[numLayers]
|
|
16
|
+
return indices.map((i: number) => fullPalette[i])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback: take first numLayers colors, or repeat if needed
|
|
20
|
+
return Array.from({ length: numLayers }, (_, i) => fullPalette[i % fullPalette.length])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculates the max value across all series in a horizon chart dataset.
|
|
25
|
+
* Used for consistent scaling across all bands.
|
|
26
|
+
*/
|
|
27
|
+
export const getHorizonMaxValue = (data: any[], seriesKeys: string[]): number => {
|
|
28
|
+
if (!data || data.length === 0 || !seriesKeys || seriesKeys.length === 0) {
|
|
29
|
+
return 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let max = 0
|
|
33
|
+
for (const row of data) {
|
|
34
|
+
for (const key of seriesKeys) {
|
|
35
|
+
const value = Math.abs(Number(row[key]) || 0)
|
|
36
|
+
if (value > max) max = value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return max
|
|
40
|
+
}
|