@cdc/chart 4.24.1 → 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 (82) hide show
  1. package/dist/cdcchart.js +48948 -37923
  2. package/examples/{private/combo.json → chart-regression-1.json} +40 -31
  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-preliminary.json +84 -37
  6. package/examples/feature/line/line-chart.json +2 -1
  7. package/examples/feature/regions/index.json +55 -5
  8. package/examples/feature/sankey/sankey-example-data.json +1364 -0
  9. package/examples/feature/sankey/sankey_chart_data.csv +20 -0
  10. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +306 -19
  11. package/examples/sparkline.json +868 -0
  12. package/index.html +128 -121
  13. package/package.json +4 -2
  14. package/src/CdcChart.tsx +73 -38
  15. package/src/_stories/ChartEditor.stories.tsx +15 -4
  16. package/src/_stories/_mock/pie_config.json +4 -3
  17. package/src/_stories/_mock/url_filter.json +1076 -0
  18. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +2 -1
  19. package/src/components/AreaChart/components/AreaChart.jsx +2 -25
  20. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -49
  21. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +36 -56
  22. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +36 -41
  23. package/src/components/BarChart/components/BarChart.Vertical.tsx +48 -64
  24. package/src/components/BoxPlot/BoxPlot.jsx +11 -9
  25. package/src/components/DeviationBar.jsx +3 -3
  26. package/src/components/EditorPanel/EditorPanel.tsx +1717 -1961
  27. package/src/components/EditorPanel/EditorPanelContext.ts +40 -0
  28. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +148 -0
  29. package/src/components/EditorPanel/components/{Panel.ForestPlotSettings.tsx → Panels/Panel.ForestPlotSettings.tsx} +16 -7
  30. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +160 -0
  31. package/src/components/EditorPanel/components/{Panel.Regions.tsx → Panels/Panel.Regions.tsx} +6 -6
  32. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +108 -0
  33. package/src/components/EditorPanel/components/{Panel.Series.tsx → Panels/Panel.Series.tsx} +50 -6
  34. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +338 -0
  35. package/src/components/EditorPanel/components/Panels/index.tsx +19 -0
  36. package/src/components/EditorPanel/components/panels.scss +11 -0
  37. package/src/components/EditorPanel/editor-panel.scss +1 -13
  38. package/src/components/EditorPanel/useEditorPermissions.js +44 -13
  39. package/src/components/Legend/Legend.Component.tsx +207 -0
  40. package/src/components/Legend/Legend.tsx +8 -327
  41. package/src/components/Legend/helpers/createFormatLabels.tsx +140 -0
  42. package/src/components/LineChart/LineChartProps.ts +2 -1
  43. package/src/components/LineChart/components/LineChart.Circle.tsx +85 -52
  44. package/src/components/LineChart/helpers.ts +3 -3
  45. package/src/components/LineChart/index.tsx +99 -23
  46. package/src/components/LinearChart.jsx +12 -33
  47. package/src/components/PairedBarChart.jsx +10 -12
  48. package/src/components/PieChart/PieChart.tsx +80 -27
  49. package/src/components/Regions/components/Regions.tsx +120 -69
  50. package/src/components/Sankey/index.tsx +434 -0
  51. package/src/components/Sankey/sankey.scss +153 -0
  52. package/src/components/Sankey/types/index.ts +16 -0
  53. package/src/components/ScatterPlot/ScatterPlot.jsx +1 -0
  54. package/src/components/Sparkline/{SparkLine.jsx → components/SparkLine.tsx} +14 -30
  55. package/src/components/Sparkline/index.scss +3 -0
  56. package/src/components/Sparkline/index.tsx +1 -1
  57. package/src/components/ZoomBrush.tsx +2 -1
  58. package/src/data/initial-state.js +51 -4
  59. package/src/helpers/computeMarginBottom.ts +4 -3
  60. package/src/helpers/tests/computeMarginBottom.test.ts +2 -1
  61. package/src/hooks/useBarChart.js +5 -2
  62. package/src/hooks/useHighlightedBars.js +1 -1
  63. package/src/hooks/useMinMax.ts +3 -3
  64. package/src/hooks/useScales.ts +28 -18
  65. package/src/hooks/useTooltip.tsx +19 -14
  66. package/src/scss/main.scss +8 -96
  67. package/src/types/ChartConfig.ts +47 -20
  68. package/src/types/ChartContext.ts +17 -4
  69. package/src/types/Label.ts +7 -0
  70. package/examples/private/chart-t.json +0 -3740
  71. package/examples/private/epi-data.csv +0 -13
  72. package/examples/private/epi-data.json +0 -62
  73. package/examples/private/epi.json +0 -403
  74. package/examples/private/occupancy.json +0 -109283
  75. package/examples/private/prod-line-config.json +0 -401
  76. package/examples/private/region-data.json +0 -822
  77. package/examples/private/region-testing.json +0 -312
  78. package/examples/private/scaling.json +0 -45325
  79. package/examples/private/testing-data.json +0 -1739
  80. package/examples/private/testing.json +0 -816
  81. package/src/components/EditorPanel/components/Panel.DateHighlighting.tsx +0 -109
  82. package/src/components/EditorPanel/components/Panels.tsx +0 -13
