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