@cdc/chart 4.26.7 → 4.26.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG.md +502 -368
- package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
- package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
- package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
- package/dist/cdcchart.js +4 -97406
- package/examples/__data__/vega-remote-data.json +6 -0
- package/examples/dendrogram.json +18 -0
- package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
- package/examples/feature/heatmap/average-age-categorical.json +2 -2
- package/examples/feature/heatmap/calendar-heatmap.json +2 -2
- package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
- package/examples/feature/heatmap/sparse-aggregation.json +2 -2
- package/examples/feature/line/line-chart-race.json +50 -0
- package/examples/feature/sankey/sankey-example-data.json +40 -1441
- package/examples/feature/sankey/sankey_chart_data.csv +9 -20
- package/examples/network.json +31 -0
- package/examples/private/box-plot.json +573 -0
- package/examples/private/custom-color-issue.json +625 -0
- package/examples/private/custom-colors.json +522 -0
- package/examples/private/cutoff.json +10750 -0
- package/examples/private/overlay-legend.json +485 -0
- package/examples/private/pie-test.json +469 -0
- package/examples/vega-remote.json +22 -0
- package/package.json +11 -5
- package/src/CdcChartComponent.tsx +405 -208
- package/src/_stories/Chart.BarRace.stories.tsx +443 -0
- package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
- package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
- package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
- package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
- package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
- package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
- package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
- package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
- package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
- package/src/_stories/Chart.LineRace.stories.tsx +149 -0
- package/src/_stories/Chart.Network.stories.tsx +432 -0
- package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
- package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
- package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
- package/src/_stories/Chart.PieLabels.stories.tsx +4 -2
- package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
- package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
- package/src/_stories/Chart.Sankey.stories.tsx +417 -0
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
- package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
- package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
- package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
- package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
- package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
- package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
- package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
- package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
- package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
- package/src/_stories/_mock/editor-test-configs.json +20 -10
- package/src/_stories/_mock/editor-test-datasets.json +38 -42
- package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
- package/src/_stories/_mock/heatmap.json +2 -2
- package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
- package/src/_stories/_mock/sankey_mock.json +23 -27
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
- package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
- package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
- package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
- package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
- package/src/components/Axis/BottomAxis.tsx +1 -1
- package/src/components/Axis/Categorical.Axis.tsx +3 -3
- package/src/components/Axis/RightAxis.tsx +21 -13
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -6
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -3
- package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +29 -25
- package/src/components/BarChart/components/BarChart.Vertical.tsx +37 -26
- package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
- package/src/components/BarChart/components/context.tsx +6 -0
- package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
- package/src/components/BarChart/helpers/index.ts +8 -0
- package/src/components/BarChart/helpers/portionPattern.ts +243 -0
- package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
- package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
- package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
- package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +104 -0
- package/src/components/BarChart/helpers/useBarChart.ts +60 -59
- package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
- package/src/components/BarChartRace/BarChartRace.tsx +230 -0
- package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
- package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
- package/src/components/BarChartRace/bar-chart-race.css +160 -0
- package/src/components/BarChartRace/helpers.test.ts +114 -0
- package/src/components/BarChartRace/helpers.ts +185 -0
- package/src/components/BarChartRace/index.ts +3 -0
- package/src/components/BoxPlot/helpers/index.ts +11 -82
- package/src/components/Brush/BrushSelector.tsx +9 -23
- package/src/components/Brush/MiniChartPreview.tsx +33 -1
- package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
- package/src/components/Dendrogram/dendrogram.css +113 -0
- package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
- package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
- package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
- package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
- package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
- package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
- package/src/components/Dendrogram/index.tsx +1 -0
- package/src/components/Dendrogram/types/index.ts +66 -0
- package/src/components/EditorPanel/EditorPanel.tsx +1005 -760
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
- package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +88 -54
- package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
- package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +233 -33
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
- package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
- package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +2 -1
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +62 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +70 -24
- package/src/components/Forecasting/Forecasting.tsx +15 -6
- package/src/components/HeatMap/components/HeatMap.tsx +511 -178
- package/src/components/HeatMap/heatmap.constants.ts +7 -3
- package/src/components/HeatMap/heatmap.css +101 -2
- package/src/components/HeatMap/helpers/layout.ts +80 -0
- package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
- package/src/components/HeatMap/tests/helpers.test.ts +79 -0
- package/src/components/Legend/Legend.Component.tsx +110 -90
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -34
- package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
- package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
- package/src/components/LineChart/helpers.test.ts +126 -38
- package/src/components/LineChart/helpers.ts +9 -4
- package/src/components/LineChart/index.tsx +69 -24
- package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
- package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
- package/src/components/LineChartRace/LineChartRace.tsx +134 -0
- package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
- package/src/components/LineChartRace/helpers.test.ts +88 -0
- package/src/components/LineChartRace/helpers.ts +157 -0
- package/src/components/LineChartRace/index.ts +3 -0
- package/src/components/LineChartRace/line-chart-race.css +16 -0
- package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
- package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
- package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
- package/src/components/LinearChart.tsx +81 -51
- package/src/components/Network/components/Network.tsx +372 -0
- package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
- package/src/components/Network/helpers/layoutNetwork.ts +87 -0
- package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
- package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
- package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
- package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
- package/src/components/Network/index.tsx +3 -0
- package/src/components/Network/network.css +107 -0
- package/src/components/Network/types/index.ts +56 -0
- package/src/components/PairedBarChart.jsx +5 -10
- package/src/components/PairedBarChart.test.tsx +71 -0
- package/src/components/PieChart/PieChart.tsx +9 -8
- package/src/components/RacingChartRenderer.tsx +44 -0
- package/src/components/Regions/components/Regions.test.tsx +179 -0
- package/src/components/Regions/components/Regions.tsx +104 -69
- package/src/components/Regions/index.tsx +2 -3
- package/src/components/Sankey/components/Sankey.tsx +264 -437
- package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
- package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
- package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
- package/src/components/Sankey/sankey.scss +57 -116
- package/src/components/Sankey/types/index.ts +41 -4
- package/src/components/ScatterPlot/ScatterPlot.jsx +28 -4
- package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
- package/src/components/raceTiming.ts +18 -0
- package/src/data/initial-state.js +77 -31
- package/src/helpers/applyChartTypeContract.ts +71 -0
- package/src/helpers/axisLabelFontSize.ts +2 -2
- package/src/helpers/boxPlotStats.ts +60 -0
- package/src/helpers/bumpChartTooltip.ts +2 -1
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
- package/src/helpers/calculateRightYAxisWidth.ts +60 -0
- package/src/helpers/colorAssignmentHelpers.ts +2 -2
- package/src/helpers/createNewChartConfig.ts +56 -0
- package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
- package/src/helpers/forecastLegend.ts +88 -0
- package/src/helpers/getBoxPlotConfig.ts +18 -37
- package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
- package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
- package/src/helpers/getColorScale.ts +31 -22
- package/src/helpers/getDynamicYAxisCategories.ts +69 -0
- package/src/helpers/getMinMax.ts +9 -7
- package/src/helpers/getNewRuntime.ts +5 -2
- package/src/helpers/getStackedSeriesMax.ts +51 -0
- package/src/helpers/getV21ChartDistributionColors.ts +57 -0
- package/src/helpers/handleRankByValue.ts +15 -15
- package/src/helpers/seriesColumnSettings.ts +10 -0
- package/src/helpers/sizeHelpers.ts +4 -3
- package/src/helpers/smallMultiplesHelpers.ts +10 -13
- package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
- package/src/helpers/tests/boxPlotStats.test.ts +31 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
- package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
- package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
- package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
- package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
- package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
- package/src/helpers/tests/getColorScale.test.ts +226 -1
- package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
- package/src/helpers/tests/getMinMax.test.ts +41 -0
- package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
- package/src/helpers/tests/handleRankByValue.test.ts +55 -37
- package/src/helpers/tests/seriesColumnSettings.test.ts +23 -0
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
- package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
- package/src/helpers/tooltipHelpers.ts +16 -0
- package/src/hooks/tests/useTooltip.test.tsx +353 -1
- package/src/hooks/usePrefersReducedMotion.ts +22 -0
- package/src/hooks/useReduceData.ts +4 -36
- package/src/hooks/useScales.ts +15 -0
- package/src/hooks/useTooltip.tsx +95 -64
- package/src/scss/main.scss +74 -2
- package/src/store/chart.reducer.ts +4 -3
- package/src/test/CdcChart.dataTable.test.tsx +490 -16
- package/src/test/CdcChart.dendrogram.test.tsx +277 -0
- package/src/test/CdcChart.finalization.test.tsx +143 -0
- package/src/test/CdcChart.network.test.tsx +271 -0
- package/src/types/ChartConfig.ts +91 -54
- package/src/types/ChartContext.ts +5 -0
- package/src/types/Horizon.ts +1 -1
- package/LICENSE +0 -201
- package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
- package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
- package/src/components/Sankey/components/ColumnList.tsx +0 -19
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
- package/src/components/Sankey/useSankeyAlert.tsx +0 -60
- package/src/data/legacy-defaults.ts +0 -18
- package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
.bar-chart-race {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
min-height: 180px;
|
|
4
|
+
padding: 0.5rem 0 1rem;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.bar-chart-race__header {
|
|
9
|
+
align-items: center;
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: 0.75rem;
|
|
12
|
+
justify-content: flex-start;
|
|
13
|
+
margin-bottom: 1rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.bar-chart-race__frame {
|
|
17
|
+
font-family: var(--fonts-nunito, var(--app-font-secondary));
|
|
18
|
+
font-size: 1.5rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.bar-chart-race__plot {
|
|
22
|
+
position: relative;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.bar-chart-race__row {
|
|
26
|
+
align-items: center;
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: clamp(6rem, 16%, 8rem) 1fr;
|
|
29
|
+
height: 40px;
|
|
30
|
+
left: 0;
|
|
31
|
+
position: absolute;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.bar-chart-race__category {
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
padding-right: 0.75rem;
|
|
39
|
+
text-align: right;
|
|
40
|
+
text-overflow: ellipsis;
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.bar-chart-race__bar-area {
|
|
45
|
+
align-items: center;
|
|
46
|
+
display: flex;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bar-chart-race__bar {
|
|
51
|
+
flex: 0 0 auto;
|
|
52
|
+
height: 32px;
|
|
53
|
+
min-width: 1px;
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.bar-chart-race__bar--value-inside {
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.bar-chart-race__value {
|
|
62
|
+
font-variant-numeric: tabular-nums;
|
|
63
|
+
position: absolute;
|
|
64
|
+
top: 50%;
|
|
65
|
+
transform: translateY(-50%);
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bar-chart-race__value--inside {
|
|
70
|
+
right: 0.3125rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.bar-chart-race__value--after {
|
|
74
|
+
color: var(--base-black, #1c1d1f);
|
|
75
|
+
left: 100%;
|
|
76
|
+
padding-left: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.bar-chart-race--labels-below-bar .bar-chart-race__row {
|
|
80
|
+
align-items: start;
|
|
81
|
+
grid-template-areas:
|
|
82
|
+
'bar'
|
|
83
|
+
'category';
|
|
84
|
+
grid-template-columns: minmax(0, 1fr);
|
|
85
|
+
grid-template-rows: 32px 24px;
|
|
86
|
+
height: 56px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.bar-chart-race--labels-below-bar .bar-chart-race__category {
|
|
90
|
+
grid-area: category;
|
|
91
|
+
max-width: 100%;
|
|
92
|
+
padding: 0.25rem 0 0;
|
|
93
|
+
text-align: left;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.bar-chart-race--labels-below-bar .bar-chart-race__bar-area {
|
|
97
|
+
grid-area: bar;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.bar-chart-race__frame-axis {
|
|
101
|
+
margin-left: clamp(6rem, 16%, 8rem);
|
|
102
|
+
margin-top: 0.75rem;
|
|
103
|
+
min-width: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.bar-chart-race--labels-below-bar .bar-chart-race__frame-axis {
|
|
107
|
+
margin-left: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.bar-chart-race__frame-axis-ticks {
|
|
111
|
+
border-top: 1px solid var(--cool-gray-30, #a9aeb1);
|
|
112
|
+
display: flex;
|
|
113
|
+
overflow-x: auto;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.bar-chart-race__frame-axis-tick {
|
|
117
|
+
color: var(--cool-gray-70, #565c65);
|
|
118
|
+
flex: 1 0 5rem;
|
|
119
|
+
font-size: 0.75rem;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
position: relative;
|
|
122
|
+
text-align: center;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.bar-chart-race__frame-axis-tick.is-active {
|
|
126
|
+
color: var(--base-black, #1c1d1f);
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bar-chart-race__frame-axis-mark {
|
|
131
|
+
border-left: 1px solid currentColor;
|
|
132
|
+
display: block;
|
|
133
|
+
height: 0.4rem;
|
|
134
|
+
margin: 0 auto 0.2rem;
|
|
135
|
+
width: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.bar-chart-race__frame-axis-value {
|
|
139
|
+
display: block;
|
|
140
|
+
padding: 0 0.25rem;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.bar-chart-race__frame-axis-label {
|
|
145
|
+
display: block;
|
|
146
|
+
font-size: 0.875rem;
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
margin-top: 0.4rem;
|
|
149
|
+
text-align: center;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@media (max-width: 480px) {
|
|
153
|
+
.bar-chart-race__row {
|
|
154
|
+
grid-template-columns: clamp(5rem, 24%, 6.5rem) 1fr;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.bar-chart-race__frame-axis {
|
|
158
|
+
margin-left: clamp(5rem, 24%, 6.5rem);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildBarRaceFrames, clampBarRaceMaxBars, getBarRaceEligibility } from './helpers'
|
|
3
|
+
|
|
4
|
+
const config = {
|
|
5
|
+
visualizationType: 'Bar',
|
|
6
|
+
visualizationSubType: 'racing',
|
|
7
|
+
orientation: 'horizontal',
|
|
8
|
+
barStyle: 'flat',
|
|
9
|
+
isLollipopChart: false,
|
|
10
|
+
xAxis: { type: 'categorical', dataKey: 'Year' },
|
|
11
|
+
series: [{ dataKey: 'Value', dynamicCategory: 'Place' }],
|
|
12
|
+
runtime: { seriesKeys: ['Beta', 'Alpha', 'Gamma'] },
|
|
13
|
+
barRace: { maxBars: 2 }
|
|
14
|
+
} as any
|
|
15
|
+
|
|
16
|
+
const data = [
|
|
17
|
+
{ Year: '2020', Place: 'Alpha', Value: 10 },
|
|
18
|
+
{ Year: '2020', Place: 'Beta', Value: 10 },
|
|
19
|
+
{ Year: '2020', Place: 'Gamma', Value: 30 },
|
|
20
|
+
{ Year: '2021', Place: 'Alpha', Value: 50 },
|
|
21
|
+
{ Year: '2021', Place: 'Beta', Value: 20 },
|
|
22
|
+
{ Year: '2021', Place: 'Gamma', Value: 'missing' }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const wideConfig = {
|
|
26
|
+
...config,
|
|
27
|
+
series: [
|
|
28
|
+
{ dataKey: 'Alpha', name: 'Alpha' },
|
|
29
|
+
{ dataKey: 'Beta', name: 'Beta' },
|
|
30
|
+
{ dataKey: 'Gamma', name: 'Gamma' }
|
|
31
|
+
],
|
|
32
|
+
runtime: { seriesKeys: ['Beta', 'Alpha', 'Gamma'] }
|
|
33
|
+
} as any
|
|
34
|
+
|
|
35
|
+
const wideData = [
|
|
36
|
+
{ Year: '2020', Alpha: 10, Beta: 10, Gamma: 30 },
|
|
37
|
+
{ Year: '2021', Alpha: 50, Beta: 20, Gamma: 'missing' }
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
describe('bar chart race helpers', () => {
|
|
41
|
+
it('preserves frame order, ranks descending, and uses runtime order to break ties', () => {
|
|
42
|
+
const result = buildBarRaceFrames(config, data)
|
|
43
|
+
|
|
44
|
+
expect(result.frames.map(frame => frame.key)).toEqual(['2020', '2021'])
|
|
45
|
+
expect(result.frames[0].items.map(item => item.category)).toEqual(['Gamma', 'Beta'])
|
|
46
|
+
expect(result.frames[1].items.map(item => item.category)).toEqual(['Alpha', 'Beta'])
|
|
47
|
+
expect(result.globalMax).toBe(50)
|
|
48
|
+
expect(result.competitorCount).toBe(3)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('uses ordinary wide series as competitors without a Dynamic Category column', () => {
|
|
52
|
+
const result = getBarRaceEligibility(wideConfig, wideData)
|
|
53
|
+
|
|
54
|
+
expect(result.eligible).toBe(true)
|
|
55
|
+
expect(result.frames.map(frame => frame.key)).toEqual(['2020', '2021'])
|
|
56
|
+
expect(result.frames[0].items.map(item => item.category)).toEqual(['Gamma', 'Beta'])
|
|
57
|
+
expect(result.frames[1].items.map(item => item.category)).toEqual(['Alpha', 'Beta'])
|
|
58
|
+
expect(result.frames[0].items[0]).toMatchObject({ dataKey: 'Gamma', seriesKey: 'Gamma' })
|
|
59
|
+
expect(result.competitorCount).toBe(3)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('keeps the global domain stable when the largest value falls outside a frame top N', () => {
|
|
63
|
+
const result = buildBarRaceFrames({ ...config, barRace: { maxBars: 1 } }, [
|
|
64
|
+
...data,
|
|
65
|
+
{ Year: '2022', Place: 'Gamma', Value: 100 }
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
expect(result.frames[0].items).toHaveLength(1)
|
|
69
|
+
expect(result.globalMax).toBe(100)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('rejects incompatible or insufficient configs with an actionable reason', () => {
|
|
73
|
+
expect(getBarRaceEligibility(config, data).eligible).toBe(true)
|
|
74
|
+
expect(getBarRaceEligibility({ ...config, smallMultiples: { mode: 'by-column' } }, data)).toMatchObject({
|
|
75
|
+
eligible: false,
|
|
76
|
+
reason: 'Racing mode does not support small multiples.'
|
|
77
|
+
})
|
|
78
|
+
expect(
|
|
79
|
+
getBarRaceEligibility(
|
|
80
|
+
config,
|
|
81
|
+
data.filter(row => row.Year === '2020')
|
|
82
|
+
).reason
|
|
83
|
+
).toContain('two valid frame')
|
|
84
|
+
expect(
|
|
85
|
+
getBarRaceEligibility(
|
|
86
|
+
config,
|
|
87
|
+
data.filter(row => row.Place === 'Alpha')
|
|
88
|
+
).reason
|
|
89
|
+
).toContain('two valid competitors')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('excludes blank, negative, and nonnumeric values and clamps max bars', () => {
|
|
93
|
+
const result = buildBarRaceFrames(config, [
|
|
94
|
+
...data,
|
|
95
|
+
{ Year: '2021', Place: 'Negative', Value: -1 },
|
|
96
|
+
{ Year: '2021', Place: 'Blank', Value: '' },
|
|
97
|
+
{ Year: '2021', Place: 'Null', Value: null },
|
|
98
|
+
{ Year: '2021', Place: 'Boolean', Value: false }
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
expect(result.competitorCount).toBe(3)
|
|
102
|
+
expect(clampBarRaceMaxBars(99, result.competitorCount)).toBe(3)
|
|
103
|
+
expect(clampBarRaceMaxBars(0, result.competitorCount)).toBe(1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('rejects duplicate rows, nonhorizontal orientation, and nonflat styles', () => {
|
|
107
|
+
expect(getBarRaceEligibility(config, [...data, data[0]]).reason).toContain('one row per frame')
|
|
108
|
+
expect(getBarRaceEligibility({ ...config, orientation: 'vertical' }, data).reason).toContain(
|
|
109
|
+
'horizontal orientation'
|
|
110
|
+
)
|
|
111
|
+
expect(getBarRaceEligibility({ ...config, barStyle: 'rounded' }, data).reason).toContain('flat bar style')
|
|
112
|
+
expect(getBarRaceEligibility(wideConfig, [...wideData, wideData[0]]).reason).toContain('one row per frame')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { type ChartConfig } from '../../types/ChartConfig'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BAR_RACE_MAX_BARS = 10
|
|
4
|
+
|
|
5
|
+
type DataRow = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
export type BarRaceItem = {
|
|
8
|
+
category: string
|
|
9
|
+
dataKey: string
|
|
10
|
+
frame: string
|
|
11
|
+
rank: number
|
|
12
|
+
seriesKey: string
|
|
13
|
+
value: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type BarRaceFrame = {
|
|
17
|
+
key: string
|
|
18
|
+
items: BarRaceItem[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type BarRaceProjection = {
|
|
22
|
+
competitorCount: number
|
|
23
|
+
hasDuplicateRows: boolean
|
|
24
|
+
frames: BarRaceFrame[]
|
|
25
|
+
globalMax: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type BarRaceEligibility = BarRaceProjection & {
|
|
29
|
+
eligible: boolean
|
|
30
|
+
reason?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toFiniteNonnegativeNumber = (value: unknown): number | null => {
|
|
34
|
+
if (typeof value !== 'string' && typeof value !== 'number') return null
|
|
35
|
+
if (typeof value === 'string' && value.trim() === '') return null
|
|
36
|
+
const numericValue = typeof value === 'string' ? Number(value.replaceAll(',', '').replace(/%$/, '')) : Number(value)
|
|
37
|
+
return Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getMaxBars = (config: Partial<ChartConfig>, competitorCount: number) => {
|
|
41
|
+
const authoredMax = Number(config.barRace?.maxBars ?? DEFAULT_BAR_RACE_MAX_BARS)
|
|
42
|
+
const roundedMax = Number.isFinite(authoredMax) ? Math.round(authoredMax) : DEFAULT_BAR_RACE_MAX_BARS
|
|
43
|
+
return Math.max(1, Math.min(competitorCount, roundedMax))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const buildBarRaceFrames = (config: Partial<ChartConfig>, data: DataRow[] = []): BarRaceProjection => {
|
|
47
|
+
const series = Array.isArray(config.series) ? config.series.filter(item => item?.dataKey) : []
|
|
48
|
+
const dynamicSeries = series.filter(item => item.dynamicCategory)
|
|
49
|
+
const usesDynamicCategory = series.length === 1 && dynamicSeries.length === 1
|
|
50
|
+
const frameKey = config.xAxis?.dataKey
|
|
51
|
+
const emptyProjection = { competitorCount: 0, hasDuplicateRows: false, frames: [], globalMax: 0 }
|
|
52
|
+
|
|
53
|
+
if (!frameKey || !series.length || !Array.isArray(data)) return emptyProjection
|
|
54
|
+
|
|
55
|
+
const frameOrder: string[] = []
|
|
56
|
+
const categoryOrder: string[] = []
|
|
57
|
+
const categoryOrderSet = new Set<string>()
|
|
58
|
+
const valuesByFrame = new Map<string, Map<string, Omit<BarRaceItem, 'frame' | 'rank'>>>()
|
|
59
|
+
let hasDuplicateRows = false
|
|
60
|
+
|
|
61
|
+
const addValue = (frame: string, category: string, seriesKey: string, dataKey: string, value: number) => {
|
|
62
|
+
if (!categoryOrderSet.has(seriesKey)) {
|
|
63
|
+
categoryOrder.push(seriesKey)
|
|
64
|
+
categoryOrderSet.add(seriesKey)
|
|
65
|
+
}
|
|
66
|
+
const frameValues = valuesByFrame.get(frame)
|
|
67
|
+
if (frameValues?.has(seriesKey)) hasDuplicateRows = true
|
|
68
|
+
frameValues?.set(seriesKey, { category, dataKey, seriesKey, value })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data.forEach(row => {
|
|
72
|
+
const rawFrame = row?.[frameKey]
|
|
73
|
+
if (rawFrame === null || rawFrame === undefined || String(rawFrame).trim() === '') return
|
|
74
|
+
const frame = String(rawFrame).trim()
|
|
75
|
+
if (!valuesByFrame.has(frame)) {
|
|
76
|
+
frameOrder.push(frame)
|
|
77
|
+
valuesByFrame.set(frame, new Map())
|
|
78
|
+
} else if (!usesDynamicCategory) {
|
|
79
|
+
hasDuplicateRows = true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (usesDynamicCategory) {
|
|
83
|
+
const item = dynamicSeries[0]
|
|
84
|
+
const rawCategory = row?.[item.dynamicCategory]
|
|
85
|
+
const value = toFiniteNonnegativeNumber(row?.[item.dataKey])
|
|
86
|
+
if (rawCategory === null || rawCategory === undefined || String(rawCategory).trim() === '' || value === null)
|
|
87
|
+
return
|
|
88
|
+
const category = String(rawCategory).trim()
|
|
89
|
+
addValue(frame, category, category, item.dataKey, value)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
series.forEach(item => {
|
|
94
|
+
const value = toFiniteNonnegativeNumber(row?.[item.dataKey])
|
|
95
|
+
if (value === null) return
|
|
96
|
+
addValue(frame, item.name || item.dataKey, item.dataKey, item.dataKey, value)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const runtimeCategoryOrder = Array.isArray(config.runtime?.seriesKeys)
|
|
101
|
+
? [...new Set(config.runtime.seriesKeys.map(value => String(value).trim()))].filter(category =>
|
|
102
|
+
categoryOrderSet.has(category)
|
|
103
|
+
)
|
|
104
|
+
: []
|
|
105
|
+
const runtimeCategorySet = new Set(runtimeCategoryOrder)
|
|
106
|
+
const stableCategoryOrder = [...runtimeCategoryOrder, ...categoryOrder.filter(item => !runtimeCategorySet.has(item))]
|
|
107
|
+
const categoryIndex = new Map(stableCategoryOrder.map((category, index) => [category, index]))
|
|
108
|
+
const maxBars = getMaxBars(config, stableCategoryOrder.length)
|
|
109
|
+
let globalMax = 0
|
|
110
|
+
|
|
111
|
+
const frames = frameOrder.map(frame => {
|
|
112
|
+
const values = valuesByFrame.get(frame) ?? new Map()
|
|
113
|
+
const items = [...values.values()]
|
|
114
|
+
.map(item => ({ ...item, frame, rank: 0 }))
|
|
115
|
+
.sort(
|
|
116
|
+
(a, b) => b.value - a.value || (categoryIndex.get(a.seriesKey) ?? 0) - (categoryIndex.get(b.seriesKey) ?? 0)
|
|
117
|
+
)
|
|
118
|
+
.slice(0, maxBars)
|
|
119
|
+
.map((item, rank) => ({ ...item, rank }))
|
|
120
|
+
|
|
121
|
+
items.forEach(item => {
|
|
122
|
+
globalMax = Math.max(globalMax, item.value)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return { key: frame, items }
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Domain stability must include valid values outside the displayed top N.
|
|
129
|
+
valuesByFrame.forEach(values => values.forEach(item => (globalMax = Math.max(globalMax, item.value))))
|
|
130
|
+
|
|
131
|
+
return { competitorCount: stableCategoryOrder.length, hasDuplicateRows, frames, globalMax }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const getBarRaceEligibility = (config: Partial<ChartConfig>, data: DataRow[] = []): BarRaceEligibility => {
|
|
135
|
+
const projection = buildBarRaceFrames(config, data)
|
|
136
|
+
const series = Array.isArray(config.series) ? config.series : []
|
|
137
|
+
const dynamicSeries = series.filter(item => item?.dynamicCategory)
|
|
138
|
+
const hasValidDynamicSeries =
|
|
139
|
+
series.length === 1 && Boolean(series[0]?.dataKey) && Boolean(String(series[0]?.dynamicCategory || '').trim())
|
|
140
|
+
const hasValidWideSeries = series.length >= 2 && dynamicSeries.length === 0 && series.every(item => item?.dataKey)
|
|
141
|
+
|
|
142
|
+
if (config.visualizationType !== 'Bar') {
|
|
143
|
+
return { ...projection, eligible: false, reason: 'Racing mode is available only for Bar charts.' }
|
|
144
|
+
}
|
|
145
|
+
if (config.xAxis?.type !== 'categorical') {
|
|
146
|
+
return { ...projection, eligible: false, reason: 'Racing mode requires a categorical Date/Category Axis.' }
|
|
147
|
+
}
|
|
148
|
+
if (!hasValidDynamicSeries && !hasValidWideSeries) {
|
|
149
|
+
return {
|
|
150
|
+
...projection,
|
|
151
|
+
eligible: false,
|
|
152
|
+
reason: 'Racing mode requires at least two ordinary data series or one series with a Dynamic Category column.'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (config.smallMultiples?.mode) {
|
|
156
|
+
return { ...projection, eligible: false, reason: 'Racing mode does not support small multiples.' }
|
|
157
|
+
}
|
|
158
|
+
if (config.orientation !== 'horizontal') {
|
|
159
|
+
return { ...projection, eligible: false, reason: 'Racing mode requires horizontal orientation.' }
|
|
160
|
+
}
|
|
161
|
+
if (config.isLollipopChart || (config.barStyle && config.barStyle !== 'flat')) {
|
|
162
|
+
return { ...projection, eligible: false, reason: 'Racing mode requires the flat bar style.' }
|
|
163
|
+
}
|
|
164
|
+
if (projection.hasDuplicateRows) {
|
|
165
|
+
return {
|
|
166
|
+
...projection,
|
|
167
|
+
eligible: false,
|
|
168
|
+
reason: 'Racing mode requires one row per frame and competitor combination.'
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (projection.frames.length < 2) {
|
|
172
|
+
return { ...projection, eligible: false, reason: 'Racing mode requires at least two valid frame values.' }
|
|
173
|
+
}
|
|
174
|
+
if (projection.competitorCount < 2) {
|
|
175
|
+
return { ...projection, eligible: false, reason: 'Racing mode requires at least two valid competitors.' }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { ...projection, eligible: true }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const clampBarRaceMaxBars = (value: unknown, competitorCount: number) => {
|
|
182
|
+
const numericValue = Number(value)
|
|
183
|
+
const nextValue = Number.isFinite(numericValue) ? Math.round(numericValue) : DEFAULT_BAR_RACE_MAX_BARS
|
|
184
|
+
return Math.max(1, Math.min(Math.max(1, competitorCount), nextValue))
|
|
185
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import sortBy from 'lodash/sortBy'
|
|
2
1
|
import uniq from 'lodash/uniq'
|
|
3
|
-
import
|
|
2
|
+
import { calculateBoxPlotStats } from '../../../helpers/boxPlotStats'
|
|
4
3
|
|
|
5
4
|
interface Plot {
|
|
6
5
|
columnCategory: string
|
|
@@ -30,47 +29,6 @@ export const handleTooltip = (boxplot, columnCategory, key, q1, q3, median, iqr,
|
|
|
30
29
|
`
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const calculateBoxPlotStats = (values: number[]) => {
|
|
34
|
-
if (!values || values.length === 0) return {}
|
|
35
|
-
|
|
36
|
-
// Sort the values
|
|
37
|
-
const sortedValues = sortBy(values.map(v => Number(v)))
|
|
38
|
-
|
|
39
|
-
// Quartiles
|
|
40
|
-
const firstQuartile = d3.quantile(sortedValues, 0.25) ?? 0
|
|
41
|
-
const thirdQuartile = d3.quantile(sortedValues, 0.75) ?? 0
|
|
42
|
-
|
|
43
|
-
// Interquartile Range (IQR)
|
|
44
|
-
const iqr = thirdQuartile - firstQuartile
|
|
45
|
-
|
|
46
|
-
// Outlier Bounds
|
|
47
|
-
const lowerBound = firstQuartile - 1.5 * iqr
|
|
48
|
-
const upperBound = thirdQuartile + 1.5 * iqr
|
|
49
|
-
// const lowerFence = q1 - 1.5 * iqr
|
|
50
|
-
// const upperFence = q3 + 1.5 * iqr
|
|
51
|
-
|
|
52
|
-
// Non-Outlier Values
|
|
53
|
-
const nonOutliers = sortedValues.filter(value => value >= lowerBound && value <= upperBound)
|
|
54
|
-
// **Outliers** =
|
|
55
|
-
const outliers = sortedValues.filter(v => v < lowerBound || v > upperBound)
|
|
56
|
-
const whiskerMax =
|
|
57
|
-
sortedValues
|
|
58
|
-
.slice()
|
|
59
|
-
.reverse()
|
|
60
|
-
.find(v => v <= upperBound) ?? thirdQuartile
|
|
61
|
-
const whiskerMin = nonOutliers.length > 0 ? nonOutliers[0] : firstQuartile
|
|
62
|
-
// Calculate Box Plot Stats
|
|
63
|
-
return {
|
|
64
|
-
min: whiskerMin,
|
|
65
|
-
max: whiskerMax,
|
|
66
|
-
median: d3.median(sortedValues),
|
|
67
|
-
firstQuartile,
|
|
68
|
-
thirdQuartile,
|
|
69
|
-
iqr,
|
|
70
|
-
outliers
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
32
|
const getValuesBySeriesKey = (group: string, config, data) => {
|
|
75
33
|
const allSeriesKeys = config.series.map(item => item?.dataKey)
|
|
76
34
|
const result = {}
|
|
@@ -82,24 +40,6 @@ const getValuesBySeriesKey = (group: string, config, data) => {
|
|
|
82
40
|
return result
|
|
83
41
|
}
|
|
84
42
|
|
|
85
|
-
// Helper to calculate outliers based on IQR
|
|
86
|
-
const calculateOutliers = (values: number[], firstQuartile: number, thirdQuartile: number) => {
|
|
87
|
-
const iqr = thirdQuartile - firstQuartile
|
|
88
|
-
const lowerBound = firstQuartile - 1.5 * iqr
|
|
89
|
-
const upperBound = thirdQuartile + 1.5 * iqr
|
|
90
|
-
return values.filter(value => value < lowerBound || value > upperBound)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Helper to calculate non-outliers based on IQR
|
|
94
|
-
const calculateNonOutliers = (values: number[], firstQuartile: number, thirdQuartile: number): number[] => {
|
|
95
|
-
const iqr = thirdQuartile - firstQuartile
|
|
96
|
-
const lowerBound = firstQuartile - 1.5 * iqr
|
|
97
|
-
const upperBound = thirdQuartile + 1.5 * iqr
|
|
98
|
-
|
|
99
|
-
// Return values within the bounds
|
|
100
|
-
return values.filter(value => value >= lowerBound && value <= upperBound)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
43
|
// Main function to create plots with additional outlier data
|
|
104
44
|
export const createPlots = (data, config) => {
|
|
105
45
|
const dataKeys = data.map(d => d[config.xAxis.dataKey])
|
|
@@ -120,29 +60,18 @@ export const createPlots = (data, config) => {
|
|
|
120
60
|
|
|
121
61
|
// Calculate outliers and non-outliers for each series key
|
|
122
62
|
Object.keys(keyValues).forEach(key => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
// 2) normalize → trim, drop empties/non-numbers, coerce to Number
|
|
126
|
-
const cleaned: number[] = raw
|
|
127
|
-
.map(v => (typeof v === 'string' ? v.trim() : v)) // trim strings
|
|
128
|
-
.filter(v => v != null && v !== '' && !isNaN(+v)) // drop null/''/non-nums
|
|
129
|
-
.map(v => +v)
|
|
130
|
-
|
|
131
|
-
if (cleaned.length === 0) {
|
|
132
|
-
return
|
|
133
|
-
}
|
|
63
|
+
const stats = calculateBoxPlotStats(keyValues[key] ?? [])
|
|
64
|
+
if (!stats) return
|
|
134
65
|
|
|
135
66
|
// Calculate box plot statistics
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
columnQ3[key] = thirdQuartile
|
|
145
|
-
columnIqr[key] = iqr
|
|
67
|
+
columnOutliers[key] = stats.outliers
|
|
68
|
+
columnNonOutliers[key] = stats.nonOutliers
|
|
69
|
+
columnMedian[key] = stats.median ?? null
|
|
70
|
+
columnMin[key] = stats.whiskerMin
|
|
71
|
+
columnMax[key] = stats.whiskerMax
|
|
72
|
+
columnQ1[key] = stats.q1
|
|
73
|
+
columnQ3[key] = stats.q3
|
|
74
|
+
columnIqr[key] = stats.iqr
|
|
146
75
|
})
|
|
147
76
|
|
|
148
77
|
// Add the plot object to the plots array
|
|
@@ -9,6 +9,7 @@ import type { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'
|
|
|
9
9
|
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
10
10
|
import ConfigContext, { ChartDispatchContext } from '../../ConfigContext'
|
|
11
11
|
import { getChartPatternId } from '../../helpers/getChartPatternId'
|
|
12
|
+
import getStackedSeriesMax from '../../helpers/getStackedSeriesMax'
|
|
12
13
|
import MiniChartPreview from './MiniChartPreview'
|
|
13
14
|
import { classifyBrushInteraction } from './brushInteraction'
|
|
14
15
|
import type { BrushInteractionTarget } from './brushInteraction'
|
|
@@ -173,8 +174,9 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
173
174
|
id={patternId}
|
|
174
175
|
height={size}
|
|
175
176
|
width={size}
|
|
176
|
-
fill=
|
|
177
|
-
|
|
177
|
+
fill='transparent'
|
|
178
|
+
stroke={pattern.color}
|
|
179
|
+
strokeWidth={0.75}
|
|
178
180
|
/>
|
|
179
181
|
)
|
|
180
182
|
default:
|
|
@@ -298,24 +300,9 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
298
300
|
let hasValidValues = false
|
|
299
301
|
|
|
300
302
|
if (isStacked) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
for (const s of leftSeries) {
|
|
305
|
-
if (!s.dataKey) continue
|
|
306
|
-
const value = parseFloat(row[s.dataKey])
|
|
307
|
-
if (!isNaN(value) && isFinite(value)) {
|
|
308
|
-
rowSum += value
|
|
309
|
-
hasRowValue = true
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (hasRowValue) {
|
|
313
|
-
hasValidValues = true
|
|
314
|
-
minValue = Math.min(minValue, rowSum)
|
|
315
|
-
maxValue = Math.max(maxValue, rowSum)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
minValue = Math.min(0, minValue)
|
|
303
|
+
maxValue = getStackedSeriesMax(tableData, leftSeries)
|
|
304
|
+
minValue = 0
|
|
305
|
+
hasValidValues = maxValue !== 0
|
|
319
306
|
} else {
|
|
320
307
|
for (const s of leftSeries) {
|
|
321
308
|
if (!s.dataKey) continue
|
|
@@ -831,14 +818,13 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
831
818
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
832
819
|
}, [config.xAxis.brushDefaultRecentDateCount])
|
|
833
820
|
|
|
834
|
-
// Selected box style with solid border and pattern fill
|
|
835
821
|
const selectedBoxStyle = useMemo(
|
|
836
822
|
() => ({
|
|
837
|
-
fill: 'url(#brush_pattern)',
|
|
823
|
+
fill: config.xAxis.brushHideHatching === true ? 'transparent' : 'url(#brush_pattern)',
|
|
838
824
|
stroke: '#333',
|
|
839
825
|
strokeWidth: 1
|
|
840
826
|
}),
|
|
841
|
-
[]
|
|
827
|
+
[config.xAxis.brushHideHatching]
|
|
842
828
|
)
|
|
843
829
|
|
|
844
830
|
// Helper to update brush position programmatically
|