@cdc/chart 4.24.2 → 4.24.3

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 (57) hide show
  1. package/dist/cdcchart.js +47386 -36618
  2. package/examples/chart-regression-1.json +378 -0
  3. package/examples/chart-regression-2.json +2360 -0
  4. package/examples/feature/filters/url-filter.json +1076 -0
  5. package/examples/feature/line/line-chart.json +2 -1
  6. package/examples/feature/regions/index.json +50 -4
  7. package/examples/feature/sankey/sankey-example-data.json +1364 -0
  8. package/examples/feature/sankey/sankey_chart_data.csv +20 -0
  9. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +306 -19
  10. package/examples/sparkline.json +868 -0
  11. package/index.html +128 -123
  12. package/package.json +4 -2
  13. package/src/CdcChart.tsx +40 -22
  14. package/src/_stories/ChartEditor.stories.tsx +14 -3
  15. package/src/_stories/_mock/url_filter.json +1076 -0
  16. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +2 -1
  17. package/src/components/AreaChart/components/AreaChart.jsx +2 -1
  18. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -49
  19. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +36 -56
  20. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +32 -39
  21. package/src/components/BarChart/components/BarChart.Vertical.tsx +40 -55
  22. package/src/components/BoxPlot/BoxPlot.jsx +2 -1
  23. package/src/components/DeviationBar.jsx +3 -3
  24. package/src/components/EditorPanel/EditorPanel.tsx +167 -15
  25. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +1 -1
  26. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +108 -0
  27. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +48 -4
  28. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +41 -0
  29. package/src/components/EditorPanel/components/Panels/index.tsx +9 -7
  30. package/src/components/EditorPanel/components/panels.scss +11 -0
  31. package/src/components/EditorPanel/useEditorPermissions.js +40 -14
  32. package/src/components/Legend/Legend.Component.tsx +23 -15
  33. package/src/components/Legend/Legend.tsx +4 -4
  34. package/src/components/LineChart/LineChartProps.ts +1 -0
  35. package/src/components/LineChart/helpers.ts +2 -2
  36. package/src/components/LineChart/index.tsx +7 -7
  37. package/src/components/LinearChart.jsx +9 -30
  38. package/src/components/PairedBarChart.jsx +6 -10
  39. package/src/components/PieChart/PieChart.tsx +3 -3
  40. package/src/components/Regions/components/Regions.tsx +120 -78
  41. package/src/components/Sankey/index.tsx +434 -0
  42. package/src/components/Sankey/sankey.scss +153 -0
  43. package/src/components/Sankey/types/index.ts +16 -0
  44. package/src/components/ScatterPlot/ScatterPlot.jsx +1 -0
  45. package/src/components/Sparkline/{SparkLine.jsx → components/SparkLine.tsx} +14 -30
  46. package/src/components/Sparkline/index.scss +3 -0
  47. package/src/components/Sparkline/index.tsx +1 -1
  48. package/src/components/ZoomBrush.tsx +2 -1
  49. package/src/data/initial-state.js +46 -2
  50. package/src/helpers/computeMarginBottom.ts +2 -1
  51. package/src/helpers/tests/computeMarginBottom.test.ts +2 -1
  52. package/src/hooks/useBarChart.js +5 -2
  53. package/src/hooks/useScales.ts +15 -18
  54. package/src/hooks/useTooltip.tsx +9 -8
  55. package/src/scss/main.scss +8 -29
  56. package/src/types/ChartConfig.ts +32 -14
  57. package/src/types/ChartContext.ts +7 -0
@@ -1,102 +1,153 @@
1
- import React, { useContext } from 'react'
2
- import { ChartConfig } from '../../../types/ChartConfig'
1
+ import React, { MouseEventHandler, useContext } from 'react'
3
2
  import ConfigContext from '../../../ConfigContext'
4
3
  import { ChartContext } from '../../../types/ChartContext'
5
4
  import { Text } from '@visx/text'
6
5
  import { Group } from '@visx/group'
7
- import * as d3 from 'd3'
8
- import { formatDate } from '@cdc/core/helpers/cove/date.js'
6
+ import { formatDate, isDateScale } from '@cdc/core/helpers/cove/date.js'
9
7
 