@@ -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
@@ -0,0 +1,153 @@
1
+ /* KPI */
2
+ .kpis-container {
3
+ display: flex;
4
+ flex-direction: row;
5
+ column-gap: 30px;
6
+ }
7
+
8
+ .cdc-open-viz-module .sankey-chart {
9
+ --font-size-small: 12px;
10
+ --font-size-medium: 14px;
11
+ --font-size-large: 18px;
12
+ --font-size-xl: 24px;
13
+
14
+ --storynode-font-size--small: 24px;
15
+ --storynode-font-size--medium: 28px;
16
+ --storynode-font-size--large: 32px;
17
+
18
+ --font-weight-normal: 400;
19
+ --font-weight-bold: 700;
20
+
21
+ overflow: visible;
22
+
23
+ .divider {
24
+ border-top: 1px solid #000;
25
+ margin: 10px 0;
26
+ }
27
+
28
+ svg.sankey-chart__diagram {
29
+ position:relative;
30
+ font-family: 'Roboto', sans-serif;
31
+ height: auto;
32
+ width: 100%;
33
+ }
34
+
35
+ .node-id {
36
+ font-weight: var(--font-weight-bold);
37
+ }
38
+
39
+ .node-value {
40
+ font-weight: var(--font-weight-normal);
41
+ }
42
+
43
+ .node-text {
44
+ font-weight: var(--font-weight-normal);
45
+ }
46
+
47
+ .node-value--storynode {
48
+ font-weight: var(--font-weight-bold);
49
+ }
50
+
51
+ /* Hover card */
52
+ .sankey-chart__tooltip {
53
+ color: black;
54
+ display: flex;
55
+ flex-direction: column;
56
+ margin: 10px;
57
+ &--tooltip-header {
58
+ font-weight: var(--font-weight-bold);
59
+ }
60
+ &--info-section {
61
+ column-gap: 10px;
62
+ display: flex;
63
+ flex-direction: row;
64
+ justify-content: space-between;
65
+ }
66
+ }
67
+
68
+ span {
69
+ max-width: 500px;
70
+ word-wrap: break-word;
71
+ }
72
+
73
+ /* Autoscaling */
74
+ //large - default
75
+ @media only screen and (min-width: 1200px) {
76
+ min-width: none;
77
+ .node-text {
78
+ font-size: var(--font-size-xl);
79
+ }
80
+ .node-value--storynode {
81
+ font-size: var(--storynode-font-size--large);
82
+ }
83
+ .node-id {
84
+ font-size: var(--font-size-large);
85
+ }
86
+ .node-value {
87
+ font-size: var(--font-size-large);
88
+ }
89
+ }
90
+ //medium
91
+ @media only screen and (max-width: 1199px) {
92
+ .node-text {
93
+ font-size: var(--font-size-medium);
94
+ }
95
+ .node-value--storynode {
96
+ font-size: var(--storynode-font-size--medium);
97
+ }
98
+ .node-id {
99
+ font-size: var(--font-size-medium);
100
+ }
101
+ .node-value {
102
+ font-size: var(--font-size-medium);
103
+ }
104
+ }
105
+
106
+ //small
107
+ @media only screen and (max-width: 799px) {
108
+ .node-text {
109
+ font-size: var(--font-size-small);
110
+ }
111
+ .node-value--storynode {
112
+ font-size: var(--storynode-font-size--small);
113
+ }
114
+ .node-id {
115
+ font-size: var(--font-size-small);
116
+ }
117
+ .node-value {
118
+ font-size: var(--font-size-small);
119
+ }
120
+ }
121
+
122
+ //x-small
123
+ @media only screen and (max-width: 600px) {
124
+ .popup {
125
+ display: block; /* Show the popup on smaller screens */
126
+ }
127
+ .sankey-chart__diagram {
128
+ opacity: .1;
129
+ }
130
+ }
131
+ }
132
+
133
+ /* Pop up */
134
+
135
+ .popup {
136
+ position: absolute;
137
+ top: 50%;
138
+ left: 50%;
139
+ transform: translate(-50%, -50%);
140
+ background-color: beige;
141
+ border: 2px solid gray !important;
142
+ border-radius: 8px;
143
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
144
+ width: 80%;
145
+ z-index: 999;
146
+ display: none;
147
+ }
148
+
149
+ .popup-content {
150
+ font-size: 30px;
151
+ padding: 10px;
152
+ text-align: center;
153
+ }
@@ -0,0 +1,16 @@
1
+ export type Link = { source: string; target: string; value: number }
2
+
3
+ export type Data = {
4
+ links: Link[]
5
+ }
6
+
7
+ export type SankeyNode = {
8
+ id: string
9
+ }
10
+
11
+ export type SankeyProps = {
12
+ width: number
13
+ height: number
14
+ data: Data
15
+ runtime: any
16
+ }
@@ -41,6 +41,7 @@ const ScatterPlot = ({ xScale, yScale, getXAxisData, getYAxisData }) => {
41
41
  style={pointStyles}
42
42
  data-tooltip-html={handleTooltip(item, s)}
43
43
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
44
+ tabIndex={-1}
44
45
  />
45
46
  )
46
47
  })