@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,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,7 +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 { getSeriesColumnFormattingParams } from '../../helpers/seriesColumnSettings'
8
+ import { getSeriesValueLabel } from '@cdc/core/helpers/getSeriesName'
9
+ import {
10
+ findColumnConfigByName,
11
+ getAdditionalColumnFormattingParams,
12
+ getSeriesColumnFormattingParams,
13
+ getSeriesOwnedColumnNames
14
+ } from '../../helpers/seriesColumnSettings'
9
15
 
10
16
  const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
11
17
  const {
@@ -24,9 +30,21 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
24
30
  // Track current hover for analytics
25
31
  const [currentHover, setCurrentHover] = useState({ dataIndex: null, seriesKey: null })
26
32
  // tooltips for additional columns
33
+ const seriesOwnedColumnNames = getSeriesOwnedColumnNames([
34
+ ...(config.series || []),
35
+ ...(config.runtime?.series || [])
36
+ ])
27
37
  const additionalColumns = Object.entries(config.columns)
28
- .filter(([_, value]) => value.tooltips)
29
- .map(([_, value]) => [value.label || value.name, value.name, getSeriesColumnFormattingParams(value)])
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
+ })
30
48
  const handleTooltip = (item, s, dataIndex) => `<div>
31
49
  ${buildSeriesTooltipListHtml({
32
50
  config,
@@ -36,7 +54,13 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
36
54
  'bottom'
37
55
  )}`,
38
56
  seriesKey: s,
39
- 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
+ )}`,
40
64
  extraRows: additionalColumns
41
65
  .map(
42
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
+ }