10
8
  type RegionsProps = {
11
9
  xScale: Function
12
10
  yMax: number
13
11
  barWidth?: number
14
12
  totalBarsInGroup?: number
13
+ handleTooltipMouseOff: MouseEventHandler<SVGElement>
14
+ handleTooltipMouseOver: MouseEventHandler<SVGElement>
15
+ handleTooltipClick: MouseEventHandler<SVGElement>
16
+ tooltipData: unknown
17
+ showTooltip: Function
18
+ hideTooltip: Function
15
19
  }
16
20
 
17
- const Regions = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, handleTooltipMouseOff, handleTooltipMouseOver, handleTooltipClick, tooltipData, showTooltip, hideTooltip }: RegionsProps) => {
21
+ // TODO: should regions be removed on categorical axis?
22
+ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, handleTooltipMouseOff, handleTooltipMouseOver, handleTooltipClick, tooltipData, showTooltip, hideTooltip }) => {
18
23
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
19
24
 
20
25
  const { runtime, regions, visualizationType, orientation, xAxis } = config
26
+ const domain = xScale.domain()
21
27
 
22
- let from
23
- let to
24
- let width
28
+ const getFromValue = region => {
29
+ let from
25
30
 
26
- if (regions && orientation === 'vertical') {
27
- return regions.map(region => {
28
- if (xAxis.type === 'date' && region.fromType !== 'Previous Days') {
29
- from = xScale(parseDate(region.from).getTime())
30
- to = xScale(parseDate(region.to).getTime())
31
- width = to - from
31
+ // Fixed Date
32
+ if (!region?.fromType || region.fromType === 'Fixed') {
33
+ const date = new Date(region.from)
34
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
35
+ from = xScale(parsedDate)
36
+
37
+ if (visualizationType === 'Bar' && xAxis.type === 'date-time') {
38
+ from = from - (barWidth * totalBarsInGroup) / 2
39
+ }
40
+ }
41
+
42
+ // Previous Date
43
+ if (region.fromType === 'Previous Days') {
44
+ const previousDays = Number(region.from) || 0
45
+ const categoricalDomain = domain.map(d => formatDate(config.xAxis.dateParseFormat, new Date(d)))
46
+ const d = region.toType === 'Last Date' ? new Date(domain[domain.length - 1]).getTime() : new Date(region.to) // on categorical charts force leading zero 03/15/2016 vs 3/15/2016 for valid date format
47
+ const to = config.xAxis.type === 'categorical' ? formatDate(config.xAxis.dateParseFormat, d) : formatDate(config.xAxis.dateParseFormat, d)
48
+ const toDate = new Date(to)
49
+ from = new Date(toDate.setDate(toDate.getDate() - Number(previousDays)))
50
+
51
+ if (xAxis.type === 'date') {
52
+ from = new Date(formatDate(xAxis.dateParseFormat, from)).getTime()
53
+
54
+ let closestDate = domain[0]
55
+ let minDiff = Math.abs(from - closestDate)
56
+
57
+ for (let i = 1; i < domain.length; i++) {
58
+ const diff = Math.abs(from - domain[i])
59
+ if (diff < minDiff) {
60
+ minDiff = diff
61
+ closestDate = domain[i]
62
+ }
63
+ }
64
+ from = closestDate
32
65
  }
33
66
 
67
+ // Here the domain is in the xScale.dateParseFormat
34
68
  if (xAxis.type === 'categorical') {
35
- from = xScale(region.from)
36
- to = xScale(region.to)
37
- width = to - from
69
+ let closestDate = domain[0]
70
+ let minDiff = Math.abs(new Date(from).getTime() - new Date(closestDate).getTime())
71
+
72
+ for (let i = 1; i < domain.length; i++) {
73
+ const diff = Math.abs(new Date(from).getTime() - new Date(domain[i]).getTime())
74
+ if (diff < minDiff) {
75
+ minDiff = diff
76
+ closestDate = domain[i]
77
+ }
78
+ }
79
+ from = closestDate
38
80
  }
39
81
 
40
- if ((visualizationType === 'Bar' || config.visualizationType === 'Combo') && xAxis.type === 'date') {
41
- from = region.fromType !== 'Previous Days' ? xScale(parseDate(region.from).getTime()) - (barWidth * totalBarsInGroup) / 2 : null
42
- to = region.toType !== 'Last Date' ? xScale(parseDate(region.to).getTime()) + (barWidth * totalBarsInGroup) / 2 : null
82
+ from = xScale(from)
83
+ }
43
84
 
44
- width = to - from
45
- }
85
+ if (xAxis.type === 'categorical' && region.fromType !== 'Previous Days') {
86
+ from = xScale(region.from)
87
+ }
46
88
 
47
- if ((visualizationType === 'Bar' || config.visualizationType === 'Combo') && config.xAxis.type === 'categorical') {
48
- from = xScale(region.from)
49
- to = xScale(region.to)
50
- width = to - from
89
+ if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
90
+ let scalePadding = Number(config.yAxis.size)
91
+ if (xScale.bandwidth) {
92
+ scalePadding += xScale.bandwidth() / 2
51
93
  }
94
+ from = from + scalePadding
95
+ }
52
96
 
53
- if (region.fromType === 'Previous Days') {
54
- to = region.toType !== 'Last Date' ? xScale(parseDate(region.to).getTime()) + (barWidth * totalBarsInGroup) / 2 : null
55
-
56
- let domain = xScale.domain()
57
- let bisectDate = d3.bisector(d => d).left
58
- let closestValue
59
-
60
- let previousDays = Number(region.from)
61
- let lastDate = region.toType === 'Last Date' ? domain[domain.length - 1] : region.to
62
- let toDate = new Date(lastDate)
63
-
64
- from = new Date(toDate.setDate(toDate.getDate() - previousDays)).getTime()
65
- let targetValue = from
66
-
67
- let index = bisectDate(domain, targetValue)
68
- if (index === 0) {
69
- closestValue = domain[0]
70
- } else if (index === domain.length) {
71
- closestValue = domain[domain.length - 1]
72
- } else {
73
- let d0 = domain[index - 1]
74
- let d1 = domain[index]
75
- closestValue = targetValue - d0 > d1 - targetValue ? d1 : d0
76
- }
77
- from = Number(xScale(closestValue) - (visualizationType === 'Bar' || visualizationType === 'Combo' ? (barWidth * totalBarsInGroup) / 2 : 0))
97
+ if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.fromType === 'Previous Days') {
98
+ from = from - (barWidth * totalBarsInGroup) / 2
99
+ }
78
100
 
79
- width = to - from
80
- }
101
+ return from
102
+ }
81
103
 
82
- // set the region max to the charts max range.
83
- if (region.toType === 'Last Date') {
84
- let domainValues = xScale.domain()
85
- let lastDate = domainValues[domainValues.length - 1]
86
- to = Number(xScale(lastDate) + (visualizationType === 'Bar' || visualizationType === 'Combo' ? (barWidth * totalBarsInGroup) / 2 : 0))
87
- width = to - from
104
+ const getToValue = region => {
105
+ let to
106
+
107
+ // when xScale is categorical leading zeros are removed, ie. 03/15/2016 is 3/15/2016
108
+ if (xAxis.type === 'categorical') {
109
+ to = xScale(region.to)
110
+ }
111
+
112
+ if (isDateScale(xAxis)) {
113
+ if (!region?.toType || region.toType === 'Fixed') {
114
+ to = xScale(parseDate(region.to).getTime())
88
115
  }
89
116
 
90
- if (region.fromType === 'Previous Days' && xAxis.type === 'date' && xAxis.sortDates && config.visualizationType === 'Line') {
91
- let domain = xScale.domain()
92
- let previousDays = Number(region.from)
93
- let to = region.toType === 'Last Date' ? formatDate(config.xAxis.dateParseFormat, domain[domain.length - 1]) : region.to
94
- let toDate = new Date(to)
95
- from = new Date(toDate.setDate(toDate.getDate() - previousDays)).getTime()
96
- from = xScale(from)
97
- to = xScale(parseDate(to))
98
- width = to - from
117
+ if (visualizationType === 'Bar' || config.visualizationType === 'Combo') {
118
+ to = region.toType !== 'Last Date' ? xScale(parseDate(region.to).getTime()) + barWidth * totalBarsInGroup : to
99
119
  }
120
+ }
121
+ if (region.toType === 'Last Date') {
122
+ const lastDate = domain[domain.length - 1]
123
+ to = Number(xScale(lastDate) + ((visualizationType === 'Bar' || visualizationType === 'Combo') && config.xAxis.type === 'date' ? barWidth * totalBarsInGroup : 0))
124
+ }
125
+
126
+ if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
127
+ let scalePadding = Number(config.yAxis.size)
128
+ if (xScale.bandwidth) {
129
+ scalePadding += xScale.bandwidth() / 2
130
+ }
131
+ to = to + scalePadding
132
+ }
133
+
134
+ if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.toType !== 'Last Date') {
135
+ to = to - (barWidth * totalBarsInGroup) / 2
136
+ }
137
+
138
+ if ((visualizationType === 'Bar' || visualizationType === 'Combo') && xAxis.type === 'categorical') {
139
+ to = to + (visualizationType === 'Bar' || visualizationType === 'Combo' ? barWidth * totalBarsInGroup : 0)
140
+ }
141
+ return to
142
+ }
143
+
144
+ const getWidth = (to, from) => to - from
145
+
146
+ if (regions && orientation === 'vertical') {
147
+ return regions.map(region => {
148
+ const from = getFromValue(region)
149
+ const to = getToValue(region)
150
+ const width = getWidth(to, from)
100
151
 
101
152
  if (!from) return null
102
153
  if (!to) return null
@@ -120,16 +171,7 @@ const Regions = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, handleToolt
120
171
  }
121
172
 
122
173
  return (
123
- <Group
124
- className='regions regions-group--line'
125
- left={config.visualizationType === 'Bar' || config.visualizationType === 'Combo' ? 0 : config?.visualizationType === 'Line' ? Number(runtime.yAxis.size) : 0}
126
- key={region.label}
127
- onMouseMove={handleTooltipMouseOver}
128
- onMouseLeave={handleTooltipMouseOff}
129
- handleTooltipClick={handleTooltipClick}
130
- tooltipData={JSON.stringify(tooltipData)}
131
- showTooltip={showTooltip}
132
- >
174
+ <Group height={100} fill='red' className='regions regions-group--line zzz' key={region.label} onMouseMove={handleTooltipMouseOver} onMouseLeave={handleTooltipMouseOff} handleTooltipClick={handleTooltipClick} tooltipData={JSON.stringify(tooltipData)} showTooltip={showTooltip}>
133
175
  <TopRegionBorderShape />
134
176
  <HighlightedArea />
135
177
  <Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
@@ -0,0 +1,434 @@
1
+ import { useContext, useState, useRef, useEffect } from 'react'
2
+
3
+ // External Libraries
4
+ import { PlacesType, Tooltip as ReactTooltip } from 'react-tooltip'
5
+ import { SankeyGraph, sankey, sankeyLinkHorizontal, sankeyLeft } from 'd3-sankey'
6
+ import { Group } from '@visx/group'
7
+ import { Text } from '@visx/text'
8
+ import ReactDOMServer from 'react-dom/server'
9
+
10
+ // Cdc
11
+ import './sankey.scss'
12
+ import 'react-tooltip/dist/react-tooltip.css'
13
+ import ConfigContext from '@cdc/chart/src/ConfigContext'
14
+ import { ChartContext } from '../../types/ChartContext'
15
+ import type { SankeyNode, SankeyProps } from './types'
16
+
17
+ const Sankey = ({ width, height, runtime }: SankeyProps) => {
18
+ const DEBUG = true
19
+ const { config } = useContext<ChartContext>(ConfigContext)
20
+ const { sankey: sankeyConfig } = config
21
+ // !info - changed config.sankey.data here to work with our current upload pattern saved on config.data
22
+ const data = config?.data[0]
23
+ const [largestGroupWidth, setLargestGroupWidth] = useState(0)
24
+ const groupRefs = useRef([])
25
+
26
+ //Tooltip
27
+ const [tooltipID, setTooltipID] = useState<string>('')
28
+
29
+ const handleNodeClick = (nodeId: string) => {
30
+ setTooltipID(nodeId)
31
+ }
32
+
33
+ const clearNodeClick = () => {
34
+ setTooltipID('')
35
+ }
36
+
37
+ //Mobile Pop Up
38
+ const [showPopup, setShowPopup] = useState(false)
39
+
40
+ useEffect(() => {
41
+ if (window.innerWidth < 768 && window.innerHeight > window.innerWidth) {
42
+ setShowPopup(true)
43
+ }
44
+ }, [window.innerWidth])
45
+
46
+ const closePopUp = () => {
47
+ setShowPopup(false)
48
+ }
49
+
50
+ // Uses Visx Groups innerRef to get all Group elements that are mapped.
51
+ // Sets the largest group width in state and subtracts that group the svg width to calculate overall width.
52
+ useEffect(() => {
53
+ let largest = 0
54
+ groupRefs?.current?.map(g => {
55
+ const groupWidth = g?.getBoundingClientRect().width
56
+ if (groupWidth > largest) {
57
+ largest = groupWidth
58
+ }
59
+ })
60
+ setLargestGroupWidth(largest)
61
+ }, [groupRefs, sankeyConfig, window.innerWidth])
62
+
63
+ //Retrieve all the unique values for the Nodes
64
+ const uniqueNodes = Array.from(new Set(data?.links?.flatMap(link => [link.source, link.target])))
65
+
66
+ // Convert JSON data to the format required
67
+ const sankeyData: SankeyGraph<SankeyNode, { source: number; target: number }> = {
68
+ nodes: uniqueNodes.map(nodeId => ({ id: nodeId })),
69
+ links: data?.links?.map(link => ({
70
+ source: uniqueNodes.findIndex(node => node === link.source),
71
+ target: uniqueNodes.findIndex(node => node === link.target),
72
+ value: link.value
73
+ }))
74
+ }
75
+
76
+ let textPositionHorizontal = 5
77
+ const BUFFER = 50
78
+
79
+ // Set the sankey diagram properties console.log('largestGroupWidth', largestGroupWidth)
80
+
81
+ const sankeyGenerator = sankey<SankeyNode, { source: number; target: number }>()
82
+ .nodeWidth(sankeyConfig.nodeSize.nodeWidth)
83
+ .nodePadding(sankeyConfig.nodePadding)
84
+ .iterations(sankeyConfig.iterations)
85
+ .nodeAlign(sankeyLeft)
86
+ .extent([
87
+ [sankeyConfig.margin.margin_x, Number(sankeyConfig.margin.margin_y)],
88
+ [width - textPositionHorizontal - largestGroupWidth, config.heights.vertical - BUFFER]
89
+ ])
90
+
91
+ const { nodes, links } = sankeyGenerator(sankeyData)
92
+
93
+ const nodeStyle = (id: string) => {
94
+ let textPositionHorizontal = 30
95
+ let textPositionVertical = 0
96
+ let classStyle = 'node-value--storynode'
97
+ let storyNodes = true
98
+
99
+ // TODO: need a dynamic way to apply classes here instead of checking static values.
100
+
101
+ if (data?.storyNodeText?.every(node => node.StoryNode !== id)) {
102
+ storyNodes = false
103
+ textPositionVertical = 10
104
+ textPositionHorizontal = 8
105
+ classStyle = 'node-value'
106
+ }
107
+
108
+ return { textPositionHorizontal, textPositionVertical, classStyle, storyNodes }
109
+ }
110
+
111
+ const activeConnection = (id: String) => {
112
+ const currentNode = sankeyData.nodes.find(node => node.id === id)
113
+
114
+ const sourceNodes = []
115
+ const activeLinks = []
116
+
117
+ if (currentNode) {
118
+ links.forEach(link => {
119
+ const targetObj: any = link.target
120
+ const sourceObj: any = link.source
121
+ if (targetObj.id === id) {
122
+ sourceNodes.push(sourceObj.id)
123
+ }
124
+ })
125
+
126
+ sourceNodes.forEach(id => {
127
+ links.forEach(link => {
128
+ const targetObj: any = link.target
129
+ const sourceObj: any = link.source
130
+ if (targetObj.id === tooltipID && sourceObj.id === id) {
131
+ activeLinks.push(link)
132
+ }
133
+ })
134
+ })
135
+ }
136
+
137
+ return { sourceNodes, activeLinks }
138
+ }
139
+
140
+ const tooltipVal = `${(data?.tooltips.find(item => item.node === tooltipID) || {}).value}`
141
+ const tooltipSummary = `${(data?.tooltips.find(item => item.node === tooltipID) || {}).summary}`
142
+ const tooltipColumn1Label = (data?.tooltips.find(item => item.node === tooltipID) || {}).column1Label
143
+ const tooltipColumn2Label = (data?.tooltips.find(item => item.node === tooltipID) || {}).column2Label
144
+ const tooltipColumn1 = (data?.tooltips.find(item => item.node === tooltipID) || {}).column1
145
+ const tooltipColumn2 = (data?.tooltips.find(item => item.node === tooltipID) || {}).column2
146
+
147
+ const ColumnList = ({ columnData }) => {
148
+ return (
149
+ <ul>
150
+ {columnData?.map((entry, index) => (
151
+ <li key={index}>
152
+ {entry.label}: {entry.value} ({entry.additional_info}%)
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ )
157
+ }
158
+
159
+ const tooltipColumn1Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn1} />)
160
+ const tooltipColumn2Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn2} />)
161
+
162
+ const sankeyToolTip = `<div class="sankey-chart__tooltip">
163
+ <span class="sankey-chart__tooltip--tooltip-header">${tooltipID}</span>
164
+ <span class="sankey-chart__tooltip--tooltip-header">${tooltipVal}</span>
165
+ <div class="divider"></div>
166
+ <span><strong>Summary: </strong>${tooltipSummary}</span>
167
+ <div class="divider"></div>
168
+ <div class="sankey-chart__tooltip--info-section">
169
+ <div>
170
+ <span><strong>${tooltipColumn1Label}<strong></span>
171
+ ${tooltipColumn1Data}
172
+ </div>
173
+ <div>
174
+ <span><strong>${tooltipColumn2Label}<strong></span>
175
+ ${tooltipColumn2Data}
176
+ </div>
177
+ </div>
178
+ </div>`
179
+
180
+ // Draw the nodes
181
+ const allNodes = sankeyData.nodes.map((node, i) => {
182
+ let { textPositionHorizontal, textPositionVertical, classStyle, storyNodes } = nodeStyle(node.id)
183
+ let { sourceNodes } = activeConnection(tooltipID)
184
+
185
+ let opacityValue = sankeyConfig.opacity.nodeOpacityDefault
186
+ let nodeColor = sankeyConfig.nodeColor.default
187
+
188
+ if (tooltipID !== node.id && tooltipID !== '' && !sourceNodes.includes(node.id)) {
189
+ nodeColor = sankeyConfig.nodeColor.inactive
190
+ opacityValue = sankeyConfig.opacity.nodeOpacityInactive
191
+ }
192
+
193
+ return (
194
+ <Group className='' key={i}>
195
+ <rect
196
+ height={node.y1! - node.y0! + 2} // increasing node size to account for smaller nodes
197
+ width={sankeyGenerator.nodeWidth()}
198
+ x={node.x0}
199
+ y={node.y0! - 1} //adjusting here the node starts so it looks more center with the link
200
+ fill={nodeColor}
201
+ fillOpacity={opacityValue}
202
+ rx={sankeyConfig.rxValue}
203
+ // todo: move enable tooltips to sankey
204
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
205
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
206
+ onClick={() => handleNodeClick(node.id)}
207
+ style={{ pointerEvents: 'visible', cursor: 'pointer' }}
208
+ />
209
+ {storyNodes ? (
210
+ <>
211
+ <Text
212
+ /* Text Position Horizontal
213
+ x0 is the left edge of the node
214
+ # - positions text # units to the right of the left edge of the node */
215
+ x={node.x0! + textPositionHorizontal}
216
+ textAnchor={sankeyData.nodes.length - 1 === i ? 'end' : 'start'}
217
+ verticalAnchor='end'
218
+ /*Text Position Vertical
219
+ y1 and y0 are the top and bottom edges of the node
220
+ y1+y0 = total height
221
+ dividing by 2 gives you the midpoint of the node
222
+ minus 30 raises the vertical position to be higher
223
+ */
224
+ y={(node.y1! + node.y0!) / 2 - 30}
225
+ /* Using x and y in combination with dominant baseline allows for a more
226
+ precise positioning of the text within the svg
227
+ dominant baseline allows for different vertical alignments
228
+ text-before-edge aligns the text's bottom edge with the bottom edge of the container
229
+ */
230
+ fill={sankeyConfig.nodeFontColor}
231
+ fontWeight='bold' // font weight
232
+ style={{ pointerEvents: 'none' }}
233
+ className='node-text'
234
+ >
235
+ {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
236
+ </Text>
237
+ <Text verticalAnchor='end' className={classStyle} x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0! + 25) / 2} fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
238
+ {typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
239
+ </Text>
240
+ <Text
241
+ x={node.x0! + textPositionHorizontal}
242
+ // plus 50 will move the vertical position down
243
+ y={(node.y1! + node.y0!) / 2 + 50}
244
+ fill={sankeyConfig.nodeFontColor}
245
+ fontWeight='bold'
246
+ textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
247
+ style={{ pointerEvents: 'none' }}
248
+ className='node-text'
249
+ verticalAnchor='end'
250
+ >
251
+ {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
252
+ </Text>
253
+ </>
254
+ ) : (
255
+ <>
256
+ <text x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0!) / 2 + textPositionVertical} dominantBaseline='text-before-edge' fill={sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
257
+ <tspan id={node.id} className='node-id'>
258
+ {node.id}
259
+ </tspan>
260
+ </text>
261
+ <text
262
+ x={node.x0! + textPositionHorizontal}
263
+ /* adding 30 allows the node value to be on the next line underneath the node id */
264
+ y={(node.y1! + node.y0!) / 2 + 30}
265
+ dominantBaseline='text-before-edge'
266
+ fill={sankeyConfig.nodeFontColor}
267
+ //fontSize={16}
268
+ fontWeight='bold'
269
+ textAnchor='start'
270
+ style={{ pointerEvents: 'none' }}
271
+ >
272
+ <tspan className={classStyle}>{sankeyConfig.nodeValueStyle.textBefore + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}</tspan>
273
+ </text>
274
+ </>
275
+ )}
276
+ </Group>
277
+ )
278
+ })
279
+
280
+ // Draw the links
281
+ const allLinks = links.map((link, i) => {
282
+ const linkGenerator = sankeyLinkHorizontal()
283
+ const path = linkGenerator(link)
284
+ let opacityValue = sankeyConfig.opacity.LinkOpacityDefault
285
+ let strokeColor = sankeyConfig.linkColor.default
286
+
287
+ let { activeLinks } = activeConnection(tooltipID)
288
+
289
+ if (!activeLinks.includes(link) && tooltipID !== '') {
290
+ strokeColor = sankeyConfig.linkColor.inactive
291
+ opacityValue = sankeyConfig.opacity.LinkOpacityInactive
292
+ }
293
+
294
+ return <path key={i} d={path!} stroke={strokeColor} fill='none' strokeOpacity={opacityValue} strokeWidth={link.width! + 2} />
295
+ })
296
+
297
+ // max depth - calculates how many nodes deep the chart goes.
298
+ const maxDepth: number = sankeyData.nodes.reduce((maxDepth, node) => {
299
+ return Math.max(maxDepth, node.depth)
300
+ }, -1)
301
+
302
+ // finalNodesAtMaxDepth - get only the right most nodes on the chart.
303
+ const finalNodesAtMaxDepth = sankeyData.nodes.filter(node => node.depth === maxDepth)
304
+
305
+ const finalNodes = finalNodesAtMaxDepth.map((node, i) => {
306
+ let { textPositionHorizontal, textPositionVertical, classStyle, storyNodes } = nodeStyle(node.id)
307
+ let { sourceNodes } = activeConnection(tooltipID)
308
+
309
+ let opacityValue = sankeyConfig.opacity.nodeOpacityDefault
310
+ let nodeColor = sankeyConfig.nodeColor.default
311
+
312
+ if (tooltipID !== node.id && tooltipID !== '' && !sourceNodes.includes(node.id)) {
313
+ nodeColor = sankeyConfig.nodeColor.inactive
314
+ opacityValue = sankeyConfig.opacity.nodeOpacityInactive
315
+ }
316
+
317
+ return (
318
+ <Group className='' key={i} innerRef={el => (groupRefs.current[i] = el)}>
319
+ <rect
320
+ height={node.y1! - node.y0! + 2} // increasing node size to account for smaller nodes
321
+ width={sankeyGenerator.nodeWidth()}
322
+ x={node.x0}
323
+ y={node.y0! - 1} //adjusting here the node starts so it looks more center with the link
324
+ fill={nodeColor}
325
+ fillOpacity={opacityValue}
326
+ rx={sankeyConfig.rxValue}
327
+ // todo: move enable tooltips to sankey
328
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
329
+ data-tooltip-id={`tooltip`}
330
+ onClick={() => handleNodeClick(node.id)}
331
+ style={{ pointerEvents: 'visible', cursor: 'pointer' }}
332
+ />
333
+ {storyNodes ? (
334
+ <>
335
+ <Text
336
+ /* Text Position Horizontal
337
+ x0 is the left edge of the node
338
+ # - positions text # units to the right of the left edge of the node */
339
+ x={node.x0! + textPositionHorizontal}
340
+ textAnchor={sankeyData.nodes.length - 1 === i ? 'end' : 'start'}
341
+ verticalAnchor='end'
342
+ /*Text Position Vertical
343
+ y1 and y0 are the top and bottom edges of the node
344
+ y1+y0 = total height
345
+ dividing by 2 gives you the midpoint of the node
346
+ minus 30 raises the vertical position to be higher
347
+ */
348
+ y={(node.y1! + node.y0!) / 2 - 30}
349
+ /* Using x and y in combination with dominant baseline allows for a more
350
+ precise positioning of the text within the svg
351
+ dominant baseline allows for different vertical alignments
352
+ text-before-edge aligns the text's bottom edge with the bottom edge of the container
353
+ */
354
+ fill={sankeyConfig.nodeFontColor}
355
+ fontWeight='bold' // font weight
356
+ style={{ pointerEvents: 'none' }}
357
+ className='node-text'
358
+ >
359
+ {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
360
+ </Text>
361
+ <Text verticalAnchor='end' className={classStyle} x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0! + 25) / 2} fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
362
+ {typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
363
+ </Text>
364
+ <Text
365
+ x={node.x0! + textPositionHorizontal}
366
+ // plus 50 will move the vertical position down
367
+ y={(node.y1! + node.y0!) / 2 + 50}
368
+ fill={sankeyConfig.nodeFontColor}
369
+ fontWeight='bold'
370
+ textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
371
+ style={{ pointerEvents: 'none' }}
372
+ className='node-text'
373
+ verticalAnchor='end'
374
+ >
375
+ {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
376
+ </Text>
377
+ </>
378
+ ) : (
379
+ <>
380
+ <text x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0!) / 2 + textPositionVertical} dominantBaseline='text-before-edge' fill={sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
381
+ <tspan id={node.id} className='node-id'>
382
+ {node.id}
383
+ </tspan>
384
+ </text>
385
+ <text
386
+ x={node.x0! + textPositionHorizontal}
387
+ /* adding 30 allows the node value to be on the next line underneath the node id */
388
+ y={(node.y1! + node.y0!) / 2 + 30}
389
+ dominantBaseline='text-before-edge'
390
+ fill={sankeyConfig.nodeFontColor}
391
+ //fontSize={16}
392
+ fontWeight='bold'
393
+ textAnchor='start'
394
+ style={{ pointerEvents: 'none' }}
395
+ >
396
+ <tspan className={classStyle}>{sankeyConfig.nodeValueStyle.textBefore + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}</tspan>
397
+ </text>
398
+ </>
399
+ )}
400
+ </Group>
401
+ )
402
+ })
403
+
404
+ return (
405
+ <>
406
+ <div className='sankey-chart'>
407
+ <svg className='sankey-chart__diagram' width={width} height={Number(config.heights.vertical)} style={{ overflow: 'visible' }}>
408
+ <Group className='links'>{allLinks}</Group>
409
+ <Group className='nodes'>{allNodes}</Group>
410
+ <Group className='finalNodes' style={{ display: 'none' }}>
411
+ {finalNodes}
412
+ </Group>
413
+ </svg>
414
+
415
+ {/* ReactTooltip needs to remain even if tooltips are disabled -- it handles when a user clicks off of the node and resets
416
+ the sankey diagram. When tooltips are disabled this will nothing */}
417
+ <ReactTooltip id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`} afterHide={() => clearNodeClick()} events={['click']} place={'bottom'} style={{ backgroundColor: `rgba(238, 238, 238, 1)`, color: 'black', boxShadow: `0 3px 10px rgb(0 0 0 / 0.2)` }} />
418
+ {showPopup && (
419
+ <div className='popup'>
420
+ <div className='popup-content'>
421
+ <button className='visually-hidden' onClick={closePopUp}>
422
+ Select for accessible version.
423
+ </button>
424
+ <p>
425
+ <strong>Please change the orientation of your screen or increase the size of your browser to view the diagram better.</strong>
426
+ </p>
427
+ </div>
428
+ </div>
429
+ )}
430
+ </div>
431
+ </>
432
+ )
433
+ }
434
+ export default Sankey