@cdc/chart 4.24.10 → 4.24.12
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 +34651 -33978
- package/examples/feature/boxplot/boxplot-data.json +88 -22
- package/examples/feature/boxplot/boxplot.json +540 -16
- package/examples/feature/boxplot/testing.csv +7 -7
- package/examples/feature/sankey/sankey-example-data.json +126 -14
- package/examples/feature/tests-date-exclusions/date-exclusions-config.json +372 -12
- package/examples/private/DEV-8850-2.json +493 -0
- package/examples/private/DEV-9822.json +574 -0
- package/examples/private/DEV-9840.json +553 -0
- package/examples/private/DEV-9850-3.json +461 -0
- package/examples/private/chart.json +1084 -0
- package/examples/private/ci_formatted.json +202 -0
- package/examples/private/ci_issue.json +3016 -0
- package/examples/private/completed.json +634 -0
- package/examples/private/dem-data-long.csv +20 -0
- package/examples/private/dem-data-long.json +36 -0
- package/examples/private/demographic_data.csv +157 -0
- package/examples/private/demographic_data.json +2654 -0
- package/examples/private/demographic_dynamic.json +443 -0
- package/examples/private/demographic_standard.json +560 -0
- package/examples/private/test.json +493 -0
- package/index.html +10 -7
- package/package.json +2 -2
- package/src/CdcChart.tsx +132 -152
- package/src/_stories/Chart.Anchors.stories.tsx +31 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
- package/src/_stories/Chart.stories.tsx +37 -6
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
- package/src/_stories/ChartEditor.stories.tsx +27 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
- package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
- package/src/_stories/_mock/boxplot_multiseries.json +647 -0
- package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
- package/src/_stories/_mock/dynamic_series_config.json +979 -0
- package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
- package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
- package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
- package/src/_stories/_mock/short_dates.json +288 -0
- package/src/_stories/_mock/suppression_mock.json +1549 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
- package/src/components/Axis/Categorical.Axis.tsx +2 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
- package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
- package/src/components/BarChart/helpers/getBarData.ts +28 -0
- package/src/components/BarChart/helpers/index.ts +1 -2
- package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
- package/src/components/BoxPlot/BoxPlot.tsx +131 -0
- package/src/components/BoxPlot/helpers/index.ts +54 -0
- package/src/components/BrushChart.tsx +23 -26
- package/src/components/EditorPanel/EditorPanel.tsx +117 -139
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
- package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
- package/src/components/Legend/Legend.Component.tsx +11 -12
- package/src/components/Legend/Legend.tsx +16 -16
- package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
- package/src/components/Legend/helpers/index.ts +2 -1
- package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
- package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
- package/src/components/LineChart/helpers.ts +49 -43
- package/src/components/LineChart/index.tsx +135 -83
- package/src/components/LinearChart.tsx +187 -181
- package/src/components/PieChart/PieChart.tsx +7 -1
- package/src/components/Sankey/components/ColumnList.tsx +19 -0
- package/src/components/Sankey/components/Sankey.tsx +479 -0
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
- package/src/components/Sankey/index.tsx +1 -492
- package/src/components/Sankey/sankey.scss +22 -21
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/Sankey/useSankeyAlert.tsx +60 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
- package/src/data/initial-state.js +7 -12
- package/src/helpers/countNumOfTicks.ts +57 -0
- package/src/helpers/getQuartiles.ts +15 -18
- package/src/hooks/useMinMax.ts +44 -16
- package/src/hooks/useReduceData.ts +43 -10
- package/src/hooks/useScales.ts +90 -35
- package/src/hooks/useTooltip.tsx +59 -50
- package/src/scss/DataTable.scss +5 -0
- package/src/scss/main.scss +6 -20
- package/src/types/ChartConfig.ts +6 -19
- package/src/types/ChartContext.ts +4 -1
- package/src/types/ForestPlot.ts +8 -0
- package/src/components/BoxPlot/BoxPlot.jsx +0 -111
- package/src/hooks/useLegendClasses.ts +0 -72
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { useContext, useState, useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// External Libraries
|
|
4
|
+
import { 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
|
+
|
|
9
|
+
// Cdc
|
|
10
|
+
import './../sankey.scss'
|
|
11
|
+
import 'react-tooltip/dist/react-tooltip.css'
|
|
12
|
+
import ConfigContext from '@cdc/chart/src/ConfigContext'
|
|
13
|
+
import type { ChartContext } from '../../../types/ChartContext'
|
|
14
|
+
import type { SankeyNode, SankeyProps } from '../types'
|
|
15
|
+
import useSankeyAlert from '../useSankeyAlert'
|
|
16
|
+
import { getSankeyTooltip } from '../helpers/getSankeyTooltip'
|
|
17
|
+
|
|
18
|
+
const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
19
|
+
const { config } = useContext<ChartContext>(ConfigContext)
|
|
20
|
+
const { sankey: sankeyConfig } = config
|
|
21
|
+
const [largestGroupWidth, setLargestGroupWidth] = useState(0)
|
|
22
|
+
const [tooltipID, setTooltipID] = useState<string>('')
|
|
23
|
+
const { showAlert, alert } = useSankeyAlert()
|
|
24
|
+
const groupRefs = useRef([])
|
|
25
|
+
|
|
26
|
+
const handleNodeClick = (nodeId: string) => {
|
|
27
|
+
// Store the previous tooltipID
|
|
28
|
+
const previousTooltipID = tooltipID
|
|
29
|
+
|
|
30
|
+
// If the previous tooltipID exists, clear it
|
|
31
|
+
if (previousTooltipID) {
|
|
32
|
+
setTooltipID('')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Update the tooltipID with the new nodeId if it's different from the previous one
|
|
36
|
+
if (previousTooltipID !== nodeId) {
|
|
37
|
+
setTooltipID(nodeId)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Uses Visx Groups innerRef to get all Group elements that are mapped.
|
|
42
|
+
// Sets the largest group width in state and subtracts that group the svg width to calculate overall width.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let largest = 0
|
|
45
|
+
groupRefs?.current?.map(g => {
|
|
46
|
+
const groupWidth = g?.getBoundingClientRect().width
|
|
47
|
+
if (groupWidth > largest) {
|
|
48
|
+
largest = groupWidth
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
setLargestGroupWidth(largest)
|
|
52
|
+
}, [groupRefs, sankeyConfig, window.innerWidth])
|
|
53
|
+
|
|
54
|
+
if (config.visualizationType !== 'Sankey') return
|
|
55
|
+
|
|
56
|
+
const data = config?.data[0]
|
|
57
|
+
|
|
58
|
+
//Retrieve all the unique values for the Nodes
|
|
59
|
+
const uniqueNodes = Array.from(new Set(data?.links?.flatMap(link => [link.source, link.target])))
|
|
60
|
+
|
|
61
|
+
// Convert JSON data to the format required
|
|
62
|
+
const sankeyData: SankeyGraph<SankeyNode, { source: number; target: number }> = {
|
|
63
|
+
nodes: uniqueNodes.map(nodeId => ({ id: nodeId })),
|
|
64
|
+
links: data?.links?.map(link => ({
|
|
65
|
+
source: uniqueNodes.findIndex(node => node === link.source),
|
|
66
|
+
target: uniqueNodes.findIndex(node => node === link.target),
|
|
67
|
+
value: link.value
|
|
68
|
+
}))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let textPositionHorizontal = 5
|
|
72
|
+
const BUFFER = 50
|
|
73
|
+
|
|
74
|
+
// Set the sankey diagram properties console.log('largestGroupWidth', largestGroupWidth)
|
|
75
|
+
|
|
76
|
+
const sankeyGenerator = sankey<SankeyNode, { source: number; target: number }>()
|
|
77
|
+
.nodeWidth(sankeyConfig.nodeSize.nodeWidth)
|
|
78
|
+
.nodePadding(sankeyConfig.nodePadding)
|
|
79
|
+
.iterations(sankeyConfig.iterations)
|
|
80
|
+
.nodeAlign(sankeyLeft)
|
|
81
|
+
.extent([
|
|
82
|
+
[sankeyConfig.margin.margin_x, Number(sankeyConfig.margin.margin_y)],
|
|
83
|
+
[width - textPositionHorizontal - largestGroupWidth, config.heights.vertical - BUFFER]
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
const { links } = sankeyGenerator(sankeyData)
|
|
87
|
+
|
|
88
|
+
const nodeStyle = (id: string) => {
|
|
89
|
+
let textPositionHorizontal = 30
|
|
90
|
+
let textPositionVertical = 0
|
|
91
|
+
let classStyle = 'node-value--storynode'
|
|
92
|
+
let storyNodes = true
|
|
93
|
+
|
|
94
|
+
if (data?.storyNodeText?.every(node => node.StoryNode !== id)) {
|
|
95
|
+
storyNodes = false
|
|
96
|
+
textPositionVertical = 10
|
|
97
|
+
textPositionHorizontal = 8
|
|
98
|
+
classStyle = 'node-value'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { textPositionHorizontal, textPositionVertical, classStyle, storyNodes }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const activeConnection = (id: String) => {
|
|
105
|
+
if (!sankeyData?.nodes) return { sourceNodes: [], activeLinks: [] }
|
|
106
|
+
|
|
107
|
+
const currentNode = sankeyData.nodes.find(node => node.id === id)
|
|
108
|
+
|
|
109
|
+
const sourceNodes = []
|
|
110
|
+
const activeLinks = []
|
|
111
|
+
|
|
112
|
+
if (currentNode) {
|
|
113
|
+
links.forEach(link => {
|
|
114
|
+
const targetObj: any = link.target
|
|
115
|
+
const sourceObj: any = link.source
|
|
116
|
+
if (targetObj.id === id) {
|
|
117
|
+
sourceNodes.push(sourceObj.id)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
sourceNodes.forEach(id => {
|
|
122
|
+
links.forEach(link => {
|
|
123
|
+
const targetObj: any = link.target
|
|
124
|
+
const sourceObj: any = link.source
|
|
125
|
+
if (targetObj.id === tooltipID && sourceObj.id === id) {
|
|
126
|
+
activeLinks.push(link)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { sourceNodes, activeLinks }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sankeyToolTip = getSankeyTooltip(data, tooltipID)
|
|
136
|
+
|
|
137
|
+
// Draw the nodes
|
|
138
|
+
const allNodes = sankeyData.nodes.map((node, i) => {
|
|
139
|
+
let { textPositionHorizontal, textPositionVertical, classStyle, storyNodes } = nodeStyle(node.id)
|
|
140
|
+
let { sourceNodes } = activeConnection(tooltipID)
|
|
141
|
+
|
|
142
|
+
let opacityValue = sankeyConfig.opacity.nodeOpacityDefault
|
|
143
|
+
let nodeColor = sankeyConfig.nodeColor.default
|
|
144
|
+
|
|
145
|
+
if (tooltipID !== node.id && tooltipID !== '' && !sourceNodes.includes(node.id)) {
|
|
146
|
+
nodeColor = sankeyConfig.nodeColor.inactive
|
|
147
|
+
opacityValue = sankeyConfig.opacity.nodeOpacityInactive
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const maxNodeWidth = sankeyGenerator.nodeWidth()
|
|
151
|
+
|
|
152
|
+
// get the link length
|
|
153
|
+
/**
|
|
154
|
+
* Calculates the length of the link between the source and target nodes
|
|
155
|
+
* using the Euclidean distance formula.
|
|
156
|
+
*
|
|
157
|
+
* @returns {number} The length of the link.
|
|
158
|
+
*/
|
|
159
|
+
const linkLength = () =>
|
|
160
|
+
Math.sqrt(
|
|
161
|
+
Math.pow(links[0].target.x0! - links[0].source.x1!, 2) + Math.pow(links[0].target.y0! - links[0].source.y1!, 2)
|
|
162
|
+
) - largestGroupWidth
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Group className='' key={i}>
|
|
166
|
+
<rect
|
|
167
|
+
height={node.y1! - node.y0! + 2} // increasing node size to account for smaller nodes
|
|
168
|
+
width={maxNodeWidth}
|
|
169
|
+
x={node.x0}
|
|
170
|
+
y={node.y0! - 1} //adjusting here the node starts so it looks more center with the link
|
|
171
|
+
fill={nodeColor}
|
|
172
|
+
fillOpacity={opacityValue}
|
|
173
|
+
rx={sankeyConfig.rxValue}
|
|
174
|
+
// todo: move enable tooltips to sankey
|
|
175
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
176
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
177
|
+
onClick={() => handleNodeClick(node.id)}
|
|
178
|
+
style={{ pointerEvents: 'visible', cursor: 'pointer' }}
|
|
179
|
+
/>
|
|
180
|
+
{storyNodes ? (
|
|
181
|
+
<>
|
|
182
|
+
<Text
|
|
183
|
+
width={linkLength()}
|
|
184
|
+
/* Text Position Horizontal
|
|
185
|
+
x0 is the left edge of the node
|
|
186
|
+
# - positions text # units to the right of the left edge of the node */
|
|
187
|
+
x={node.x0! + textPositionHorizontal}
|
|
188
|
+
textAnchor={sankeyData.nodes.length - 1 === i ? 'end' : 'start'}
|
|
189
|
+
verticalAnchor='end'
|
|
190
|
+
/*Text Position Vertical
|
|
191
|
+
y1 and y0 are the top and bottom edges of the node
|
|
192
|
+
y1+y0 = total height
|
|
193
|
+
dividing by 2 gives you the midpoint of the node
|
|
194
|
+
minus 30 raises the vertical position to be higher
|
|
195
|
+
*/
|
|
196
|
+
y={(node.y1! + node.y0!) / 2 - 30}
|
|
197
|
+
/* Using x and y in combination with dominant baseline allows for a more
|
|
198
|
+
precise positioning of the text within the svg
|
|
199
|
+
dominant baseline allows for different vertical alignments
|
|
200
|
+
text-before-edge aligns the text's bottom edge with the bottom edge of the container
|
|
201
|
+
*/
|
|
202
|
+
fill={sankeyConfig.nodeFontColor}
|
|
203
|
+
fontWeight='bold' // font weight
|
|
204
|
+
className='node-text'
|
|
205
|
+
style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
|
|
206
|
+
onClick={() => handleNodeClick(node.id)}
|
|
207
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
208
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
209
|
+
>
|
|
210
|
+
{(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
|
|
211
|
+
</Text>
|
|
212
|
+
<Text
|
|
213
|
+
width={linkLength()}
|
|
214
|
+
verticalAnchor='middle'
|
|
215
|
+
className={classStyle}
|
|
216
|
+
x={node.x0! + textPositionHorizontal}
|
|
217
|
+
y={(node.y1! + node.y0! + 25) / 2}
|
|
218
|
+
fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor}
|
|
219
|
+
fontWeight='bold'
|
|
220
|
+
textAnchor='start'
|
|
221
|
+
style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
|
|
222
|
+
onClick={() => handleNodeClick(node.id)}
|
|
223
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
224
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
225
|
+
>
|
|
226
|
+
{typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
|
|
227
|
+
</Text>
|
|
228
|
+
<Text
|
|
229
|
+
width={linkLength()}
|
|
230
|
+
x={node.x0! + textPositionHorizontal}
|
|
231
|
+
// plus 50 will move the vertical position down
|
|
232
|
+
y={(node.y1! + node.y0!) / 2 + 50}
|
|
233
|
+
fill={sankeyConfig.nodeFontColor}
|
|
234
|
+
fontWeight='bold'
|
|
235
|
+
textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
|
|
236
|
+
className='node-text'
|
|
237
|
+
verticalAnchor='start'
|
|
238
|
+
style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
|
|
239
|
+
onClick={() => handleNodeClick(node.id)}
|
|
240
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
241
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
242
|
+
>
|
|
243
|
+
{(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
|
|
244
|
+
</Text>
|
|
245
|
+
</>
|
|
246
|
+
) : (
|
|
247
|
+
<>
|
|
248
|
+
<Text
|
|
249
|
+
style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
|
|
250
|
+
onClick={() => handleNodeClick(node.id)}
|
|
251
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
252
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
253
|
+
x={node.x0! + textPositionHorizontal}
|
|
254
|
+
y={(node.y1! + node.y0!) / 2 + textPositionVertical}
|
|
255
|
+
dominantBaseline='text-before-edge'
|
|
256
|
+
fill={sankeyConfig.nodeFontColor}
|
|
257
|
+
fontWeight='bold'
|
|
258
|
+
textAnchor='start'
|
|
259
|
+
>
|
|
260
|
+
{node.id}
|
|
261
|
+
</Text>
|
|
262
|
+
<text
|
|
263
|
+
x={node.x0! + textPositionHorizontal}
|
|
264
|
+
/* adding 30 allows the node value to be on the next line underneath the node id */
|
|
265
|
+
y={(node.y1! + node.y0!) / 2 + 30}
|
|
266
|
+
dominantBaseline='text-before-edge'
|
|
267
|
+
fill={sankeyConfig.nodeFontColor}
|
|
268
|
+
//fontSize={16}
|
|
269
|
+
fontWeight='bold'
|
|
270
|
+
textAnchor='start'
|
|
271
|
+
style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
|
|
272
|
+
onClick={() => handleNodeClick(node.id)}
|
|
273
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
274
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
275
|
+
>
|
|
276
|
+
<tspan className={classStyle}>
|
|
277
|
+
{sankeyConfig.nodeValueStyle.textBefore +
|
|
278
|
+
(typeof node.value === 'number' ? node.value.toLocaleString() : node.value) +
|
|
279
|
+
sankeyConfig.nodeValueStyle.textAfter}
|
|
280
|
+
</tspan>
|
|
281
|
+
</text>
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
</Group>
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// Draw the links
|
|
289
|
+
const allLinks = links.map((link, i) => {
|
|
290
|
+
const linkGenerator = sankeyLinkHorizontal()
|
|
291
|
+
const path = linkGenerator(link)
|
|
292
|
+
let opacityValue = sankeyConfig.opacity.LinkOpacityDefault
|
|
293
|
+
let strokeColor = sankeyConfig.linkColor.default
|
|
294
|
+
|
|
295
|
+
let { activeLinks } = activeConnection(tooltipID)
|
|
296
|
+
|
|
297
|
+
if (!activeLinks.includes(link) && tooltipID !== '') {
|
|
298
|
+
strokeColor = sankeyConfig.linkColor.inactive
|
|
299
|
+
opacityValue = sankeyConfig.opacity.LinkOpacityInactive
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<path
|
|
304
|
+
key={i}
|
|
305
|
+
d={path!}
|
|
306
|
+
stroke={strokeColor}
|
|
307
|
+
fill='none'
|
|
308
|
+
strokeOpacity={opacityValue}
|
|
309
|
+
strokeWidth={link.width! + 2}
|
|
310
|
+
style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
|
|
311
|
+
onClick={() => handleNodeClick(link.target.id || null)}
|
|
312
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
313
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
314
|
+
/>
|
|
315
|
+
)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
// max depth - calculates how many nodes deep the chart goes.
|
|
319
|
+
const maxDepth: number = sankeyData.nodes.reduce((maxDepth, node) => {
|
|
320
|
+
return Math.max(maxDepth, node.depth)
|
|
321
|
+
}, -1)
|
|
322
|
+
|
|
323
|
+
// finalNodesAtMaxDepth - get only the right most nodes on the chart.
|
|
324
|
+
const finalNodesAtMaxDepth = sankeyData.nodes.filter(node => node.depth === maxDepth)
|
|
325
|
+
|
|
326
|
+
const finalNodes = finalNodesAtMaxDepth.map((node, i) => {
|
|
327
|
+
let { textPositionHorizontal, textPositionVertical, classStyle, storyNodes } = nodeStyle(node.id)
|
|
328
|
+
let { sourceNodes } = activeConnection(tooltipID)
|
|
329
|
+
|
|
330
|
+
let opacityValue = sankeyConfig.opacity.nodeOpacityDefault
|
|
331
|
+
let nodeColor = sankeyConfig.nodeColor.default
|
|
332
|
+
|
|
333
|
+
if (tooltipID !== node.id && tooltipID !== '' && !sourceNodes.includes(node.id)) {
|
|
334
|
+
nodeColor = sankeyConfig.nodeColor.inactive
|
|
335
|
+
opacityValue = sankeyConfig.opacity.nodeOpacityInactive
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<Group className='' key={i} innerRef={el => (groupRefs.current[i] = el)}>
|
|
340
|
+
<rect
|
|
341
|
+
height={node.y1! - node.y0! + 2} // increasing node size to account for smaller nodes
|
|
342
|
+
width={sankeyGenerator.nodeWidth()}
|
|
343
|
+
x={node.x0}
|
|
344
|
+
y={node.y0! - 1} //adjusting here the node starts so it looks more center with the link
|
|
345
|
+
fill={nodeColor}
|
|
346
|
+
fillOpacity={opacityValue}
|
|
347
|
+
rx={sankeyConfig.rxValue}
|
|
348
|
+
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
349
|
+
data-tooltip-id={`tooltip`}
|
|
350
|
+
onClick={() => handleNodeClick(node.id)}
|
|
351
|
+
style={{ pointerEvents: 'visible', cursor: 'pointer' }}
|
|
352
|
+
/>
|
|
353
|
+
{storyNodes ? (
|
|
354
|
+
<>
|
|
355
|
+
<Text
|
|
356
|
+
/* Text Position Horizontal
|
|
357
|
+
x0 is the left edge of the node
|
|
358
|
+
# - positions text # units to the right of the left edge of the node */
|
|
359
|
+
x={node.x0! + textPositionHorizontal}
|
|
360
|
+
textAnchor={sankeyData.nodes.length - 1 === i ? 'end' : 'start'}
|
|
361
|
+
verticalAnchor='end'
|
|
362
|
+
/*Text Position Vertical
|
|
363
|
+
y1 and y0 are the top and bottom edges of the node
|
|
364
|
+
y1+y0 = total height
|
|
365
|
+
dividing by 2 gives you the midpoint of the node
|
|
366
|
+
minus 30 raises the vertical position to be higher
|
|
367
|
+
*/
|
|
368
|
+
y={(node.y1! + node.y0!) / 2 - 30}
|
|
369
|
+
/* Using x and y in combination with dominant baseline allows for a more
|
|
370
|
+
precise positioning of the text within the svg
|
|
371
|
+
dominant baseline allows for different vertical alignments
|
|
372
|
+
text-before-edge aligns the text's bottom edge with the bottom edge of the container
|
|
373
|
+
*/
|
|
374
|
+
fill={sankeyConfig.nodeFontColor}
|
|
375
|
+
fontWeight='bold' // font weight
|
|
376
|
+
style={{ pointerEvents: 'none' }}
|
|
377
|
+
className='node-text'
|
|
378
|
+
>
|
|
379
|
+
{(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
|
|
380
|
+
</Text>
|
|
381
|
+
<Text
|
|
382
|
+
verticalAnchor='end'
|
|
383
|
+
className={classStyle}
|
|
384
|
+
x={node.x0! + textPositionHorizontal}
|
|
385
|
+
y={(node.y1! + node.y0! + 25) / 2}
|
|
386
|
+
fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor}
|
|
387
|
+
fontWeight='bold'
|
|
388
|
+
textAnchor='start'
|
|
389
|
+
style={{ pointerEvents: 'none' }}
|
|
390
|
+
>
|
|
391
|
+
{typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
|
|
392
|
+
</Text>
|
|
393
|
+
<Text
|
|
394
|
+
x={node.x0! + textPositionHorizontal}
|
|
395
|
+
// plus 50 will move the vertical position down
|
|
396
|
+
y={(node.y1! + node.y0!) / 2 + 50}
|
|
397
|
+
fill={sankeyConfig.nodeFontColor}
|
|
398
|
+
fontWeight='bold'
|
|
399
|
+
textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
|
|
400
|
+
style={{ pointerEvents: 'none' }}
|
|
401
|
+
className='node-text'
|
|
402
|
+
verticalAnchor='end'
|
|
403
|
+
>
|
|
404
|
+
{(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
|
|
405
|
+
</Text>
|
|
406
|
+
</>
|
|
407
|
+
) : (
|
|
408
|
+
<>
|
|
409
|
+
<text
|
|
410
|
+
x={node.x0! + textPositionHorizontal}
|
|
411
|
+
y={(node.y1! + node.y0!) / 2 + textPositionVertical}
|
|
412
|
+
dominantBaseline='text-before-edge'
|
|
413
|
+
fill={sankeyConfig.nodeFontColor}
|
|
414
|
+
fontWeight='bold'
|
|
415
|
+
textAnchor='start'
|
|
416
|
+
style={{ pointerEvents: 'none' }}
|
|
417
|
+
>
|
|
418
|
+
<tspan id={node.id} className='node-id'>
|
|
419
|
+
{node.id}
|
|
420
|
+
</tspan>
|
|
421
|
+
</text>
|
|
422
|
+
<text
|
|
423
|
+
x={node.x0! + textPositionHorizontal}
|
|
424
|
+
/* adding 30 allows the node value to be on the next line underneath the node id */
|
|
425
|
+
y={(node.y1! + node.y0!) / 2 + 30}
|
|
426
|
+
dominantBaseline='text-before-edge'
|
|
427
|
+
fill={sankeyConfig.nodeFontColor}
|
|
428
|
+
fontWeight='bold'
|
|
429
|
+
textAnchor='start'
|
|
430
|
+
style={{ pointerEvents: 'none' }}
|
|
431
|
+
>
|
|
432
|
+
<tspan onClick={() => handleNodeClick(node.id)} className={classStyle}>
|
|
433
|
+
{sankeyConfig.nodeValueStyle.textBefore +
|
|
434
|
+
(typeof node.value === 'number' ? node.value.toLocaleString() : node.value) +
|
|
435
|
+
sankeyConfig.nodeValueStyle.textAfter}
|
|
436
|
+
</tspan>
|
|
437
|
+
</text>
|
|
438
|
+
</>
|
|
439
|
+
)}
|
|
440
|
+
</Group>
|
|
441
|
+
)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
return !showAlert ? (
|
|
445
|
+
<>
|
|
446
|
+
<div className='sankey-chart'>
|
|
447
|
+
<svg
|
|
448
|
+
className='sankey-chart__diagram'
|
|
449
|
+
width={width}
|
|
450
|
+
height={Number(config.heights.vertical)}
|
|
451
|
+
style={{ overflow: 'visible' }}
|
|
452
|
+
>
|
|
453
|
+
<Group className='links'>{allLinks}</Group>
|
|
454
|
+
<Group className='nodes'>{allNodes}</Group>
|
|
455
|
+
<Group className='finalNodes' style={{ display: 'none' }}>
|
|
456
|
+
{finalNodes}
|
|
457
|
+
</Group>
|
|
458
|
+
</svg>
|
|
459
|
+
|
|
460
|
+
{/* ReactTooltip needs to remain even if tooltips are disabled -- it handles when a user clicks off of the node and resets
|
|
461
|
+
the sankey diagram. When tooltips are disabled this will nothing */}
|
|
462
|
+
<ReactTooltip
|
|
463
|
+
id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
464
|
+
afterHide={() => setTooltipID('')}
|
|
465
|
+
events={['click']}
|
|
466
|
+
place={'bottom'}
|
|
467
|
+
style={{
|
|
468
|
+
backgroundColor: `rgba(238, 238, 238, 1)`,
|
|
469
|
+
color: 'black',
|
|
470
|
+
boxShadow: `0 3px 10px rgb(0 0 0 / 0.2)`
|
|
471
|
+
}}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
</>
|
|
475
|
+
) : (
|
|
476
|
+
alert
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
export default Sankey
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ReactDOMServer from 'react-dom/server'
|
|
2
|
+
import ColumnList from '../components/ColumnList'
|
|
3
|
+
|
|
4
|
+
export const getSankeyTooltip = (data: Object, tooltipID: string) => {
|
|
5
|
+
const tooltipVal = `${(data?.tooltips?.find(item => item.node === tooltipID) || {}).value}`
|
|
6
|
+
const tooltipSummary = `${(data?.tooltips?.find(item => item.node === tooltipID) || {}).summary}`
|
|
7
|
+
const tooltipColumn1Label = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column1Label
|
|
8
|
+
const tooltipColumn2Label = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column2Label
|
|
9
|
+
const tooltipColumn1 = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column1
|
|
10
|
+
const tooltipColumn2 = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column2
|
|
11
|
+
|
|
12
|
+
const tooltipColumn1Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn1} />)
|
|
13
|
+
const tooltipColumn2Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn2} />)
|
|
14
|
+
|
|
15
|
+
return `<div class="sankey-chart__tooltip">
|
|
16
|
+
<span class="sankey-chart__tooltip--tooltip-header">${tooltipID}</span>
|
|
17
|
+
<span class="sankey-chart__tooltip--tooltip-header">${tooltipVal}</span>
|
|
18
|
+
<div class="divider"></div>
|
|
19
|
+
<span><strong>Summary: </strong>${tooltipSummary}</span>
|
|
20
|
+
<div class="divider"></div>
|
|
21
|
+
<div class="sankey-chart__tooltip--info-section">
|
|
22
|
+
<div>
|
|
23
|
+
<span><strong>${tooltipColumn1Label}</strong></span>
|
|
24
|
+
${tooltipColumn1Data}
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<span><strong>${tooltipColumn2Label}</strong></span>
|
|
28
|
+
${tooltipColumn2Data}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
`
|
|
33
|
+
}
|