@cdc/chart 4.26.6 → 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 (276) hide show
  1. package/CONFIG.md +504 -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 -96498
  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/flueview.json +22051 -0
  22. package/examples/private/overlay-legend.json +485 -0
  23. package/examples/private/pie-test.json +469 -0
  24. package/examples/vega-remote.json +22 -0
  25. package/package.json +11 -13
  26. package/src/CdcChart.tsx +2 -2
  27. package/src/CdcChartComponent.tsx +444 -217
  28. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  29. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  30. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  31. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  32. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  33. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  34. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  35. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  36. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  37. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  38. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  39. package/src/_stories/Chart.Network.stories.tsx +432 -0
  40. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  41. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  42. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  43. package/src/_stories/Chart.PieLabels.stories.tsx +49 -0
  44. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  45. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  46. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  47. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  48. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  49. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  50. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  51. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  52. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  53. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  54. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  55. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  56. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  57. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  58. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  59. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  60. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  61. package/src/_stories/_mock/editor-test-configs.json +20 -10
  62. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  63. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  64. package/src/_stories/_mock/heatmap.json +2 -2
  65. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  66. package/src/_stories/_mock/sankey_mock.json +23 -27
  67. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  68. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  69. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  70. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  71. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  72. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  73. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  74. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  75. package/src/components/Axis/BottomAxis.tsx +1 -1
  76. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  77. package/src/components/Axis/RightAxis.tsx +21 -13
  78. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -8
  79. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +13 -4
  80. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  81. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +30 -26
  82. package/src/components/BarChart/components/BarChart.Vertical.tsx +38 -28
  83. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  84. package/src/components/BarChart/components/context.tsx +6 -0
  85. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  86. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  87. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  88. package/src/components/BarChart/helpers/index.ts +8 -0
  89. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  90. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  91. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  92. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  93. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +202 -0
  94. package/src/components/BarChart/helpers/useBarChart.ts +60 -60
  95. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  96. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  97. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  98. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  99. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  100. package/src/components/BarChartRace/helpers.test.ts +114 -0
  101. package/src/components/BarChartRace/helpers.ts +185 -0
  102. package/src/components/BarChartRace/index.ts +3 -0
  103. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  104. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  105. package/src/components/BoxPlot/helpers/index.ts +13 -83
  106. package/src/components/Brush/BrushSelector.tsx +10 -24
  107. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  108. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  109. package/src/components/Dendrogram/dendrogram.css +113 -0
  110. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  111. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  112. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  113. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  114. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  115. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  116. package/src/components/Dendrogram/index.tsx +1 -0
  117. package/src/components/Dendrogram/types/index.ts +66 -0
  118. package/src/components/EditorPanel/EditorPanel.tsx +1111 -805
  119. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  120. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  121. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  122. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  123. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +104 -61
  124. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  125. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  126. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +236 -36
  127. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  128. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  129. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +21 -6
  130. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  131. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  132. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  133. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  134. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  135. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  136. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  137. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  138. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +5 -4
  139. package/src/components/EditorPanel/useEditorPermissions.test.tsx +94 -0
  140. package/src/components/EditorPanel/useEditorPermissions.ts +75 -25
  141. package/src/components/Forecasting/Forecasting.tsx +16 -7
  142. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  143. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  144. package/src/components/HeatMap/heatmap.css +101 -2
  145. package/src/components/HeatMap/helpers/layout.ts +80 -0
  146. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  147. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  148. package/src/components/Legend/Legend.Component.tsx +163 -148
  149. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  150. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  151. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -35
  152. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  153. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  154. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  155. package/src/components/LineChart/helpers.test.ts +126 -38
  156. package/src/components/LineChart/helpers.ts +9 -6
  157. package/src/components/LineChart/index.tsx +71 -27
  158. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  159. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  160. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  161. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  162. package/src/components/LineChartRace/helpers.test.ts +88 -0
  163. package/src/components/LineChartRace/helpers.ts +157 -0
  164. package/src/components/LineChartRace/index.ts +3 -0
  165. package/src/components/LineChartRace/line-chart-race.css +16 -0
  166. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  167. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  168. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  169. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  170. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  171. package/src/components/LinearChart.tsx +85 -54
  172. package/src/components/Network/components/Network.tsx +372 -0
  173. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  174. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  175. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  176. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  177. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  178. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  179. package/src/components/Network/index.tsx +3 -0
  180. package/src/components/Network/network.css +107 -0
  181. package/src/components/Network/types/index.ts +56 -0
  182. package/src/components/PairedBarChart.jsx +5 -10
  183. package/src/components/PairedBarChart.test.tsx +71 -0
  184. package/src/components/PieChart/PieChart.tsx +121 -13
  185. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  186. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  187. package/src/components/RacingChartRenderer.tsx +44 -0
  188. package/src/components/Regions/components/Regions.test.tsx +179 -0
  189. package/src/components/Regions/components/Regions.tsx +104 -69
  190. package/src/components/Regions/index.tsx +2 -3
  191. package/src/components/Sankey/components/Sankey.tsx +264 -437
  192. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  193. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  194. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  195. package/src/components/Sankey/sankey.scss +57 -116
  196. package/src/components/Sankey/types/index.ts +41 -4
  197. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -12
  198. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  199. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  200. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  201. package/src/components/raceTiming.ts +18 -0
  202. package/src/data/initial-state.js +79 -31
  203. package/src/helpers/applyChartTypeContract.ts +71 -0
  204. package/src/helpers/axisLabelFontSize.ts +2 -2
  205. package/src/helpers/boxPlotStats.ts +60 -0
  206. package/src/helpers/bumpChartTooltip.ts +2 -1
  207. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  208. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  209. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  210. package/src/helpers/categoryOrder.ts +60 -0
  211. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  212. package/src/helpers/createNewChartConfig.ts +56 -0
  213. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  214. package/src/helpers/forecastLegend.ts +88 -0
  215. package/src/helpers/getBoxPlotConfig.ts +26 -38
  216. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  217. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  218. package/src/helpers/getColorScale.ts +31 -22
  219. package/src/helpers/getComboChartConfig.ts +0 -1
  220. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  221. package/src/helpers/getExcludedData.ts +0 -1
  222. package/src/helpers/getHasBoundarySuppression.ts +4 -3
  223. package/src/helpers/getMinMax.ts +11 -8
  224. package/src/helpers/getNewRuntime.ts +4 -3
  225. package/src/helpers/getPiePercent.ts +3 -3
  226. package/src/helpers/getStackedSeriesMax.ts +51 -0
  227. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  228. package/src/helpers/handleRankByValue.ts +15 -15
  229. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  230. package/src/helpers/seriesColumnSettings.ts +31 -3
  231. package/src/helpers/sizeHelpers.ts +4 -5
  232. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  233. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  234. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  235. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  236. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  237. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  238. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  239. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  240. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  241. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  242. package/src/helpers/tests/getColorScale.test.ts +226 -1
  243. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  244. package/src/helpers/tests/getMinMax.test.ts +41 -0
  245. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  246. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  247. package/src/helpers/tests/seriesColumnSettings.test.ts +83 -0
  248. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  249. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  250. package/src/helpers/tooltipHelpers.ts +16 -0
  251. package/src/hooks/tests/useRightAxis.test.tsx +143 -2
  252. package/src/hooks/tests/useScales.test.tsx +3 -6
  253. package/src/hooks/tests/useTooltip.test.tsx +484 -0
  254. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  255. package/src/hooks/useReduceData.ts +4 -36
  256. package/src/hooks/useRightAxis.ts +19 -26
  257. package/src/hooks/useScales.ts +15 -0
  258. package/src/hooks/useTooltip.tsx +106 -71
  259. package/src/scss/main.scss +74 -2
  260. package/src/store/chart.reducer.ts +4 -4
  261. package/src/test/CdcChart.dataTable.test.tsx +582 -14
  262. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  263. package/src/test/CdcChart.finalization.test.tsx +143 -0
  264. package/src/test/CdcChart.network.test.tsx +271 -0
  265. package/src/types/ChartConfig.ts +91 -54
  266. package/src/types/ChartContext.ts +5 -0
  267. package/src/types/Horizon.ts +1 -1
  268. package/LICENSE +0 -201
  269. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  270. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  271. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  272. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  273. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  274. package/src/data/legacy-defaults.ts +0 -18
  275. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  276. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -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'
