@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.
Files changed (255) hide show
  1. package/CONFIG.md +502 -368
  2. package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
  3. package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
  4. package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
  5. package/dist/cdcchart.js +4 -97406
  6. package/examples/__data__/vega-remote-data.json +6 -0
  7. package/examples/dendrogram.json +18 -0
  8. package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
  9. package/examples/feature/heatmap/average-age-categorical.json +2 -2
  10. package/examples/feature/heatmap/calendar-heatmap.json +2 -2
  11. package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
  12. package/examples/feature/heatmap/sparse-aggregation.json +2 -2
  13. package/examples/feature/line/line-chart-race.json +50 -0
  14. package/examples/feature/sankey/sankey-example-data.json +40 -1441
  15. package/examples/feature/sankey/sankey_chart_data.csv +9 -20
  16. package/examples/network.json +31 -0
  17. package/examples/private/box-plot.json +573 -0
  18. package/examples/private/custom-color-issue.json +625 -0
  19. package/examples/private/custom-colors.json +522 -0
  20. package/examples/private/cutoff.json +10750 -0
  21. package/examples/private/overlay-legend.json +485 -0
  22. package/examples/private/pie-test.json +469 -0
  23. package/examples/vega-remote.json +22 -0
  24. package/package.json +11 -5
  25. package/src/CdcChartComponent.tsx +405 -208
  26. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  27. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  28. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  29. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  30. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  31. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  32. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  33. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  34. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  35. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  36. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  37. package/src/_stories/Chart.Network.stories.tsx +432 -0
  38. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  39. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  40. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  41. package/src/_stories/Chart.PieLabels.stories.tsx +4 -2
  42. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  43. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  44. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  45. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  46. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  47. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  48. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  49. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  50. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  51. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  52. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  53. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  54. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  55. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  56. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  57. package/src/_stories/_mock/editor-test-configs.json +20 -10
  58. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  59. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  60. package/src/_stories/_mock/heatmap.json +2 -2
  61. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  62. package/src/_stories/_mock/sankey_mock.json +23 -27
  63. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  64. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  65. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  66. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  67. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  68. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  69. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  70. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  71. package/src/components/Axis/BottomAxis.tsx +1 -1
  72. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  73. package/src/components/Axis/RightAxis.tsx +21 -13
  74. package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -6
  75. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -3
  76. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  77. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +29 -25
  78. package/src/components/BarChart/components/BarChart.Vertical.tsx +37 -26
  79. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  80. package/src/components/BarChart/components/context.tsx +6 -0
  81. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  82. package/src/components/BarChart/helpers/index.ts +8 -0
  83. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  84. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  85. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  86. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  87. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +104 -0
  88. package/src/components/BarChart/helpers/useBarChart.ts +60 -59
  89. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  90. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  91. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  92. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  93. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  94. package/src/components/BarChartRace/helpers.test.ts +114 -0
  95. package/src/components/BarChartRace/helpers.ts +185 -0
  96. package/src/components/BarChartRace/index.ts +3 -0
  97. package/src/components/BoxPlot/helpers/index.ts +11 -82
  98. package/src/components/Brush/BrushSelector.tsx +9 -23
  99. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  100. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  101. package/src/components/Dendrogram/dendrogram.css +113 -0
  102. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  103. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  104. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  105. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  106. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  107. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  108. package/src/components/Dendrogram/index.tsx +1 -0
  109. package/src/components/Dendrogram/types/index.ts +66 -0
  110. package/src/components/EditorPanel/EditorPanel.tsx +1005 -760
  111. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  112. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  113. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  114. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  115. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +88 -54
  116. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  117. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  118. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +233 -33
  119. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  120. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  121. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  122. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  123. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  124. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  125. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  126. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  127. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  128. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  129. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  130. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +2 -1
  131. package/src/components/EditorPanel/useEditorPermissions.test.tsx +62 -0
  132. package/src/components/EditorPanel/useEditorPermissions.ts +70 -24
  133. package/src/components/Forecasting/Forecasting.tsx +15 -6
  134. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  135. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  136. package/src/components/HeatMap/heatmap.css +101 -2
  137. package/src/components/HeatMap/helpers/layout.ts +80 -0
  138. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  139. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  140. package/src/components/Legend/Legend.Component.tsx +110 -90
  141. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  142. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -34
  143. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  144. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  145. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  146. package/src/components/LineChart/helpers.test.ts +126 -38
  147. package/src/components/LineChart/helpers.ts +9 -4
  148. package/src/components/LineChart/index.tsx +69 -24
  149. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  150. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  151. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  152. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  153. package/src/components/LineChartRace/helpers.test.ts +88 -0
  154. package/src/components/LineChartRace/helpers.ts +157 -0
  155. package/src/components/LineChartRace/index.ts +3 -0
  156. package/src/components/LineChartRace/line-chart-race.css +16 -0
  157. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  158. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  159. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  160. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  161. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  162. package/src/components/LinearChart.tsx +81 -51
  163. package/src/components/Network/components/Network.tsx +372 -0
  164. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  165. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  166. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  167. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  168. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  169. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  170. package/src/components/Network/index.tsx +3 -0
  171. package/src/components/Network/network.css +107 -0
  172. package/src/components/Network/types/index.ts +56 -0
  173. package/src/components/PairedBarChart.jsx +5 -10
  174. package/src/components/PairedBarChart.test.tsx +71 -0
  175. package/src/components/PieChart/PieChart.tsx +9 -8
  176. package/src/components/RacingChartRenderer.tsx +44 -0
  177. package/src/components/Regions/components/Regions.test.tsx +179 -0
  178. package/src/components/Regions/components/Regions.tsx +104 -69
  179. package/src/components/Regions/index.tsx +2 -3
  180. package/src/components/Sankey/components/Sankey.tsx +264 -437
  181. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  182. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  183. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  184. package/src/components/Sankey/sankey.scss +57 -116
  185. package/src/components/Sankey/types/index.ts +41 -4
  186. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -4
  187. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  188. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  189. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  190. package/src/components/raceTiming.ts +18 -0
  191. package/src/data/initial-state.js +77 -31
  192. package/src/helpers/applyChartTypeContract.ts +71 -0
  193. package/src/helpers/axisLabelFontSize.ts +2 -2
  194. package/src/helpers/boxPlotStats.ts +60 -0
  195. package/src/helpers/bumpChartTooltip.ts +2 -1
  196. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  197. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  198. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  199. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  200. package/src/helpers/createNewChartConfig.ts +56 -0
  201. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  202. package/src/helpers/forecastLegend.ts +88 -0
  203. package/src/helpers/getBoxPlotConfig.ts +18 -37
  204. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  205. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  206. package/src/helpers/getColorScale.ts +31 -22
  207. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  208. package/src/helpers/getMinMax.ts +9 -7
  209. package/src/helpers/getNewRuntime.ts +5 -2
  210. package/src/helpers/getStackedSeriesMax.ts +51 -0
  211. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  212. package/src/helpers/handleRankByValue.ts +15 -15
  213. package/src/helpers/seriesColumnSettings.ts +10 -0
  214. package/src/helpers/sizeHelpers.ts +4 -3
  215. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  216. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  217. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  218. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  219. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  220. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  221. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  222. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  223. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  224. package/src/helpers/tests/getColorScale.test.ts +226 -1
  225. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  226. package/src/helpers/tests/getMinMax.test.ts +41 -0
  227. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  228. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  229. package/src/helpers/tests/seriesColumnSettings.test.ts +23 -0
  230. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  231. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  232. package/src/helpers/tooltipHelpers.ts +16 -0
  233. package/src/hooks/tests/useTooltip.test.tsx +353 -1
  234. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  235. package/src/hooks/useReduceData.ts +4 -36
  236. package/src/hooks/useScales.ts +15 -0
  237. package/src/hooks/useTooltip.tsx +95 -64
  238. package/src/scss/main.scss +74 -2
  239. package/src/store/chart.reducer.ts +4 -3
  240. package/src/test/CdcChart.dataTable.test.tsx +490 -16
  241. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  242. package/src/test/CdcChart.finalization.test.tsx +143 -0
  243. package/src/test/CdcChart.network.test.tsx +271 -0
  244. package/src/types/ChartConfig.ts +91 -54
  245. package/src/types/ChartContext.ts +5 -0
  246. package/src/types/Horizon.ts +1 -1
  247. package/LICENSE +0 -201
  248. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  249. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  250. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  251. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  252. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  253. package/src/data/legacy-defaults.ts +0 -18
  254. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  255. 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
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './BarChartRace'
2
+ export { default as BarChartRaceFallback } from './BarChartRaceFallback'
3
+ export * from './helpers'
@@ -1,6 +1,5 @@
1
- import sortBy from 'lodash/sortBy'
2
1
  import uniq from 'lodash/uniq'
3
- import * as d3 from 'd3-array'
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 raw = keyValues[key] ?? []
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
- const { firstQuartile, thirdQuartile, min, max, median, iqr, outliers } = calculateBoxPlotStats(cleaned)
137
- // Calculate outliers and non-outliers
138
- columnOutliers[key] = calculateOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
139
- columnNonOutliers[key] = calculateNonOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
140
- columnMedian[key] = median
141
- columnMin[key] = Number(min)
142
- columnMax[key] = Number(max)
143
- columnQ1[key] = firstQuartile
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={pattern.color}
177
- strokeWidth={0.25}
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
- for (const row of tableData) {
302
- let rowSum = 0
303
- let hasRowValue = false
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