@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,12 @@
1
+ const escapeHtml = (value: unknown) =>
2
+ String(value ?? '')
3
+ .replace(/&/g, '&')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&#39;')
8
+
9
+ export const getSankeyTooltip = (label: string, value?: string) => `<div class="sankey-chart__tooltip">
10
+ <span class="sankey-chart__tooltip--tooltip-header">${escapeHtml(label)}</span>
11
+ ${value ? `<span>${escapeHtml(value)}</span>` : ''}
12
+ </div>`
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { prepareSankeyData } from './prepareSankeyData'
3
+
4
+ describe('prepareSankeyData', () => {
5
+ it('accepts tabular source target value rows', () => {
6
+ const result = prepareSankeyData([
7
+ { source: 'Screened', target: 'Eligible', value: '850' },
8
+ { source: 'Eligible', target: 'Enrolled', value: 700 }
9
+ ])
10
+
11
+ expect(result.nodes.map(node => node.id)).toEqual(['Screened', 'Eligible', 'Enrolled'])
12
+ expect(result.links).toMatchObject([
13
+ { source: 'Screened', target: 'Eligible', value: 850 },
14
+ { source: 'Eligible', target: 'Enrolled', value: 700 }
15
+ ])
16
+ expect(result.rejectedRowCount).toBe(0)
17
+ })
18
+
19
+ it('handles common case-insensitive field names and comma-formatted values', () => {
20
+ const result = prepareSankeyData([{ Source: 'A', Target: 'B', Value: '1,250' }])
21
+
22
+ expect(result.links[0]).toMatchObject({ source: 'A', target: 'B', value: 1250 })
23
+ })
24
+
25
+ it('uses configured columns for arbitrary imported headers', () => {
26
+ const result = prepareSankeyData(
27
+ [
28
+ { Step: 'Screened', Next: 'Eligible', Patients: '850' },
29
+ { Step: 'Eligible', Next: 'Enrolled', Patients: 700 }
30
+ ],
31
+ { source: 'Step', target: 'Next', value: 'Patients' }
32
+ )
33
+
34
+ expect(result.links).toMatchObject([
35
+ { source: 'Screened', target: 'Eligible', value: 850 },
36
+ { source: 'Eligible', target: 'Enrolled', value: 700 }
37
+ ])
38
+ expect(result.rejectedRowCount).toBe(0)
39
+ })
40
+
41
+ it('prefers configured columns before alias fallbacks', () => {
42
+ const result = prepareSankeyData(
43
+ [
44
+ {
45
+ source: 'Alias Source',
46
+ target: 'Alias Target',
47
+ value: 1,
48
+ Step: 'Mapped Source',
49
+ Next: 'Mapped Target',
50
+ Patients: 2
51
+ }
52
+ ],
53
+ { source: 'Step', target: 'Next', value: 'Patients' }
54
+ )
55
+
56
+ expect(result.links[0]).toMatchObject({ source: 'Mapped Source', target: 'Mapped Target', value: 2 })
57
+ })
58
+
59
+ it('combines duplicate links and rejects missing, zero, and invalid values', () => {
60
+ const result = prepareSankeyData([
61
+ { source: 'A', target: 'B', value: 10 },
62
+ { source: 'A', target: 'B', value: 15 },
63
+ { source: 'A', target: 'C', value: 0 },
64
+ { source: 'A', target: 'D', value: 'not a number' },
65
+ { source: '', target: 'E', value: 4 }
66
+ ])
67
+
68
+ expect(result.links).toMatchObject([{ source: 'A', target: 'B', value: 25 }])
69
+ expect(result.rejectedRowCount).toBe(3)
70
+ })
71
+ })
@@ -0,0 +1,90 @@
1
+ import type { SankeyColumns, SankeyPreparedData, SankeyPreparedLink, SankeyRawRow } from '../types'
2
+
3
+ const SOURCE_FIELDS = ['source', 'from']
4
+ const TARGET_FIELDS = ['target', 'to']
5
+ const VALUE_FIELDS = ['value', 'weight', 'count']
6
+
7
+ const getFieldValue = (row: SankeyRawRow, fieldNames: string[], selectedFieldName?: string) => {
8
+ const keys = Object.keys(row)
9
+ const selectedField = selectedFieldName?.trim()
10
+
11
+ if (selectedField) {
12
+ if (Object.prototype.hasOwnProperty.call(row, selectedField)) return row[selectedField]
13
+
14
+ const matchingSelectedKey = keys.find(key => key.toLowerCase() === selectedField.toLowerCase())
15
+ if (matchingSelectedKey) return row[matchingSelectedKey]
16
+ }
17
+
18
+ for (const fieldName of fieldNames) {
19
+ if (Object.prototype.hasOwnProperty.call(row, fieldName)) return row[fieldName]
20
+
21
+ const matchingKey = keys.find(key => key.toLowerCase() === fieldName)
22
+ if (matchingKey) return row[matchingKey]
23
+ }
24
+ }
25
+
26
+ const toLabel = (value: unknown) => {
27
+ if (value === undefined || value === null) return ''
28
+ return String(value).trim()
29
+ }
30
+
31
+ const toPositiveNumber = (value: unknown) => {
32
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : null
33
+ if (typeof value !== 'string') return null
34
+
35
+ const normalizedValue = Number(value.replace(/,/g, '').trim())
36
+ return Number.isFinite(normalizedValue) && normalizedValue > 0 ? normalizedValue : null
37
+ }
38
+
39
+ export const getSankeyInputRows = (data: unknown): SankeyRawRow[] => {
40
+ if (!Array.isArray(data)) return []
41
+ return data.filter(row => row && typeof row === 'object') as SankeyRawRow[]
42
+ }
43
+
44
+ export const prepareSankeyData = (data: unknown, columns: SankeyColumns = {}): SankeyPreparedData => {
45
+ const rows = getSankeyInputRows(data)
46
+ const linksByPair = new Map<string, SankeyPreparedLink>()
47
+ let rejectedRowCount = 0
48
+
49
+ rows.forEach(row => {
50
+ const source = toLabel(getFieldValue(row, SOURCE_FIELDS, columns.source))
51
+ const target = toLabel(getFieldValue(row, TARGET_FIELDS, columns.target))
52
+ const value = toPositiveNumber(getFieldValue(row, VALUE_FIELDS, columns.value))
53
+
54
+ if (!source || !target || value === null) {
55
+ rejectedRowCount += 1
56
+ return
57
+ }
58
+
59
+ const key = JSON.stringify([source, target])
60
+ const existingLink = linksByPair.get(key)
61
+
62
+ if (existingLink) {
63
+ existingLink.value += value
64
+ existingLink.rows.push(row)
65
+ return
66
+ }
67
+
68
+ linksByPair.set(key, {
69
+ id: `${source}--${target}`,
70
+ source,
71
+ target,
72
+ value,
73
+ rows: [row]
74
+ })
75
+ })
76
+
77
+ const nodeIds = new Set<string>()
78
+ const links = Array.from(linksByPair.values())
79
+
80
+ links.forEach(link => {
81
+ nodeIds.add(link.source)
82
+ nodeIds.add(link.target)
83
+ })
84
+
85
+ return {
86
+ links,
87
+ nodes: Array.from(nodeIds).map(id => ({ id })),
88
+ rejectedRowCount
89
+ }
90
+ }
@@ -1,66 +1,85 @@
1
- /* KPI */
2
- .kpis-container {
3
- display: flex;
4
- flex-direction: row;
5
- column-gap: 30px;
6
- }
7
-
8
1
  .cove-visualization .sankey-chart {
9
- --font-size-small: 12px;
10
- --font-size-medium: 16px;
11
- --font-size-large: 18px;
12
- --font-size-xl: 24px;
2
+ --sankey-label-size: 0.875rem;
13
3
 
14
- --storynode-font-size--small: 24px;
15
- --storynode-font-size--medium: 28px;
16
- --storynode-font-size--large: 32px;
4
+ min-height: 16rem;
5
+ width: 100%;
17
6
 
18
- --font-weight-normal: 400;
19
- --font-weight-bold: 700;
7
+ &--compact {
8
+ --sankey-label-size: 0.6875rem;
9
+ --sankey-story-label-size: 0.75rem;
10
+ }
20
11
 
21
- overflow: visible;
12
+ &--empty {
13
+ align-items: center;
14
+ border: 1px dashed #b7b7b7;
15
+ color: #555;
16
+ display: flex;
17
+ justify-content: center;
18
+ min-height: 12rem;
19
+ padding: 1rem;
20
+ text-align: center;
21
+ }
22
22
 
23
- .divider {
24
- border-top: 1px solid #000;
25
- margin: 10px 0;
23
+ .sankey-chart__diagram {
24
+ display: block;
25
+ font-family: 'Roboto', sans-serif;
26
+ max-width: 100%;
27
+ overflow: visible;
26
28
  }
27
29
 
28
- .alert-dismissible {
29
- padding-right: 4rem !important;
30
+ .sankey-chart__scroll-area {
31
+ overflow-x: auto;
32
+ overflow-y: hidden;
33
+ -webkit-overflow-scrolling: touch;
30
34
  }
31
35
 
32
- svg.sankey-chart__diagram {
33
- position: relative;
34
- font-family: 'Roboto', sans-serif;
35
- height: auto;
36
- width: 100%;
36
+ &--compact,
37
+ &--scrollable {
38
+ .sankey-chart__scroll-area {
39
+ padding-bottom: 0.5rem;
40
+ }
41
+
42
+ .sankey-chart__diagram {
43
+ max-width: none;
44
+ }
37
45
  }
38
46
 
39
- .node-id {
40
- font-weight: var(--font-weight-bold);
47
+ .sankey-chart__link,
48
+ .sankey-chart__node {
49
+ cursor: pointer;
50
+ transition: opacity 150ms ease, stroke-opacity 150ms ease, fill-opacity 150ms ease;
41
51
  }
42
52
 
43
- .node-value {
44
- font-weight: var(--font-weight-normal);
53
+ .sankey-chart__link {
54
+ pointer-events: stroke;
45
55
  }
46
56
 
47
- .node-text {
48
- font-weight: var(--font-weight-normal);
57
+ .sankey-chart__node:focus {
58
+ outline: none;
59
+ stroke: #1c1d1f;
60
+ stroke-width: 2px;
49
61
  }
50
62
 
51
- .node-value--storynode {
52
- font-weight: var(--font-weight-bold);
63
+ .sankey-chart__label {
64
+ font-size: var(--sankey-label-size);
65
+ letter-spacing: 0;
66
+ }
67
+
68
+ .divider {
69
+ border-top: 1px solid #000;
70
+ margin: 10px 0;
53
71
  }
54
72
 
55
- /* Hover card */
56
73
  .sankey-chart__tooltip {
57
74
  color: black;
58
75
  display: flex;
59
76
  flex-direction: column;
60
77
  margin: 10px;
78
+
61
79
  &--tooltip-header {
62
- font-weight: var(--font-weight-bold);
80
+ font-weight: 700;
63
81
  }
82
+
64
83
  &--info-section {
65
84
  column-gap: 10px;
66
85
  display: flex;
@@ -71,84 +90,6 @@
71
90
 
72
91
  span {
73
92
  max-width: 500px;
74
- word-wrap: break-word;
75
- }
76
-
77
- /* Autoscaling */
78
- //large - default
79
- @media only screen and (min-width: 1200px) {
80
- min-width: none;
81
- .node-text {
82
- font-size: var(--font-size-xl);
83
- }
84
- .node-value--storynode {
85
- font-size: var(--storynode-font-size--large);
86
- }
87
- .node-id {
88
- font-size: var(--font-size-large);
89
- }
90
- .node-value {
91
- font-size: var(--font-size-large);
92
- }
93
+ overflow-wrap: break-word;
93
94
  }
94
- //medium
95
- @media only screen and (max-width: 1199px) {
96
- .node-text {
97
- font-size: var(--font-size-medium);
98
- }
99
- .node-value--storynode {
100
- font-size: var(--storynode-font-size--medium);
101
- }
102
- .node-id {
103
- font-size: var(--font-size-medium);
104
- }
105
- .node-value {
106
- font-size: var(--font-size-medium);
107
- }
108
- }
109
-
110
- // //small
111
- // @media only screen and (max-width: 799px) {
112
- // .node-text {
113
- // font-size: var(--font-size-small);
114
- // }
115
- // .node-value--storynode {
116
- // font-size: var(--storynode-font-size--small);
117
- // }
118
- // .node-id {
119
- // font-size: var(--font-size-small);
120
- // }
121
- // .node-value {
122
- // font-size: var(--font-size-small);
123
- // }
124
- // }
125
-
126
- //x-small
127
- @media only screen and (max-width: 600px) {
128
- .popup {
129
- display: block; /* Show the popup on smaller screens */
130
- }
131
- }
132
- }
133
-
134
- /* Pop up */
135
-
136
- .popup {
137
- position: absolute;
138
- top: 50%;
139
- left: 50%;
140
- transform: translate(-50%, -50%);
141
- background-color: beige;
142
- border: 2px solid gray !important;
143
- border-radius: 8px;
144
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
145
- width: 80%;
146
- z-index: 999;
147
- display: none;
148
- }
149
-
150
- .popup-content {
151
- font-size: 30px;
152
- padding: 10px;
153
- text-align: center;
154
95
  }
@@ -1,16 +1,53 @@
1
- export type Link = { source: string; target: string; value: number; id: string }
1
+ export type SankeyRawRow = Record<string, unknown>
2
2
 
3
- type Data = {
4
- links: Link[]
3
+ export type SankeyColumns = {
4
+ source?: string
5
+ target?: string
6
+ value?: string
7
+ }
8
+
9
+ export type SankeyPreparedLink = {
10
+ id: string
11
+ source: string
12
+ target: string
13
+ value: number
14
+ rows: SankeyRawRow[]
5
15
  }
6
16
 
7
17
  export type SankeyNode = {
8
18
  id: string
9
19
  }
10
20
 
21
+ export type SankeyLayoutNode = SankeyNode & {
22
+ depth?: number
23
+ height?: number
24
+ index?: number
25
+ layer?: number
26
+ sourceLinks?: SankeyLayoutLink[]
27
+ targetLinks?: SankeyLayoutLink[]
28
+ value?: number
29
+ x0?: number
30
+ x1?: number
31
+ y0?: number
32
+ y1?: number
33
+ }
34
+
35
+ export type SankeyLayoutLink = Omit<SankeyPreparedLink, 'source' | 'target'> & {
36
+ source: SankeyLayoutNode
37
+ target: SankeyLayoutNode
38
+ width?: number
39
+ y0?: number
40
+ y1?: number
41
+ }
42
+
43
+ export type SankeyPreparedData = {
44
+ links: SankeyPreparedLink[]
45
+ nodes: SankeyNode[]
46
+ rejectedRowCount: number
47
+ }
48
+
11
49
  export type SankeyProps = {
12
50
  width: number
13
51
  height: number
14
- data: Data
15
52
  runtime: any
16
53
  }
@@ -5,6 +5,13 @@ import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
5
5
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
6
6
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
7
7
  import { buildSeriesTooltipListHtml } from '../../helpers/tooltipHelpers'
8
+ import { getSeriesValueLabel } from '@cdc/core/helpers/getSeriesName'
9
+ import {
10
+ findColumnConfigByName,
11
+ getAdditionalColumnFormattingParams,
12
+ getSeriesColumnFormattingParams,
13
+ getSeriesOwnedColumnNames
14
+ } from '../../helpers/seriesColumnSettings'
8
15
 
9
16
  const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
10
17
  const {
@@ -23,18 +30,21 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
23
30
  // Track current hover for analytics
24
31
  const [currentHover, setCurrentHover] = useState({ dataIndex: null, seriesKey: null })
25
32
  // tooltips for additional columns
33
+ const seriesOwnedColumnNames = getSeriesOwnedColumnNames([
34
+ ...(config.series || []),
35
+ ...(config.runtime?.series || [])
36
+ ])
26
37
  const additionalColumns = Object.entries(config.columns)
27
- .filter(([_, value]) => value.tooltips)
28
- .map(([_, value]) => [
29
- value.label || value.name,
30
- value.name,
31
- {
32
- addColPrefix: value.prefix,
33
- addColSuffix: value.suffix,
34
- addColRoundTo: value.roundToPlace,
35
- addColCommas: value.commas
36
- }
37
- ])
38
+ .filter(([columnKey, value]) => {
39
+ const columnName = value.name || columnKey
40
+ if (!value.tooltips || !columnName) return false
41
+ if (columnName === config.xAxis.dataKey) return false
42
+ return !seriesOwnedColumnNames.includes(columnName)
43
+ })
44
+ .map(([columnKey, value]) => {
45
+ const columnName = value.name || columnKey
46
+ return [value.label || columnName, columnName, getAdditionalColumnFormattingParams(value)]
47
+ })
38
48
  const handleTooltip = (item, s, dataIndex) => `<div>
39
49
  ${buildSeriesTooltipListHtml({
40
50
  config,
@@ -44,7 +54,13 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
44
54
  'bottom'
45
55
  )}`,
46
56
  seriesKey: s,
47
- seriesText: `${config.runtime.seriesLabels[s] || s}: ${formatNumber(item[s], 'left')}`,
57
+ seriesText: `${getSeriesValueLabel(s, config)}: ${formatColNumber(
58
+ item[s],
59
+ 'left',
60
+ false,
61
+ config,
62
+ getSeriesColumnFormattingParams(findColumnConfigByName(config.columns || {}, s)?.columnConfig)
63
+ )}`,
48
64
  extraRows: additionalColumns
49
65
  .map(
50
66
  ([label, name, options]) =>
@@ -0,0 +1,60 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import ConfigContext from '../../ConfigContext'
5
+ import ScatterPlot from './ScatterPlot'
6
+ import { createMockChartContext, createMockConfig } from '../LinearChart/tests/mockConfigContext'
7
+
8
+ const renderScatterPlot = (casesLabel: string) => {
9
+ const baseConfig = createMockConfig()
10
+ const data = [{ Year: '2024', Cases: 10 }]
11
+ const config = createMockConfig({
12
+ visualizationType: 'Scatter Plot',
13
+ xAxis: { ...baseConfig.xAxis, dataKey: 'Year', label: 'Year', type: 'categorical' },
14
+ series: [{ dataKey: 'Cases', name: 'Reported Cases', type: 'Scatter Plot', axis: 'Left', tooltip: true }] as any,
15
+ columns: {
16
+ Cases: { name: 'Cases', label: casesLabel }
17
+ } as any,
18
+ general: { showMissingDataLabel: true, hideNullValue: true } as any,
19
+ legend: { ...baseConfig.legend, behavior: 'highlight', hide: false },
20
+ runtime: {
21
+ ...baseConfig.runtime,
22
+ xAxis: { ...baseConfig.runtime.xAxis, dataKey: 'Year', label: 'Year', type: 'categorical' },
23
+ series: [{ dataKey: 'Cases', name: 'Reported Cases', type: 'Scatter Plot', axis: 'Left', tooltip: true }],
24
+ seriesKeys: ['Cases'],
25
+ seriesLabels: { Cases: 'Reported Cases' },
26
+ seriesLabelsAll: ['Reported Cases']
27
+ } as any
28
+ })
29
+ const context = createMockChartContext(config, {
30
+ transformedData: data,
31
+ tableData: data,
32
+ colorScale: () => '#005ea8',
33
+ formatNumber: value => String(value)
34
+ })
35
+
36
+ return render(
37
+ <ConfigContext.Provider value={context}>
38
+ <svg>
39
+ <ScatterPlot xScale={() => 10} yScale={() => 10} yAxisWidth={0} getXAxisData={row => row.Year} />
40
+ </svg>
41
+ </ConfigContext.Provider>
42
+ )
43
+ }
44
+
45
+ describe('ScatterPlot tooltips', () => {
46
+ it('uses a customized Label instead of the Series Name', () => {
47
+ const { container } = renderScatterPlot('Case Rate')
48
+ const tooltip = container.querySelector('circle')?.getAttribute('data-tooltip-html') || ''
49
+
50
+ expect(tooltip).toContain('Case Rate: 10')
51
+ expect(tooltip).not.toContain('Reported Cases: 10')
52
+ })
53
+
54
+ it('falls back to the Series Name when the Label is cleared', () => {
55
+ const { container } = renderScatterPlot('')
56
+ const tooltip = container.querySelector('circle')?.getAttribute('data-tooltip-html') || ''
57
+
58
+ expect(tooltip).toContain('Reported Cases: 10')
59
+ })
60
+ })
@@ -6,6 +6,7 @@ import useScales from '../../hooks/useScales'
6
6
  import { hasSpacedInlineLabel } from '../../helpers/hasSpacedInlineLabel'
7
7
  import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
8
8
  import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
9
+ import { sortByNumericText } from '@cdc/core/helpers/sorting'
9
10
  import './SmallMultiples.css'
10
11
  import { ChartConfig } from '../../types/ChartConfig'
11
12
 
@@ -63,9 +64,9 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({
63
64
  seriesKey: series.dataKey
64
65
  }))
65
66
  } else if (mode === 'by-column') {
66
- const uniqueValues = Array.from(new Set(data.map(row => row[tileColumn])))
67
- .filter(val => val != null)
68
- .sort()
67
+ const uniqueValues = sortByNumericText(
68
+ Array.from(new Set(data.map(row => row[tileColumn]))).filter(val => val != null)
69
+ )
69
70
  items = uniqueValues.map(value => ({
70
71
  key: value,
71
72
  mode: 'by-column' as const,
@@ -94,15 +95,14 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({
94
95
  const sharedYAxisTileItems = useMemo<Array<TileItem>>(
95
96
  () =>
96
97
  mode === 'by-column'
97
- ? Array.from(new Set(dataForSharedYAxis.map(row => row[tileColumn])))
98
- .filter(val => val != null)
99
- .sort()
100
- .map(value => ({
101
- key: value,
102
- mode: 'by-column' as const,
103
- tileValue: value,
104
- tileColumn: tileColumn
105
- }))
98
+ ? sortByNumericText(
99
+ Array.from(new Set(dataForSharedYAxis.map(row => row[tileColumn]))).filter(val => val != null)
100
+ ).map(value => ({
101
+ key: value,
102
+ mode: 'by-column' as const,
103
+ tileValue: value,
104
+ tileColumn: tileColumn
105
+ }))
106
106
  : tileItems,
107
107
  [mode, dataForSharedYAxis, tileColumn, tileItems]
108
108
  )
@@ -9,11 +9,13 @@ const linearChartConfigs = vi.hoisted(() => [] as any[])
9
9
 
10
10
  vi.stubGlobal(
11
11
  'ResizeObserver',
12
- vi.fn(callback => ({
13
- observe: vi.fn(() => callback([{ contentRect: { width: 200, height: 300 } }])),
14
- unobserve: vi.fn(),
15
- disconnect: vi.fn()
16
- }))
12
+ vi.fn(function ResizeObserver(callback) {
13
+ return {
14
+ observe: vi.fn(() => callback([{ contentRect: { width: 200, height: 300 } }])),
15
+ unobserve: vi.fn(),
16
+ disconnect: vi.fn()
17
+ }
18
+ })
17
19
  )
18
20
 
19
21
  vi.mock('@visx/responsive/lib/components/ParentSize', () => ({
@@ -0,0 +1,18 @@
1
+ export type RaceTiming = {
2
+ elapsedSeconds: number
3
+ frameKey: string
4
+ isPlaying: boolean
5
+ totalSeconds: number
6
+ }
7
+
8
+ export const DEFAULT_RACE_SECONDS_PER_FRAME = 0.5
9
+ export const RACE_SECONDS_PER_FRAME_OPTIONS = [0, DEFAULT_RACE_SECONDS_PER_FRAME, 1, 1.5] as const
10
+
11
+ export const clampRaceSecondsPerFrame = (value: unknown) => {
12
+ if (value === null || value === '') return DEFAULT_RACE_SECONDS_PER_FRAME
13
+ const parsed = Number(value)
14
+ if (!Number.isFinite(parsed)) return DEFAULT_RACE_SECONDS_PER_FRAME
15
+ return RACE_SECONDS_PER_FRAME_OPTIONS.reduce((closest, option) =>
16
+ Math.abs(option - parsed) <= Math.abs(closest - parsed) ? option : closest
17
+ )
18
+ }