@@ -5,7 +5,8 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
5
5
  import ConfigContext from '../../ConfigContext'
6
6
  import { handleTooltip, createPlots } from './helpers/index'
7
7
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
8
- import _ from 'lodash'
8
+ import map from 'lodash/map'
9
+ import round from 'lodash/round'
9
10
 
10
11
  const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
11
12
  const { config, transformedData: data, colorScale, seriesHighlight } = useContext(ConfigContext)
@@ -63,7 +64,7 @@ const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
63
64
  maxProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
64
65
  medianProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
65
66
  outliers={
66
- config.boxplot.plotOutlierValues ? _.map(plot.columnOutliers[item.dataKey], item => item) : []
67
+ config.boxplot.plotOutlierValues ? map(plot.columnOutliers[item.dataKey], item => item) : []
67
68
  }
68
69
  outlierProps={{
69
70
  style: {
@@ -78,10 +79,10 @@ const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
78
79
  config.boxplot,
79
80
  plot.columnCategory,
80
81
  item.dataKey,
81
- _.round(plot.q1[item.dataKey], config.dataFormat.roundTo),
82
- _.round(plot.q3[item.dataKey], config.dataFormat.roundTo),
83
- _.round(plot.median[item.dataKey], config.dataFormat.roundTo),
84
- _.round(plot.iqr[item.dataKey], config.dataFormat.roundTo),
82
+ round(plot.q1[item.dataKey], config.dataFormat.roundTo),
83
+ round(plot.q3[item.dataKey], config.dataFormat.roundTo),
84
+ round(plot.median[item.dataKey], config.dataFormat.roundTo),
85
+ round(plot.iqr[item.dataKey], config.dataFormat.roundTo),
85
86
  config.xAxis.label,
86
87
  defaultColor
87
88
  ),
@@ -7,7 +7,9 @@ import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
7
  import { getCurrentPaletteName } from '@cdc/core/helpers/palettes/utils'
8
8
  import { handleTooltip, createPlots } from './helpers/index'
9
9
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
10
- import _ from 'lodash'
10
+ import get from 'lodash/get'
11
+ import map from 'lodash/map'
12
+ import round from 'lodash/round'
11
13
 
12
14
  const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
13
15
  const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
@@ -19,7 +21,7 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
19
21
  const bodyStyles = getComputedStyle(document.body)
20
22
  const defaultColor = APP_FONT_COLOR
21
23
  const constrainedWidth = Math.min(40, boxWidth)
22
- const color_0 = _.get(colorPalettesChart, [getCurrentPaletteName(config), 0], '#000')
24
+ const color_0 = get(colorPalettesChart, [getCurrentPaletteName(config), 0], '#000')
23
25
  const plots = createPlots(data, config)
24
26
  return (
25
27
  <ErrorBoundary component='BoxPlot Vertical'>
@@ -76,7 +78,7 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
76
78
  fillOpacity={1}
77
79
  stroke={defaultColor}
78
80
  valueScale={yScale}
79
- outliers={boxplot.plotOutlierValues ? _.map(d.columnOutliers[item.dataKey], item => item) : []}
81
+ outliers={boxplot.plotOutlierValues ? map(d.columnOutliers[item.dataKey], item => item) : []}
80
82
  outlierProps={{
81
83
  style: {
82
84
  fill: defaultColor,
@@ -109,10 +111,10 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
109
111
  boxplot,
110
112
  d.columnCategory,
111
113
  item.dataKey,
112
- _.round(d.q1[item.dataKey], config.dataFormat.roundTo),
113
- _.round(d.q3[item.dataKey], config.dataFormat.roundTo),
114
- _.round(d.median[item.dataKey], config.dataFormat.roundTo),
115
- _.round(d.iqr[item.dataKey], config.dataFormat.roundTo),
114
+ round(d.q1[item.dataKey], config.dataFormat.roundTo),
115
+ round(d.q3[item.dataKey], config.dataFormat.roundTo),
116
+ round(d.median[item.dataKey], config.dataFormat.roundTo),
117
+ round(d.iqr[item.dataKey], config.dataFormat.roundTo),
116
118
  config.xAxis.label,
117
119
  defaultColor
118
120
  ),
@@ -1,5 +1,5 @@
1
- import _ from 'lodash'
2
- import * as d3 from 'd3-array'
1
+ import uniq from 'lodash/uniq'
2
+ import { calculateBoxPlotStats } from '../../../helpers/boxPlotStats'
3
3
 
4
4
  interface Plot {
5
5
  columnCategory: string
@@ -29,47 +29,6 @@ export const handleTooltip = (boxplot, columnCategory, key, q1, q3, median, iqr,
29
29
  `
30
30
  }
31
31
 
32
- const calculateBoxPlotStats = (values: number[]) => {
33
- if (!values || values.length === 0) return {}
34
-
35
- // Sort the values
36
- const sortedValues = _.sortBy(values.map(v => Number(v)))
37
-
38
- // Quartiles
39
- const firstQuartile = d3.quantile(sortedValues, 0.25) ?? 0
40
- const thirdQuartile = d3.quantile(sortedValues, 0.75) ?? 0
41
-
42
- // Interquartile Range (IQR)
43
- const iqr = thirdQuartile - firstQuartile
44
-
45
- // Outlier Bounds
46
- const lowerBound = firstQuartile - 1.5 * iqr
47
- const upperBound = thirdQuartile + 1.5 * iqr
48
- // const lowerFence = q1 - 1.5 * iqr
49
- // const upperFence = q3 + 1.5 * iqr
50
-
51
- // Non-Outlier Values
52
- const nonOutliers = sortedValues.filter(value => value >= lowerBound && value <= upperBound)
53
- // **Outliers** =
54
- const outliers = sortedValues.filter(v => v < lowerBound || v > upperBound)
55
- const whiskerMax =
56
- sortedValues
57
- .slice()
58
- .reverse()
59
- .find(v => v <= upperBound) ?? thirdQuartile
60
- const whiskerMin = nonOutliers.length > 0 ? nonOutliers[0] : firstQuartile
61
- // Calculate Box Plot Stats
62
- return {
63
- min: whiskerMin,
64
- max: whiskerMax,
65
- median: d3.median(sortedValues),
66
- firstQuartile,
67
- thirdQuartile,
68
- iqr,
69
- outliers
70
- }
71
- }
72
-
73
32
  const getValuesBySeriesKey = (group: string, config, data) => {
74
33
  const allSeriesKeys = config.series.map(item => item?.dataKey)
75
34
  const result = {}
@@ -81,29 +40,11 @@ const getValuesBySeriesKey = (group: string, config, data) => {
81
40
  return result
82
41
  }
83
42
 
84
- // Helper to calculate outliers based on IQR
85
- const calculateOutliers = (values: number[], firstQuartile: number, thirdQuartile: number) => {
86
- const iqr = thirdQuartile - firstQuartile
87
- const lowerBound = firstQuartile - 1.5 * iqr
88
- const upperBound = thirdQuartile + 1.5 * iqr
89
- return values.filter(value => value < lowerBound || value > upperBound)
90
- }
91
-
92
- // Helper to calculate non-outliers based on IQR
93
- const calculateNonOutliers = (values: number[], firstQuartile: number, thirdQuartile: number): number[] => {
94
- const iqr = thirdQuartile - firstQuartile
95
- const lowerBound = firstQuartile - 1.5 * iqr
96
- const upperBound = thirdQuartile + 1.5 * iqr
97
-
98
- // Return values within the bounds
99
- return values.filter(value => value >= lowerBound && value <= upperBound)
100
- }
101
-
102
43
  // Main function to create plots with additional outlier data
103
44
  export const createPlots = (data, config) => {
104
45
  const dataKeys = data.map(d => d[config.xAxis.dataKey])
105
46
  const plots: Plot[] = []
106
- const groups: string[] = _.uniq(dataKeys)
47
+ const groups: string[] = uniq(dataKeys)
107
48
 
108
49
  if (groups && groups.length > 0) {
109
50
  groups.forEach(group => {
@@ -119,29 +60,18 @@ export const createPlots = (data, config) => {
119
60
 
120
61
  // Calculate outliers and non-outliers for each series key
121
62
  Object.keys(keyValues).forEach(key => {
122
- const raw = keyValues[key] ?? []
123
-
124
- // 2) normalize → trim, drop empties/non-numbers, coerce to Number
125
- const cleaned: number[] = raw
126
- .map(v => (typeof v === 'string' ? v.trim() : v)) // trim strings
127
- .filter(v => v != null && v !== '' && !isNaN(+v)) // drop null/''/non-nums
128
- .map(v => +v)
129
-
130
- if (cleaned.length === 0) {
131
- return
132
- }
63
+ const stats = calculateBoxPlotStats(keyValues[key] ?? [])
64
+ if (!stats) return
133
65
 
134
66
  // Calculate box plot statistics
135
- const { firstQuartile, thirdQuartile, min, max, median, iqr, outliers } = calculateBoxPlotStats(cleaned)
136
- // Calculate outliers and non-outliers
137
- columnOutliers[key] = calculateOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
138
- columnNonOutliers[key] = calculateNonOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
139
- columnMedian[key] = median
140
- columnMin[key] = Number(min)
141
- columnMax[key] = Number(max)
142
- columnQ1[key] = firstQuartile
143
- columnQ3[key] = thirdQuartile
144
- 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
145
75
  })
146
76
 
147
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'
@@ -128,7 +129,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
128
129
  <>
129
130
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
130
131
  const patternId = getChartPatternId(key)
131
- const size = pattern.patternSize || 8
132
+ const size = pattern.patternSize || 10
132
133
 
133
134
  switch (pattern.shape) {
134
135
  case 'circles':
@@ -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
@@ -4,6 +4,8 @@ import * as allCurves from '@visx/curve'
4
4
  import { handleLineType } from '../../helpers/handleLineType'
5
5
  import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
6
6
  import { getPatternUrl as getPatternUrlForBar } from '../BarChart/helpers/getPatternUrl'
7
+ import { getPortionPatternRenderData } from '../BarChart/helpers/portionPattern'
8
+ import { PortionPatternBoundary } from '../BarChart/components/PortionPatternOverlay'
7
9
 
8
10
  interface MiniChartPreviewProps {
9
11
  series: any[]
@@ -141,12 +143,20 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
141
143
  seriesKeys: patternSeriesKeys,
142
144
  allowNonSeriesFieldMatch: allowGroupedNonSeriesFieldMatch
143
145
  })
144
-
145
146
  // Calculate bar position and height
146
147
  const valueY = miniYScale(value)
147
148
  const barHeight = Math.abs(valueY - zeroY)
148
149
  const barX = x - barWidth / 2 + seriesIndex * groupBarWidth
149
150
  const y = Math.min(valueY, zeroY) // Top of bar (smaller Y value)
151
+ const portionPatternRenderData = getPortionPatternRenderData({
152
+ config,
153
+ orientation: 'vertical',
154
+ bounds: { x: barX, y, width: groupBarWidth, height: barHeight },
155
+ patterns: config.legend?.patterns,
156
+ datum: d,
157
+ seriesKey: s.dataKey,
158
+ totalValue: value
159
+ })
150
160
 
151
161
  bars.push(
152
162
  <rect
@@ -178,6 +188,28 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
178
188
  />
179
189
  )
180
190
  }
191
+ if (portionPatternRenderData) {
192
+ bars.push(
193
+ <rect
194
+ key={`mini-bar-grouped-portion-pattern-${i}-${seriesIndex}`}
195
+ className='pattern-overlay pattern-overlay--portion pattern-overlay--brush'
196
+ {...portionPatternRenderData.geometry}
197
+ fill={portionPatternRenderData.patternUrl}
198
+ stroke='transparent'
199
+ strokeWidth={0}
200
+ fillOpacity={1}
201
+ pointerEvents='none'
202
+ />
203
+ )
204
+
205
+ bars.push(
206
+ <PortionPatternBoundary
207
+ key={`mini-bar-grouped-portion-pattern-boundary-${i}-${seriesIndex}`}
208
+ renderData={portionPatternRenderData}
209
+ brush
210
+ />
211
+ )
212
+ }
181
213
  })
182
214
  }
183
215
  })