1
1
import { useState , useEffect , useMemo , useCallback } from 'react'
2
- import get from 'lodash/get'
3
- import isString from 'lodash/isString'
4
- import isNumber from 'lodash/isNumber'
5
2
import { forceSimulation , forceManyBody , forceCenter , forceLink } from 'd3-force'
6
3
import { useTheme } from '@nivo/core'
7
4
import { useInheritedColor } from '@nivo/colors'
@@ -14,53 +11,53 @@ import {
14
11
DerivedProp ,
15
12
ComputedNode ,
16
13
ComputedLink ,
14
+ TransientNode ,
15
+ TransientLink ,
17
16
} from './types'
18
17
19
- const computeForces = < Node extends InputNode > ( {
18
+ const useDerivedProp = < Target , Output extends string | number > (
19
+ instruction : DerivedProp < Target , Output >
20
+ ) =>
21
+ useMemo ( ( ) => {
22
+ if ( typeof instruction === 'function' ) return instruction
23
+ return ( ) => instruction
24
+ } , [ instruction ] )
25
+
26
+ const useComputeForces = < Node extends InputNode , Link extends InputLink > ( {
20
27
linkDistance,
21
28
repulsivity,
22
29
distanceMin,
23
30
distanceMax,
24
31
center,
25
32
} : {
26
- linkDistance : NetworkCommonProps < Node > [ 'linkDistance' ]
27
- repulsivity : NetworkCommonProps < Node > [ 'repulsivity' ]
28
- distanceMin : NetworkCommonProps < Node > [ 'distanceMin' ]
29
- distanceMax : NetworkCommonProps < Node > [ 'distanceMax' ]
33
+ linkDistance : NetworkCommonProps < Node , Link > [ 'linkDistance' ]
34
+ repulsivity : NetworkCommonProps < Node , Link > [ 'repulsivity' ]
35
+ distanceMin : NetworkCommonProps < Node , Link > [ 'distanceMin' ]
36
+ distanceMax : NetworkCommonProps < Node , Link > [ 'distanceMax' ]
30
37
center : [ number , number ]
31
38
} ) => {
32
- let getLinkDistance
33
- if ( typeof linkDistance === 'function' ) {
34
- getLinkDistance = linkDistance
35
- } else if ( isNumber ( linkDistance ) ) {
36
- getLinkDistance = linkDistance
37
- } else if ( isString ( linkDistance ) ) {
38
- getLinkDistance = ( link : InputLink ) => get ( link , linkDistance )
39
- }
39
+ const getLinkDistance = useDerivedProp < Link , number > ( linkDistance )
40
40
41
- const linkForce = forceLink ( )
42
- . id ( ( d : any ) => d . id )
43
- . distance ( getLinkDistance as any )
41
+ const centerX = center [ 0 ]
42
+ const centerY = center [ 1 ]
44
43
45
- const chargeForce = forceManyBody ( )
46
- . strength ( - repulsivity )
47
- . distanceMin ( distanceMin )
48
- . distanceMax ( distanceMax )
44
+ return useMemo ( ( ) => {
45
+ const linkForce = forceLink < TransientNode < Node > , TransientLink < Node , Link > > ( ) . distance (
46
+ link => getLinkDistance ( link . data )
47
+ )
49
48
50
- const centerForce = forceCenter ( center [ 0 ] , center [ 1 ] )
49
+ const chargeForce = forceManyBody ( )
50
+ . strength ( - repulsivity )
51
+ . distanceMin ( distanceMin )
52
+ . distanceMax ( distanceMax )
51
53
52
- return { link : linkForce , charge : chargeForce , center : centerForce }
53
- }
54
+ const centerForce = forceCenter ( centerX , centerY )
54
55
55
- const useDerivedProp = < Target , Output extends string | number > (
56
- instruction : DerivedProp < Target , Output >
57
- ) =>
58
- useMemo ( ( ) => {
59
- if ( typeof instruction === 'function' ) return instruction
60
- return ( ) => instruction
61
- } , [ instruction ] )
56
+ return { link : linkForce , charge : chargeForce , center : centerForce }
57
+ } , [ getLinkDistance , repulsivity , distanceMin , distanceMax , centerX , centerY ] )
58
+ }
62
59
63
- const useNodeStyle = < Node extends InputNode > ( {
60
+ const useNodeStyle = < Node extends InputNode , Link extends InputLink > ( {
64
61
size,
65
62
activeSize,
66
63
inactiveSize,
@@ -70,25 +67,23 @@ const useNodeStyle = <Node extends InputNode>({
70
67
isInteractive,
71
68
activeNodeIds,
72
69
} : {
73
- size : NetworkCommonProps < Node > [ 'nodeSize' ]
74
- activeSize : NetworkCommonProps < Node > [ 'activeNodeSize' ]
75
- inactiveSize : NetworkCommonProps < Node > [ 'inactiveNodeSize' ]
76
- color : NetworkCommonProps < Node > [ 'nodeColor' ]
77
- borderWidth : NetworkCommonProps < Node > [ 'nodeBorderWidth' ]
78
- borderColor : NetworkCommonProps < Node > [ 'nodeBorderColor' ]
79
- isInteractive : NetworkCommonProps < Node > [ 'isInteractive' ]
70
+ size : NetworkCommonProps < Node , Link > [ 'nodeSize' ]
71
+ activeSize : NetworkCommonProps < Node , Link > [ 'activeNodeSize' ]
72
+ inactiveSize : NetworkCommonProps < Node , Link > [ 'inactiveNodeSize' ]
73
+ color : NetworkCommonProps < Node , Link > [ 'nodeColor' ]
74
+ borderWidth : NetworkCommonProps < Node , Link > [ 'nodeBorderWidth' ]
75
+ borderColor : NetworkCommonProps < Node , Link > [ 'nodeBorderColor' ]
76
+ isInteractive : NetworkCommonProps < Node , Link > [ 'isInteractive' ]
80
77
activeNodeIds : string [ ]
81
78
} ) => {
82
- type IntermediateNode = Pick < ComputedNode < Node > , 'id' | 'data' | 'index' | 'x' | 'y' >
83
-
84
79
const theme = useTheme ( )
85
80
86
81
const getSize = useDerivedProp ( size )
87
82
const getColor = useDerivedProp ( color )
88
83
const getBorderWidth = useDerivedProp ( borderWidth )
89
84
const getBorderColor = useInheritedColor ( borderColor , theme )
90
85
const getNormalStyle = useCallback (
91
- ( node : IntermediateNode ) => {
86
+ ( node : TransientNode < Node > ) => {
92
87
const color = getColor ( node . data )
93
88
94
89
return {
@@ -103,7 +98,7 @@ const useNodeStyle = <Node extends InputNode>({
103
98
104
99
const getActiveSize = useDerivedProp ( activeSize )
105
100
const getActiveStyle = useCallback (
106
- ( node : IntermediateNode ) => {
101
+ ( node : TransientNode < Node > ) => {
107
102
const color = getColor ( node . data )
108
103
109
104
return {
@@ -118,7 +113,7 @@ const useNodeStyle = <Node extends InputNode>({
118
113
119
114
const getInactiveSize = useDerivedProp ( inactiveSize )
120
115
const getInactiveStyle = useCallback (
121
- ( node : IntermediateNode ) => {
116
+ ( node : TransientNode < Node > ) => {
122
117
const color = getColor ( node . data )
123
118
124
119
return {
@@ -132,7 +127,7 @@ const useNodeStyle = <Node extends InputNode>({
132
127
)
133
128
134
129
return useCallback (
135
- ( node : IntermediateNode ) => {
130
+ ( node : TransientNode < Node > ) => {
136
131
if ( ! isInteractive || activeNodeIds . length === 0 ) return getNormalStyle ( node )
137
132
if ( activeNodeIds . includes ( node . id ) ) return getActiveStyle ( node )
138
133
return getInactiveStyle ( node )
@@ -141,7 +136,7 @@ const useNodeStyle = <Node extends InputNode>({
141
136
)
142
137
}
143
138
144
- export const useNetwork = < Node extends InputNode = InputNode > ( {
139
+ export const useNetwork = < Node extends InputNode = InputNode , Link extends InputLink = InputLink > ( {
145
140
center,
146
141
nodes,
147
142
links,
@@ -162,87 +157,78 @@ export const useNetwork = <Node extends InputNode = InputNode>({
162
157
} : {
163
158
center : [ number , number ]
164
159
nodes : Node [ ]
165
- links : InputLink [ ]
166
- linkDistance ?: NetworkCommonProps < Node > [ 'linkDistance' ]
167
- repulsivity ?: NetworkCommonProps < Node > [ 'repulsivity' ]
168
- distanceMin ?: NetworkCommonProps < Node > [ 'distanceMin' ]
169
- distanceMax ?: NetworkCommonProps < Node > [ 'distanceMax' ]
170
- iterations ?: NetworkCommonProps < Node > [ 'iterations' ]
171
- nodeSize ?: NetworkCommonProps < Node > [ 'nodeSize' ]
172
- activeNodeSize ?: NetworkCommonProps < Node > [ 'activeNodeSize' ]
173
- inactiveNodeSize ?: NetworkCommonProps < Node > [ 'inactiveNodeSize' ]
174
- nodeColor ?: NetworkCommonProps < Node > [ 'nodeColor' ]
175
- nodeBorderWidth ?: NetworkCommonProps < Node > [ 'nodeBorderWidth' ]
176
- nodeBorderColor ?: NetworkCommonProps < Node > [ 'nodeBorderColor' ]
177
- linkThickness ?: NetworkCommonProps < Node > [ 'linkThickness' ]
178
- linkColor ?: NetworkCommonProps < Node > [ 'linkColor' ]
179
- isInteractive ?: NetworkCommonProps < Node > [ 'isInteractive' ]
160
+ links : Link [ ]
161
+ linkDistance ?: NetworkCommonProps < Node , Link > [ 'linkDistance' ]
162
+ repulsivity ?: NetworkCommonProps < Node , Link > [ 'repulsivity' ]
163
+ distanceMin ?: NetworkCommonProps < Node , Link > [ 'distanceMin' ]
164
+ distanceMax ?: NetworkCommonProps < Node , Link > [ 'distanceMax' ]
165
+ iterations ?: NetworkCommonProps < Node , Link > [ 'iterations' ]
166
+ nodeSize ?: NetworkCommonProps < Node , Link > [ 'nodeSize' ]
167
+ activeNodeSize ?: NetworkCommonProps < Node , Link > [ 'activeNodeSize' ]
168
+ inactiveNodeSize ?: NetworkCommonProps < Node , Link > [ 'inactiveNodeSize' ]
169
+ nodeColor ?: NetworkCommonProps < Node , Link > [ 'nodeColor' ]
170
+ nodeBorderWidth ?: NetworkCommonProps < Node , Link > [ 'nodeBorderWidth' ]
171
+ nodeBorderColor ?: NetworkCommonProps < Node , Link > [ 'nodeBorderColor' ]
172
+ linkThickness ?: NetworkCommonProps < Node , Link > [ 'linkThickness' ]
173
+ linkColor ?: NetworkCommonProps < Node , Link > [ 'linkColor' ]
174
+ isInteractive ?: NetworkCommonProps < Node , Link > [ 'isInteractive' ]
180
175
} ) => {
181
176
// we're using `null` instead of empty array so that we can dissociate
182
177
// initial rendering from updates when using transitions.
183
- const [ currentNodes , setCurrentNodes ] = useState <
184
- null | Pick < ComputedNode < Node > , 'id' | 'data' | 'index' | 'x' | 'y' > [ ]
185
- > ( null )
186
- const [ currentLinks , setCurrentLinks ] = useState < null | ComputedLink < Node > [ ] > ( null )
178
+ const [ transientNodes , setTransientNodes ] = useState < null | TransientNode < Node > [ ] > ( null )
179
+ const [ transientLinks , setTransientLinks ] = useState < null | TransientLink < Node , Link > [ ] > ( null )
187
180
188
- const centerX = center [ 0 ]
189
- const centerY = center [ 1 ]
181
+ const forces = useComputeForces < Node , Link > ( {
182
+ linkDistance,
183
+ repulsivity,
184
+ distanceMin,
185
+ distanceMax,
186
+ center,
187
+ } )
190
188
191
189
useEffect ( ( ) => {
192
- const forces = computeForces < Node > ( {
193
- linkDistance,
194
- repulsivity,
195
- distanceMin,
196
- distanceMax,
197
- center : [ centerX , centerY ] ,
198
- } )
199
-
200
- const nodesCopy : Pick < ComputedNode < Node > , 'id' | 'data' | 'index' | 'x' | 'y' > [ ] =
201
- nodes . map ( node => ( {
202
- id : node . id ,
203
- data : { ...node } ,
204
- // populated later by D3, mutation
205
- index : 0 ,
206
- x : 0 ,
207
- y : 0 ,
208
- } ) )
209
- const linksCopy : InputLink [ ] = links . map ( link => ( {
210
- // generate a unique id for each link
211
- id : `${ link . source } .${ link . target } ` ,
212
- ...link ,
190
+ // copy the nodes & links to avoid mutating the original ones.
191
+ const _transientNodes : TransientNode < Node > [ ] = nodes . map ( node => ( {
192
+ id : node . id ,
193
+ data : { ...node } ,
194
+ // the properties below are populated by D3, via mutations
195
+ index : 0 ,
196
+ x : 0 ,
197
+ y : 0 ,
198
+ vx : 0 ,
199
+ vy : 0 ,
200
+ } ) )
201
+ const _transientLinks : TransientLink < Node , Link > [ ] = links . map ( link => ( {
202
+ data : { ...link } ,
203
+ // populated by D3, via mutation
204
+ index : 0 ,
205
+ // replace ids with objects, otherwise D3 does this automatically
206
+ // which is a bit annoying with typings because then `source` & `target`
207
+ // can be either strings (before the simulation) or an objects (after).
208
+ source : _transientNodes . find ( node => node . id === link . source ) ! ,
209
+ target : _transientNodes . find ( node => node . id === link . target ) ! ,
213
210
} ) )
214
211
215
- const simulation = forceSimulation ( nodesCopy as any [ ] )
216
- . force ( 'link' , forces . link . links ( linksCopy ) )
212
+ const simulation = forceSimulation ( _transientNodes )
213
+ . force ( 'link' , forces . link . links ( _transientLinks ) )
217
214
. force ( 'charge' , forces . charge )
218
215
. force ( 'center' , forces . center )
219
216
. stop ( )
220
217
218
+ // this will set `index`, `x`, `y`, `vx`, `vy` for each node.
221
219
simulation . tick ( iterations )
222
220
223
- // d3 mutates data, hence the castings
224
- setCurrentNodes ( nodesCopy )
225
- setCurrentLinks ( linksCopy as unknown as ComputedLink < Node > [ ] )
221
+ setTransientNodes ( _transientNodes )
222
+ setTransientLinks ( _transientLinks )
226
223
227
224
return ( ) => {
228
- // prevent the simulation from continuing in case the data is updated.
229
225
simulation . stop ( )
230
226
}
231
- } , [
232
- centerX ,
233
- centerY ,
234
- nodes ,
235
- links ,
236
- linkDistance ,
237
- repulsivity ,
238
- distanceMin ,
239
- distanceMax ,
240
- iterations ,
241
- ] )
227
+ } , [ nodes , links , forces , iterations , setTransientNodes , setTransientLinks ] )
242
228
243
229
const [ activeNodeIds , setActiveNodeIds ] = useState < string [ ] > ( [ ] )
244
230
245
- const getNodeStyle = useNodeStyle < Node > ( {
231
+ const getNodeStyle = useNodeStyle < Node , Link > ( {
246
232
size : nodeSize ,
247
233
activeSize : activeNodeSize ,
248
234
inactiveSize : inactiveNodeSize ,
@@ -252,39 +238,42 @@ export const useNetwork = <Node extends InputNode = InputNode>({
252
238
isInteractive,
253
239
activeNodeIds,
254
240
} )
255
- const enhancedNodes : ComputedNode < Node > [ ] | null = useMemo ( ( ) => {
256
- if ( currentNodes === null ) return null
241
+ const computedNodes : ComputedNode < Node > [ ] | null = useMemo ( ( ) => {
242
+ if ( transientNodes === null ) return null
257
243
258
- return currentNodes . map ( node => ( {
244
+ return transientNodes . map ( node => ( {
259
245
...node ,
260
246
...getNodeStyle ( node ) ,
261
247
} ) )
262
- } , [ currentNodes , getNodeStyle ] )
248
+ } , [ transientNodes , getNodeStyle ] )
263
249
264
250
const theme = useTheme ( )
265
251
const getLinkThickness = useDerivedProp ( linkThickness )
266
252
const getLinkColor = useInheritedColor ( linkColor , theme )
267
- const enhancedLinks : ComputedLink < Node > [ ] | null = useMemo ( ( ) => {
268
- if ( currentLinks === null || enhancedNodes === null ) return null
269
-
270
- return currentLinks . map ( link => {
271
- const linkWithEnhancedNodes = {
272
- ...link ,
273
- source : enhancedNodes . find ( node => node . id === link . source . id ) ! ,
274
- target : enhancedNodes . find ( node => node . id === link . target . id ) ! ,
253
+
254
+ const computedLinks : ComputedLink < Node , Link > [ ] | null = useMemo ( ( ) => {
255
+ if ( transientLinks === null || computedNodes === null ) return null
256
+
257
+ return transientLinks . map ( ( { index, ...link } ) => {
258
+ const linkWithComputedNodes : Omit < ComputedLink < Node , Link > , 'color' | 'thickness' > = {
259
+ id : `${ link . source . id } .${ link . target . id } ` ,
260
+ data : link . data ,
261
+ index,
262
+ source : computedNodes . find ( node => node . id === link . source . id ) ! ,
263
+ target : computedNodes . find ( node => node . id === link . target . id ) ! ,
275
264
}
276
265
277
266
return {
278
- ...linkWithEnhancedNodes ,
279
- thickness : getLinkThickness ( linkWithEnhancedNodes ) ,
280
- color : getLinkColor ( linkWithEnhancedNodes ) ,
267
+ ...linkWithComputedNodes ,
268
+ thickness : getLinkThickness ( linkWithComputedNodes ) ,
269
+ color : getLinkColor ( linkWithComputedNodes ) ,
281
270
}
282
271
} )
283
- } , [ currentLinks , getLinkThickness , getLinkColor , enhancedNodes ] )
272
+ } , [ transientLinks , computedNodes , getLinkThickness , getLinkColor ] )
284
273
285
274
return {
286
- nodes : enhancedNodes ,
287
- links : enhancedLinks ,
275
+ nodes : computedNodes ,
276
+ links : computedLinks ,
288
277
setActiveNodeIds,
289
278
}
290
279
}
0 commit comments