@cdc/chart 4.24.2 → 4.24.4
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.
- package/dist/cdcchart.js +47933 -36918
- package/examples/chart-regression-1.json +378 -0
- package/examples/chart-regression-2.json +2360 -0
- package/examples/feature/filters/url-filter.json +1076 -0
- package/examples/feature/line/line-chart.json +362 -37
- package/examples/feature/regions/index.json +50 -4
- package/examples/feature/sankey/sankey-example-data.json +1364 -0
- package/examples/feature/sankey/sankey_chart_data.csv +20 -0
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +306 -19
- package/examples/region-issue.json +2065 -0
- package/examples/sparkline.json +868 -0
- package/examples/test.json +5409 -0
- package/index.html +130 -123
- package/package.json +4 -2
- package/src/CdcChart.tsx +178 -94
- package/src/_stories/ChartEditor.stories.tsx +14 -3
- package/src/_stories/_mock/url_filter.json +1076 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +2 -1
- package/src/components/AreaChart/components/AreaChart.jsx +2 -1
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -63
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +36 -56
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +32 -39
- package/src/components/BarChart/components/BarChart.Vertical.tsx +44 -59
- package/src/components/BoxPlot/BoxPlot.jsx +2 -1
- package/src/components/DeviationBar.jsx +3 -3
- package/src/components/EditorPanel/EditorPanel.tsx +1684 -1564
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +107 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +48 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +41 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +9 -7
- package/src/components/EditorPanel/components/panels.scss +11 -0
- package/src/components/EditorPanel/editor-panel.scss +0 -724
- package/src/components/EditorPanel/useEditorPermissions.js +40 -14
- package/src/components/Legend/Legend.Component.tsx +43 -63
- package/src/components/Legend/Legend.tsx +8 -4
- package/src/components/LineChart/LineChartProps.ts +1 -0
- package/src/components/LineChart/helpers.ts +2 -2
- package/src/components/LineChart/index.tsx +7 -7
- package/src/components/LinearChart.jsx +11 -31
- package/src/components/PairedBarChart.jsx +6 -10
- package/src/components/PieChart/PieChart.tsx +3 -3
- package/src/components/Regions/components/Regions.tsx +120 -78
- package/src/components/Sankey/index.tsx +434 -0
- package/src/components/Sankey/sankey.scss +153 -0
- package/src/components/Sankey/types/index.ts +16 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +1 -0
- package/src/components/Sparkline/{SparkLine.jsx → components/SparkLine.tsx} +14 -30
- package/src/components/Sparkline/index.scss +3 -0
- package/src/components/Sparkline/index.tsx +1 -1
- package/src/components/ZoomBrush.tsx +2 -1
- package/src/data/initial-state.js +46 -2
- package/src/helpers/computeMarginBottom.ts +2 -1
- package/src/helpers/tests/computeMarginBottom.test.ts +2 -1
- package/src/hooks/useBarChart.js +5 -2
- package/src/hooks/useScales.ts +47 -18
- package/src/hooks/useTooltip.tsx +9 -8
- package/src/scss/main.scss +33 -29
- package/src/types/ChartConfig.ts +32 -14
- package/src/types/ChartContext.ts +7 -0
|
@@ -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
|
+
}
|