@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
@@ -1,489 +1,316 @@
1
- import { useContext, useState, useRef, useEffect } from 'react'
1
+ import { useContext, useMemo, useState } from 'react'
2
2
 
3
3
  // External Libraries
4
4
  import { Tooltip as ReactTooltip } from 'react-tooltip'
5
- import { SankeyGraph, sankey, sankeyLinkHorizontal, sankeyLeft } from 'd3-sankey'
5
+ import { sankey, sankeyJustify, sankeyLinkHorizontal } from 'd3-sankey'
6
6
  import { Group } from '@visx/group'
7
7
  import { Text } from '@visx/text'
8
+ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
8
9
 
9
10
  // Cdc
10
11
  import './../sankey.scss'
11
12
  import 'react-tooltip/dist/react-tooltip.css'
12
13
  import ConfigContext from '../../../ConfigContext'
13
14
  import type { ChartContext } from '../../../types/ChartContext'
14
- import type { SankeyNode, SankeyProps } from '../types'
15
- import useSankeyAlert from '../useSankeyAlert'
16
15
  import { getSankeyTooltip } from '../helpers/getSankeyTooltip'
16
+ import { prepareSankeyData } from '../helpers/prepareSankeyData'
17
+ import type { SankeyLayoutLink, SankeyLayoutNode, SankeyProps } from '../types'
18
+
19
+ const formatNodeValue = (value: unknown, locale?: string) =>
20
+ typeof value === 'number' ? value.toLocaleString(locale) : String(value ?? '')
17
21
 
18
22
  const Sankey = ({ width, height, runtime }: SankeyProps) => {
19
- const { config, handleChartAriaLabels } = useContext<ChartContext>(ConfigContext)
23
+ const { colorPalettes, config, handleChartAriaLabels } = useContext<ChartContext>(ConfigContext)
20
24
  const { sankey: sankeyConfig } = config
25
+ const [activeNode, setActiveNode] = useState<string>('')
21
26
 
22
- const a11y = handleChartAriaLabels(config)
23
- const [largestGroupWidth, setLargestGroupWidth] = useState(0)
24
- const [tooltipID, setTooltipID] = useState<string>('')
25
- const { showAlert, alert } = useSankeyAlert()
26
- const groupRefs = useRef([])
27
-
28
- const handleNodeClick = (nodeId: string) => {
29
- // Store the previous tooltipID
30
- const previousTooltipID = tooltipID
31
-
32
- // If the previous tooltipID exists, clear it
33
- if (previousTooltipID) {
34
- setTooltipID('')
35
- }
36
-
37
- // Update the tooltipID with the new nodeId if it's different from the previous one
38
- if (previousTooltipID !== nodeId) {
39
- setTooltipID(nodeId)
40
- }
41
- }
27
+ const preparedData = useMemo(
28
+ () => prepareSankeyData(config?.data, sankeyConfig?.columns),
29
+ [config?.data, sankeyConfig?.columns]
30
+ )
31
+ const paletteColors = useMemo(() => {
32
+ const orderedCustomColors = config.general?.palette?.customColorsOrdered
33
+ const colors =
34
+ Array.isArray(orderedCustomColors) && orderedCustomColors.length > 0
35
+ ? orderedCustomColors
36
+ : getPaletteColors(config, colorPalettes)
37
+ const validColors = colors.filter(color => typeof color === 'string' && color.trim() !== '')
38
+
39
+ return validColors.length ? validColors : [sankeyConfig?.nodeColor?.default || '#005eaa']
40
+ }, [colorPalettes, config, sankeyConfig?.nodeColor?.default])
41
+
42
+ const paletteColorPair = useMemo(
43
+ () => ({
44
+ node: paletteColors[0] || sankeyConfig?.nodeColor?.default || '#005eaa',
45
+ link: paletteColors[paletteColors.length - 1] || sankeyConfig?.linkColor?.default || paletteColors[0] || '#7bafd4'
46
+ }),
47
+ [paletteColors, sankeyConfig?.linkColor?.default, sankeyConfig?.nodeColor?.default]
48
+ )
42
49
 
43
- // Uses Visx Groups innerRef to get all Group elements that are mapped.
44
- // Sets the largest group width in state and subtracts that group the svg width to calculate overall width.
45
- useEffect(() => {
46
- let largest = 0
47
- groupRefs?.current?.map(g => {
48
- const groupWidth = g?.getBBox?.().width || g?.getBoundingClientRect?.().width || 0
49
- if (groupWidth > largest) {
50
- largest = groupWidth
50
+ const layout = useMemo(() => {
51
+ const containerWidth = Number(width) || 640
52
+ const isCompact = containerWidth < 520
53
+ const configuredHorizontalScrollWidth = sankeyConfig?.horizontalScrollWidth
54
+ const hasHorizontalScrollWidth =
55
+ (typeof configuredHorizontalScrollWidth === 'number' &&
56
+ Number.isFinite(configuredHorizontalScrollWidth) &&
57
+ configuredHorizontalScrollWidth > 0) ||
58
+ (typeof configuredHorizontalScrollWidth === 'string' &&
59
+ configuredHorizontalScrollWidth.trim() !== '' &&
60
+ Number.isFinite(Number(configuredHorizontalScrollWidth)) &&
61
+ Number(configuredHorizontalScrollWidth) > 0)
62
+ const horizontalScrollWidth = hasHorizontalScrollWidth ? Number(configuredHorizontalScrollWidth) : containerWidth
63
+ const isScrollable = hasHorizontalScrollWidth && containerWidth < horizontalScrollWidth
64
+ const labelWidth = isCompact
65
+ ? Math.max(64, Math.min(96, Math.floor(containerWidth * 0.28)))
66
+ : Math.max(128, Math.min(180, Math.floor(containerWidth * 0.22)))
67
+ const horizontalMargin = Math.max(Number(sankeyConfig?.margin?.margin_x) || 0, labelWidth + 16)
68
+ const topMargin = Math.max(Number(sankeyConfig?.margin?.margin_y) || 0, 18)
69
+ const bottomMargin = 28
70
+ const layoutWidth = Math.max(isScrollable ? horizontalScrollWidth : containerWidth, horizontalMargin * 2 + 120)
71
+ const configuredHeight = Number(config?.heights?.vertical) || Number(height) || 500
72
+ const minimumHeight = Math.max(
73
+ isCompact ? 360 : 420,
74
+ preparedData.nodes.length * (isCompact ? 28 : 34),
75
+ preparedData.links.length * 20
76
+ )
77
+ const layoutHeight = Math.max(configuredHeight, minimumHeight)
78
+ const nodeWidth = Math.max(10, Math.min(Number(sankeyConfig?.nodeSize?.nodeWidth) || 18, isCompact ? 14 : 24))
79
+ const nodePadding = Math.max(8, Math.min(Number(sankeyConfig?.nodePadding) || 24, isCompact ? 16 : 32))
80
+ const iterations = Math.max(Number(sankeyConfig?.iterations) || 1, 6)
81
+
82
+ try {
83
+ const graph = {
84
+ nodes: preparedData.nodes.map(node => ({ ...node })),
85
+ links: preparedData.links.map(link => ({ ...link }))
51
86
  }
52
- })
53
- setLargestGroupWidth(largest)
54
- }, [groupRefs, sankeyConfig, window.innerWidth])
55
-
56
- if (config.visualizationType !== 'Sankey') return
57
87
 
58
- const data = config?.data[0]
59
-
60
- //Retrieve all the unique values for the Nodes
61
- const uniqueNodes = Array.from(new Set(data?.links?.flatMap(link => [link.source, link.target])))
62
-
63
- // Convert JSON data to the format required
64
- const sankeyData: SankeyGraph<SankeyNode, { source: number; target: number }> = {
65
- nodes: uniqueNodes.map(nodeId => ({ id: nodeId })),
66
- links: data?.links?.map(link => ({
67
- source: uniqueNodes.findIndex(node => node === link.source),
68
- target: uniqueNodes.findIndex(node => node === link.target),
69
- value: link.value
70
- }))
71
- }
72
-
73
- let textPositionHorizontal = 5
74
- const BUFFER = 50
75
-
76
- // Set the sankey diagram properties console.log('largestGroupWidth', largestGroupWidth)
77
-
78
- const sankeyGenerator = sankey<SankeyNode, { source: number; target: number }>()
79
- .nodeWidth(sankeyConfig.nodeSize.nodeWidth)
80
- .nodePadding(sankeyConfig.nodePadding)
81
- .iterations(sankeyConfig.iterations)
82
- .nodeAlign(sankeyLeft)
83
- .extent([
84
- [sankeyConfig.margin.margin_x, Number(sankeyConfig.margin.margin_y)],
85
- [width - textPositionHorizontal - largestGroupWidth, config.heights.vertical - BUFFER]
86
- ])
87
-
88
- const { links } = sankeyGenerator(sankeyData)
89
-
90
- const nodeStyle = (id: string) => {
91
- let textPositionHorizontal = 30
92
- let textPositionVertical = 0
93
- let classStyle = 'node-value--storynode'
94
- let storyNodes = true
95
-
96
- if (data?.storyNodeText?.every(node => node.StoryNode !== id)) {
97
- storyNodes = false
98
- textPositionVertical = 10
99
- textPositionHorizontal = 8
100
- classStyle = 'node-value'
88
+ const sankeyGenerator = sankey<SankeyLayoutNode, any>()
89
+ .nodeId(node => node.id)
90
+ .nodeWidth(nodeWidth)
91
+ .nodePadding(nodePadding)
92
+ .iterations(iterations)
93
+ .nodeAlign(sankeyJustify)
94
+ .extent([
95
+ [horizontalMargin, topMargin],
96
+ [layoutWidth - horizontalMargin, layoutHeight - bottomMargin]
97
+ ])
98
+
99
+ const sankeyGraph = sankeyGenerator(graph as any)
100
+
101
+ return {
102
+ error: '',
103
+ isCompact,
104
+ isScrollable,
105
+ labelWidth,
106
+ links: sankeyGraph.links as SankeyLayoutLink[],
107
+ nodes: sankeyGraph.nodes as SankeyLayoutNode[],
108
+ width: layoutWidth,
109
+ height: layoutHeight
110
+ }
111
+ } catch (error) {
112
+ return {
113
+ error: error instanceof Error ? error.message : 'Unable to render Sankey data.',
114
+ isCompact,
115
+ isScrollable,
116
+ labelWidth,
117
+ links: [] as SankeyLayoutLink[],
118
+ nodes: [] as SankeyLayoutNode[],
119
+ width: layoutWidth,
120
+ height: layoutHeight
121
+ }
101
122
  }
123
+ }, [height, preparedData, sankeyConfig, width, config?.heights?.vertical])
102
124
 
103
- return { textPositionHorizontal, textPositionVertical, classStyle, storyNodes }
104
- }
125
+ const activeFlow = useMemo(() => {
126
+ if (!activeNode) return null
105
127
 
106
- const activeConnection = (id: String) => {
107
- if (!sankeyData?.nodes) return { sourceNodes: [], activeLinks: [] }
128
+ const nodeIds = new Set<string>([activeNode])
129
+ const linkIds = new Set<string>()
130
+ const nodesToVisit = [activeNode]
108
131
 
109
- const currentNode = sankeyData.nodes.find(node => node.id === id)
132
+ while (nodesToVisit.length) {
133
+ const currentNodeId = nodesToVisit.shift()
134
+ const downstreamLinks = layout.links.filter(link => link.source.id === currentNodeId)
110
135
 
111
- const sourceNodes = []
112
- const activeLinks = []
136
+ downstreamLinks.forEach(link => {
137
+ linkIds.add(link.id)
113
138
 
114
- if (currentNode) {
115
- links.forEach(link => {
116
- const targetObj: any = link.target
117
- const sourceObj: any = link.source
118
- if (targetObj.id === id) {
119
- sourceNodes.push(sourceObj.id)
139
+ if (!nodeIds.has(link.target.id)) {
140
+ nodeIds.add(link.target.id)
141
+ nodesToVisit.push(link.target.id)
120
142
  }
121
143
  })
122
-
123
- sourceNodes.forEach(id => {
124
- links.forEach(link => {
125
- const targetObj: any = link.target
126
- const sourceObj: any = link.source
127
- if (targetObj.id === tooltipID && sourceObj.id === id) {
128
- activeLinks.push(link)
129
- }
130
- })
131
- })
132
144
  }
133
145
 
134
- return { sourceNodes, activeLinks }
135
- }
136
-
137
- const sankeyToolTip = getSankeyTooltip(data, tooltipID)
138
-
139
- // Draw the nodes
140
- const allNodes = sankeyData.nodes.map((node, i) => {
141
- let { textPositionHorizontal, textPositionVertical, classStyle, storyNodes } = nodeStyle(node.id)
142
- let { sourceNodes } = activeConnection(tooltipID)
143
-
144
- let opacityValue = sankeyConfig.opacity.nodeOpacityDefault
145
- let nodeColor = sankeyConfig.nodeColor.default
146
-
147
- if (tooltipID !== node.id && tooltipID !== '' && !sourceNodes.includes(node.id)) {
148
- nodeColor = sankeyConfig.nodeColor.inactive
149
- opacityValue = sankeyConfig.opacity.nodeOpacityInactive
150
- }
146
+ return { nodeIds, linkIds }
147
+ }, [activeNode, layout.links])
151
148
 
152
- const maxNodeWidth = sankeyGenerator.nodeWidth()
149
+ if (config.visualizationType !== 'Sankey') return null
153
150
 
154
- // get the link length
155
- /**
156
- * Calculates the length of the link between the source and target nodes
157
- * using the Euclidean distance formula.
158
- *
159
- * @returns {number} The length of the link.
160
- */
161
- const linkLength = () =>
162
- Math.sqrt(
163
- Math.pow(links[0].target.x0! - links[0].source.x1!, 2) + Math.pow(links[0].target.y0! - links[0].source.y1!, 2)
164
- ) - largestGroupWidth
165
-
166
- return (
167
- <Group className='' key={i}>
168
- <rect
169
- height={node.y1! - node.y0! + 2} // increasing node size to account for smaller nodes
170
- width={maxNodeWidth}
171
- x={node.x0}
172
- y={node.y0! - 1} //adjusting here the node starts so it looks more center with the link
173
- fill={nodeColor}
174
- fillOpacity={opacityValue}
175
- rx={sankeyConfig.rxValue}
176
- onMouseEnter={() => {}}
177
- // todo: move enable tooltips to sankey
178
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
179
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
180
- onClick={() => handleNodeClick(node.id)}
181
- style={{ pointerEvents: 'visible', cursor: 'pointer' }}
182
- />
183
- {storyNodes ? (
184
- <>
185
- <Text
186
- width={linkLength()}
187
- /* Text Position Horizontal
188
- x0 is the left edge of the node
189
- # - positions text # units to the right of the left edge of the node */
190
- x={node.x0! + textPositionHorizontal}
191
- textAnchor={sankeyData.nodes.length - 1 === i ? 'end' : 'start'}
192
- verticalAnchor='end'
193
- /*Text Position Vertical
194
- y1 and y0 are the top and bottom edges of the node
195
- y1+y0 = total height
196
- dividing by 2 gives you the midpoint of the node
197
- minus 30 raises the vertical position to be higher
198
- */
199
- y={(node.y1! + node.y0!) / 2 - 30}
200
- /* Using x and y in combination with dominant baseline allows for a more
201
- precise positioning of the text within the svg
202
- dominant baseline allows for different vertical alignments
203
- text-before-edge aligns the text's bottom edge with the bottom edge of the container
204
- */
205
- fill={sankeyConfig.nodeFontColor}
206
- fontWeight='bold' // font weight
207
- className='node-text'
208
- style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
209
- onClick={() => handleNodeClick(node.id)}
210
- onMouseEnter={() => {}}
211
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
212
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
213
- >
214
- {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
215
- </Text>
216
- <Text
217
- width={linkLength()}
218
- verticalAnchor='middle'
219
- className={classStyle}
220
- x={node.x0! + textPositionHorizontal}
221
- y={(node.y1! + node.y0! + 25) / 2}
222
- fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor}
223
- fontWeight='bold'
224
- textAnchor='start'
225
- style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
226
- onClick={() => handleNodeClick(node.id)}
227
- onMouseEnter={() => {}}
228
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
229
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
230
- >
231
- {typeof node.value === 'number' ? node.value.toLocaleString(config.locale) : node.value}
232
- </Text>
233
- <Text
234
- width={linkLength()}
235
- x={node.x0! + textPositionHorizontal}
236
- // plus 50 will move the vertical position down
237
- y={(node.y1! + node.y0!) / 2 + 50}
238
- fill={sankeyConfig.nodeFontColor}
239
- fontWeight='bold'
240
- textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
241
- className='node-text'
242
- verticalAnchor='start'
243
- style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
244
- onClick={() => handleNodeClick(node.id)}
245
- onMouseEnter={() => {}}
246
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
247
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
248
- >
249
- {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
250
- </Text>
251
- </>
252
- ) : (
253
- <>
254
- <Text
255
- style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
256
- onClick={() => handleNodeClick(node.id)}
257
- onMouseEnter={() => {}}
258
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
259
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
260
- x={node.x0! + textPositionHorizontal}
261
- y={(node.y1! + node.y0!) / 2 + textPositionVertical}
262
- dominantBaseline='text-before-edge'
263
- fill={sankeyConfig.nodeFontColor}
264
- fontWeight='bold'
265
- textAnchor='start'
266
- >
267
- {node.id}
268
- </Text>
269
- <text
270
- x={node.x0! + textPositionHorizontal}
271
- /* adding 30 allows the node value to be on the next line underneath the node id */
272
- y={(node.y1! + node.y0!) / 2 + 30}
273
- dominantBaseline='text-before-edge'
274
- fill={sankeyConfig.nodeFontColor}
275
- //fontSize={16}
276
- fontWeight='bold'
277
- textAnchor='start'
278
- style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
279
- onClick={() => handleNodeClick(node.id)}
280
- onMouseEnter={() => {}}
281
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
282
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
283
- >
284
- <tspan className={classStyle}>
285
- {sankeyConfig.nodeValueStyle.textBefore +
286
- (typeof node.value === 'number' ? node.value.toLocaleString(config.locale) : node.value) +
287
- sankeyConfig.nodeValueStyle.textAfter}
288
- </tspan>
289
- </text>
290
- </>
291
- )}
292
- </Group>
293
- )
294
- })
295
-
296
- // Draw the links
297
- const allLinks = links.map((link, i) => {
298
- const linkGenerator = sankeyLinkHorizontal()
299
- const path = linkGenerator(link)
300
- let opacityValue = sankeyConfig.opacity.LinkOpacityDefault
301
- let strokeColor = sankeyConfig.linkColor.default
302
-
303
- let { activeLinks } = activeConnection(tooltipID)
304
-
305
- if (!activeLinks.includes(link) && tooltipID !== '') {
306
- strokeColor = sankeyConfig.linkColor.inactive
307
- opacityValue = sankeyConfig.opacity.LinkOpacityInactive
308
- }
151
+ const tooltipId = `cdc-open-viz-tooltip-${runtime?.uniqueId || 'chart'}-sankey`
152
+ const hasRenderableData = layout.links.length > 0 && layout.nodes.length > 0
153
+ const valuePrefix = sankeyConfig?.nodeValueStyle?.textBefore ?? '('
154
+ const valueSuffix = sankeyConfig?.nodeValueStyle?.textAfter ?? ')'
155
+ const a11y = handleChartAriaLabels(config)
309
156
 
310
- return (
311
- <path
312
- key={i}
313
- d={path!}
314
- stroke={strokeColor}
315
- fill='none'
316
- strokeOpacity={opacityValue}
317
- strokeWidth={link.width! + 2}
318
- style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
319
- onClick={() => handleNodeClick(link.target.id || null)}
320
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
321
- data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
322
- />
323
- )
324
- })
157
+ const toggleActiveNode = (nodeId: string) => {
158
+ setActiveNode(currentActiveNode => (currentActiveNode === nodeId ? '' : nodeId))
159
+ }
325
160
 
326
- // max depth - calculates how many nodes deep the chart goes.
327
- const maxDepth: number = sankeyData.nodes.reduce((maxDepth, node) => {
328
- return Math.max(maxDepth, node.depth)
329
- }, -1)
161
+ const handleKeyboardToggle = (event: React.KeyboardEvent<SVGElement>, nodeId: string) => {
162
+ if (event.key !== 'Enter' && event.key !== ' ') return
163
+ event.preventDefault()
164
+ toggleActiveNode(nodeId)
165
+ }
330
166
 
331
- // finalNodesAtMaxDepth - get only the right most nodes on the chart.
332
- const finalNodesAtMaxDepth = sankeyData.nodes.filter(node => node.depth === maxDepth)
167
+ const getNodeOpacity = (nodeId: string) => {
168
+ if (!activeFlow) return Number(sankeyConfig?.opacity?.nodeOpacityDefault) || 1
169
+ return activeFlow.nodeIds.has(nodeId)
170
+ ? Number(sankeyConfig?.opacity?.nodeOpacityDefault) || 1
171
+ : Number(sankeyConfig?.opacity?.nodeOpacityInactive) || 0.1
172
+ }
333
173
 
334
- const finalNodes = finalNodesAtMaxDepth.map((node, i) => {
335
- let { textPositionHorizontal, textPositionVertical, classStyle, storyNodes } = nodeStyle(node.id)
336
- let { sourceNodes } = activeConnection(tooltipID)
174
+ const getLinkOpacity = (link: SankeyLayoutLink) => {
175
+ if (!activeFlow) return Number(sankeyConfig?.opacity?.LinkOpacityDefault) || 0.75
176
+ return activeFlow.linkIds.has(link.id)
177
+ ? Number(sankeyConfig?.opacity?.LinkOpacityDefault) || 0.75
178
+ : Number(sankeyConfig?.opacity?.LinkOpacityInactive) || 0.1
179
+ }
337
180
 
338
- let opacityValue = sankeyConfig.opacity.nodeOpacityDefault
339
- let nodeColor = sankeyConfig.nodeColor.default
181
+ const getNodeColor = (nodeId: string) =>
182
+ !activeFlow || activeFlow.nodeIds.has(nodeId)
183
+ ? paletteColorPair.node
184
+ : sankeyConfig?.nodeColor?.inactive || '#808080'
340
185
 
341
- if (tooltipID !== node.id && tooltipID !== '' && !sourceNodes.includes(node.id)) {
342
- nodeColor = sankeyConfig.nodeColor.inactive
343
- opacityValue = sankeyConfig.opacity.nodeOpacityInactive
344
- }
186
+ const getLinkColor = (link: SankeyLayoutLink) =>
187
+ !activeFlow || activeFlow.linkIds.has(link.id)
188
+ ? paletteColorPair.link
189
+ : sankeyConfig?.linkColor?.inactive || '#D3D3D3'
345
190
 
191
+ if (!hasRenderableData) {
346
192
  return (
347
- <Group className='' key={i} innerRef={el => (groupRefs.current[i] = el)}>
348
- <rect
349
- height={node.y1! - node.y0! + 2} // increasing node size to account for smaller nodes
350
- width={sankeyGenerator.nodeWidth()}
351
- x={node.x0}
352
- y={node.y0! - 1} //adjusting here the node starts so it looks more center with the link
353
- fill={nodeColor}
354
- fillOpacity={opacityValue}
355
- rx={sankeyConfig.rxValue}
356
- data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
357
- data-tooltip-id={`tooltip`}
358
- onClick={() => handleNodeClick(node.id)}
359
- style={{ pointerEvents: 'visible', cursor: 'pointer' }}
360
- />
361
- {storyNodes ? (
362
- <>
363
- <Text
364
- /* Text Position Horizontal
365
- x0 is the left edge of the node
366
- # - positions text # units to the right of the left edge of the node */
367
- x={node.x0! + textPositionHorizontal}
368
- textAnchor={sankeyData.nodes.length - 1 === i ? 'end' : 'start'}
369
- verticalAnchor='end'
370
- /*Text Position Vertical
371
- y1 and y0 are the top and bottom edges of the node
372
- y1+y0 = total height
373
- dividing by 2 gives you the midpoint of the node
374
- minus 30 raises the vertical position to be higher
375
- */
376
- y={(node.y1! + node.y0!) / 2 - 30}
377
- /* Using x and y in combination with dominant baseline allows for a more
378
- precise positioning of the text within the svg
379
- dominant baseline allows for different vertical alignments
380
- text-before-edge aligns the text's bottom edge with the bottom edge of the container
381
- */
382
- fill={sankeyConfig.nodeFontColor}
383
- fontWeight='bold' // font weight
384
- style={{ pointerEvents: 'none' }}
385
- className='node-text'
386
- >
387
- {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
388
- </Text>
389
- <Text
390
- verticalAnchor='end'
391
- className={classStyle}
392
- x={node.x0! + textPositionHorizontal}
393
- y={(node.y1! + node.y0! + 25) / 2}
394
- fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor}
395
- fontWeight='bold'
396
- textAnchor='start'
397
- style={{ pointerEvents: 'none' }}
398
- >
399
- {typeof node.value === 'number' ? node.value.toLocaleString(config.locale) : node.value}
400
- </Text>
401
- <Text
402
- x={node.x0! + textPositionHorizontal}
403
- // plus 50 will move the vertical position down
404
- y={(node.y1! + node.y0!) / 2 + 50}
405
- fill={sankeyConfig.nodeFontColor}
406
- fontWeight='bold'
407
- textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
408
- style={{ pointerEvents: 'none' }}
409
- className='node-text'
410
- verticalAnchor='end'
411
- >
412
- {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
413
- </Text>
414
- </>
415
- ) : (
416
- <>
417
- <text
418
- x={node.x0! + textPositionHorizontal}
419
- y={(node.y1! + node.y0!) / 2 + textPositionVertical}
420
- dominantBaseline='text-before-edge'
421
- fill={sankeyConfig.nodeFontColor}
422
- fontWeight='bold'
423
- textAnchor='start'
424
- style={{ pointerEvents: 'none' }}
425
- >
426
- <tspan id={node.id} className='node-id'>
427
- {node.id}
428
- </tspan>
429
- </text>
430
- <text
431
- x={node.x0! + textPositionHorizontal}
432
- /* adding 30 allows the node value to be on the next line underneath the node id */
433
- y={(node.y1! + node.y0!) / 2 + 30}
434
- dominantBaseline='text-before-edge'
435
- fill={sankeyConfig.nodeFontColor}
436
- fontWeight='bold'
437
- textAnchor='start'
438
- style={{ pointerEvents: 'none' }}
439
- >
440
- <tspan onClick={() => handleNodeClick(node.id)} className={classStyle}>
441
- {sankeyConfig.nodeValueStyle.textBefore +
442
- (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) +
443
- sankeyConfig.nodeValueStyle.textAfter}
444
- </tspan>
445
- </text>
446
- </>
447
- )}
448
- </Group>
193
+ <div className='sankey-chart sankey-chart--empty' role='status'>
194
+ {layout.error || config?.chartMessage?.noData || 'No Data Available'}
195
+ </div>
449
196
  )
450
- })
197
+ }
451
198
 
452
- return !showAlert ? (
453
- <>
454
- <div className='sankey-chart'>
199
+ return (
200
+ <div
201
+ className={`sankey-chart${layout.isCompact ? ' sankey-chart--compact' : ''}${
202
+ layout.isScrollable ? ' sankey-chart--scrollable' : ''
203
+ }`}
204
+ >
205
+ <div
206
+ className='sankey-chart__scroll-area'
207
+ role={layout.isScrollable ? 'region' : undefined}
208
+ tabIndex={layout.isScrollable ? 0 : undefined}
209
+ aria-label={layout.isScrollable ? 'Scrollable Sankey diagram' : undefined}
210
+ >
455
211
  <svg
456
212
  className='sankey-chart__diagram'
457
- width={width}
458
- height={Number(config.heights.vertical)}
459
- style={{ overflow: 'visible' }}
213
+ height={layout.height}
460
214
  role='img'
215
+ viewBox={`0 0 ${layout.width} ${layout.height}`}
216
+ width={layout.isScrollable ? layout.width : '100%'}
461
217
  aria-label={a11y}
218
+ data-rejected-row-count={preparedData.rejectedRowCount}
462
219
  >
463
- <Group className='links'>{allLinks}</Group>
464
- <Group className='nodes'>{allNodes}</Group>
465
- <Group className='finalNodes' style={{ visibility: 'hidden', pointerEvents: 'none' }} aria-hidden='true'>
466
- {finalNodes}
220
+ <Group className='sankey-chart__links'>
221
+ {layout.links.map(link => {
222
+ const path = sankeyLinkHorizontal()(link as any)
223
+ const tooltipLabel = `${link.source.id} → ${link.target.id}`
224
+ const tooltipValue = formatNodeValue(link.value, config.locale)
225
+ const linkOpacity = getLinkOpacity(link)
226
+ const linkWidth = Math.max(1, link.width || 1)
227
+
228
+ return (
229
+ <path
230
+ key={link.id}
231
+ className='sankey-chart__link'
232
+ d={path || ''}
233
+ fill='none'
234
+ stroke={getLinkColor(link)}
235
+ strokeOpacity={linkOpacity}
236
+ strokeWidth={linkWidth}
237
+ onClick={() => toggleActiveNode(link.source.id)}
238
+ data-tooltip-html={config.enableTooltips ? getSankeyTooltip(tooltipLabel, tooltipValue) : undefined}
239
+ data-tooltip-id={tooltipId}
240
+ data-link-source-id={link.source.id}
241
+ data-link-target-id={link.target.id}
242
+ />
243
+ )
244
+ })}
467
245
  </Group>
468
- </svg>
469
246
 
470
- {/* ReactTooltip needs to remain even if tooltips are disabled -- it handles when a user clicks off of the node and resets
471
- the sankey diagram. When tooltips are disabled this will nothing */}
472
- <ReactTooltip
473
- id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
474
- afterHide={() => setTooltipID('')}
475
- events={['click']}
476
- place={'bottom'}
477
- style={{
478
- backgroundColor: `rgba(238, 238, 238, 1)`,
479
- color: 'black',
480
- boxShadow: `0 3px 10px rgb(0 0 0 / 0.2)`
481
- }}
482
- />
247
+ <Group className='sankey-chart__nodes'>
248
+ {layout.nodes.map(node => {
249
+ const nodeHeight = Math.max(1, (node.y1 || 0) - (node.y0 || 0))
250
+ const nodeWidth = Math.max(1, (node.x1 || 0) - (node.x0 || 0))
251
+ const labelOnLeft = !node.targetLinks?.length
252
+ const labelX = labelOnLeft ? (node.x0 || 0) - 8 : (node.x1 || 0) + 8
253
+ const labelY = ((node.y0 || 0) + (node.y1 || 0)) / 2
254
+ const value = formatNodeValue(node.value, config.locale)
255
+ const label = `${node.id} ${valuePrefix}${value}${valueSuffix}`
256
+ const tooltipValue = `${node.id}: ${value}`
257
+
258
+ return (
259
+ <Group key={node.id} className='sankey-chart__node-group'>
260
+ <rect
261
+ className='sankey-chart__node'
262
+ height={nodeHeight}
263
+ width={nodeWidth}
264
+ x={node.x0}
265
+ y={node.y0}
266
+ fill={getNodeColor(node.id)}
267
+ fillOpacity={getNodeOpacity(node.id)}
268
+ rx={Number(sankeyConfig?.rxValue) || 0}
269
+ role='button'
270
+ tabIndex={0}
271
+ aria-label={`${node.id}, ${value}`}
272
+ onClick={() => toggleActiveNode(node.id)}
273
+ onKeyDown={event => handleKeyboardToggle(event, node.id)}
274
+ data-tooltip-html={config.enableTooltips ? getSankeyTooltip(node.id, tooltipValue) : undefined}
275
+ data-tooltip-id={tooltipId}
276
+ data-node-id={node.id}
277
+ />
278
+ <Text
279
+ className='sankey-chart__label'
280
+ data-label-side={labelOnLeft ? 'left' : 'right'}
281
+ data-node-id={node.id}
282
+ dominantBaseline='middle'
283
+ fill={sankeyConfig?.nodeFontColor}
284
+ fontWeight={600}
285
+ pointerEvents='none'
286
+ textAnchor={labelOnLeft ? 'end' : 'start'}
287
+ verticalAnchor='middle'
288
+ width={layout.labelWidth}
289
+ x={labelX}
290
+ y={labelY}
291
+ >
292
+ {label}
293
+ </Text>
294
+ </Group>
295
+ )
296
+ })}
297
+ </Group>
298
+ </svg>
483
299
  </div>
484
- </>
485
- ) : (
486
- alert
300
+
301
+ <ReactTooltip
302
+ id={tooltipId}
303
+ afterHide={() => setActiveNode('')}
304
+ events={['click']}
305
+ place='bottom'
306
+ style={{
307
+ backgroundColor: 'rgba(238, 238, 238, 1)',
308
+ color: 'black',
309
+ boxShadow: '0 3px 10px rgb(0 0 0 / 0.2)'
310
+ }}
311
+ />
312
+ </div>
487
313
  )
488
314
  }
315
+
489
316
  export default Sankey