@@ -18,7 +18,7 @@ const defaultOptions = {
18
18
collapsed : 'lightsteelblue' ,
19
19
parent : 'white'
20
20
} ,
21
- radius : 5
21
+ radius : 7
22
22
} ,
23
23
text : {
24
24
colors : {
@@ -44,6 +44,7 @@ const defaultOptions = {
44
44
heightBetweenNodesCoeff : 2 ,
45
45
widthBetweenNodesCoeff : 1 ,
46
46
transitionDuration : 750 ,
47
+ blinkDuration : 100 ,
47
48
onClickText : ( ) => { } ,
48
49
tooltipOptions : {
49
50
disabled : false ,
@@ -69,6 +70,7 @@ export default function(DOMNode, options = {}) {
69
70
widthBetweenNodesCoeff,
70
71
heightBetweenNodesCoeff,
71
72
transitionDuration,
73
+ blinkDuration,
72
74
state,
73
75
rootKeyName,
74
76
pushMethod,
@@ -119,6 +121,34 @@ export default function(DOMNode, options = {}) {
119
121
layout . sort ( ( a , b ) => b . name . toLowerCase ( ) < a . name . toLowerCase ( ) ? 1 : - 1 )
120
122
}
121
123
124
+ // previousNodePositionsById stores node x and y
125
+ // as well as hierarchy (id / parentId);
126
+ // helps animating transitions
127
+ let previousNodePositionsById = {
128
+ root : {
129
+ id : 'root' ,
130
+ parentId : null ,
131
+ x : height / 2 ,
132
+ y : 0
133
+ }
134
+ }
135
+
136
+ // traverses a map with node positions by going through the chain
137
+ // of parent ids; once a parent that matches the given filter is found,
138
+ // the parent position gets returned
139
+ function findParentNodePosition ( nodePositionsById , nodeId , filter ) {
140
+ let currentPosition = nodePositionsById [ nodeId ]
141
+ while ( currentPosition ) {
142
+ currentPosition = nodePositionsById [ currentPosition . parentId ]
143
+ if ( ! currentPosition ) {
144
+ return null
145
+ }
146
+ if ( ! filter || filter ( currentPosition ) ) {
147
+ return currentPosition
148
+ }
149
+ }
150
+ }
151
+
122
152
return function renderChart ( nextState = tree || state ) {
123
153
data = ! tree ? map2tree ( nextState , { key : rootKeyName , pushMethod} ) : nextState
124
154
@@ -129,18 +159,25 @@ export default function(DOMNode, options = {}) {
129
159
let nodeIndex = 0
130
160
let maxLabelLength = 0
131
161
162
+ // nodes are assigned with string ids, which reflect their location
163
+ // within the hierarcy; e.g. "root|branch|subBranch|subBranch[0]|property"
164
+ // top-level elemnt always has id "root"
132
165
visit ( data ,
133
- node => maxLabelLength = Math . max ( node . name . length , maxLabelLength ) ,
134
- node => node . children && node . children . length > 0 ? node . children : null
166
+ node => {
167
+ maxLabelLength = Math . max ( node . name . length , maxLabelLength )
168
+ node . id = node . id || 'root'
169
+ } ,
170
+ node => node . children && node . children . length > 0 ? node . children . map ( ( c ) => {
171
+ c . id = `${ node . id || '' } |${ c . name } `
172
+ return c
173
+ } ) : null
135
174
)
136
175
137
- data . x0 = height / 2
138
- data . y0 = 0
139
176
/*eslint-disable*/
140
- update ( data )
177
+ update ( )
141
178
/*eslint-enable*/
142
179
143
- function update ( source ) {
180
+ function update ( ) {
144
181
// path generator for links
145
182
const diagonal = d3 . svg . diagonal ( ) . projection ( d => [ d . y , d . x ] )
146
183
// set tree dimensions and spacing between branches and nodes
@@ -153,27 +190,39 @@ export default function(DOMNode, options = {}) {
153
190
154
191
nodes . forEach ( node => node . y = node . depth * ( maxLabelLength * 7 * widthBetweenNodesCoeff ) )
155
192
193
+ const nodePositions = nodes . map ( n => ( {
194
+ parentId : n . parent && n . parent . id ,
195
+ id : n . id ,
196
+ x : n . x ,
197
+ y : n . y
198
+ } ) )
199
+ const nodePositionsById = { }
200
+ nodePositions . forEach ( node => nodePositionsById [ node . id ] = node )
201
+
156
202
// process the node selection
157
203
let node = vis . selectAll ( 'g.node' )
158
204
. property ( '__oldData__' , d => d )
159
205
. data ( nodes , d => d . id || ( d . id = ++ nodeIndex ) )
160
-
161
206
let nodeEnter = node . enter ( ) . append ( 'g' )
162
207
. attr ( {
163
208
'class' : 'node' ,
164
- transform : d => `translate(${ source . y0 } ,${ source . x0 } )`
209
+ transform : d => {
210
+ const position = findParentNodePosition ( nodePositionsById , d . id , ( n ) => previousNodePositionsById [ n . id ] )
211
+ const previousPosition = position && previousNodePositionsById [ position . id ] || previousNodePositionsById . root
212
+ return `translate(${ previousPosition . y } ,${ previousPosition . x } )`
213
+ }
165
214
} )
166
215
. style ( {
167
216
fill : style . text . colors . default ,
168
217
cursor : 'pointer'
169
218
} )
170
219
. on ( {
171
- mouseover : function mouseover ( d , i ) {
220
+ mouseover : function mouseover ( ) {
172
221
d3 . select ( this ) . style ( {
173
222
fill : style . text . colors . hover
174
223
} )
175
224
} ,
176
- mouseout : function mouseout ( d , i ) {
225
+ mouseout : function mouseout ( ) {
177
226
d3 . select ( this ) . style ( {
178
227
fill : style . text . colors . default
179
228
} )
@@ -187,20 +236,27 @@ export default function(DOMNode, options = {}) {
187
236
)
188
237
}
189
238
190
- nodeEnter . append ( 'circle' )
239
+ // g inside node contains circle and text
240
+ // this extra wrapper helps run d3 transitions in parallel
241
+ const nodeEnterInnerGroup = nodeEnter . append ( 'g' )
242
+ nodeEnterInnerGroup . append ( 'circle' )
191
243
. attr ( {
192
- 'class' : 'nodeCircle'
244
+ 'class' : 'nodeCircle' ,
245
+ r : 0
193
246
} )
194
247
. on ( {
195
248
click : clickedNode => {
196
249
if ( d3 . event . defaultPrevented ) return
197
- update ( toggleChildren ( clickedNode ) )
250
+ toggleChildren ( clickedNode )
251
+ update ( )
198
252
}
199
253
} )
200
254
201
- nodeEnter . append ( 'text' )
255
+ nodeEnterInnerGroup . append ( 'text' )
202
256
. attr ( {
203
257
'class' : 'nodeText' ,
258
+ 'text-anchor' : 'middle' ,
259
+ 'transform' : `translate(0,0)` ,
204
260
dy : '.35em'
205
261
} )
206
262
. style ( {
@@ -213,17 +269,10 @@ export default function(DOMNode, options = {}) {
213
269
214
270
// update the text to reflect whether node has children or not
215
271
node . select ( 'text' )
216
- . attr ( {
217
- x : d => d . children || d . _children ? - ( style . node . radius + 10 ) : style . node . radius + 10 ,
218
- 'text-anchor' : d => d . children || d . _children ? 'end' : 'start'
219
- } )
220
272
. text ( d => d . name )
221
273
222
274
// change the circle fill depending on whether it has children and is collapsed
223
- node . select ( 'circle.nodeCircle' )
224
- . attr ( {
225
- r : style . node . radius
226
- } )
275
+ node . select ( 'circle' )
227
276
. style ( {
228
277
stroke : 'black' ,
229
278
'stroke-width' : '1.5px' ,
@@ -237,29 +286,41 @@ export default function(DOMNode, options = {}) {
237
286
transform : d => `translate(${ d . y } ,${ d . x } )`
238
287
} )
239
288
240
- // fade the text in
289
+ // ensure circle radius is correct
290
+ nodeUpdate . select ( 'circle' )
291
+ . attr ( 'r' , style . node . radius )
292
+
293
+ // fade the text in and align it
241
294
nodeUpdate . select ( 'text' )
242
295
. style ( 'fill-opacity' , 1 )
243
-
244
- // restore the circle
245
- nodeUpdate . select ( 'circle' ) . attr ( 'r' , 7 )
296
+ . attr ( {
297
+ transform : function transform ( d ) {
298
+ const x = ( d . children || d . _children ? - 1 : 1 ) * ( this . getBBox ( ) . width / 2 + style . node . radius + 5 )
299
+ return `translate(${ x } ,0)`
300
+ }
301
+ } )
246
302
247
303
// blink updated nodes
248
- nodeUpdate . filter ( function flick ( d ) {
304
+ node . filter ( function flick ( d ) {
249
305
// test whether the relevant properties of d match
250
306
// the equivalent property of the oldData
251
307
// also test whether the old data exists,
252
308
// to catch the entering elements!
253
- return ( ! this . __oldData__ || d . value !== this . __oldData__ . value )
309
+ return ( this . __oldData__ && d . value !== this . __oldData__ . value )
254
310
} )
255
- . style ( 'fill-opacity' , '0.3' ) . transition ( )
256
- . duration ( 100 ) . style ( 'fill-opacity' , '1' )
311
+ . select ( 'g' )
312
+ . style ( 'opacity' , '0.3' ) . transition ( )
313
+ . duration ( blinkDuration ) . style ( 'opacity' , '1' )
257
314
258
315
// transition exiting nodes to the parent's new position
259
316
let nodeExit = node . exit ( ) . transition ( )
260
317
. duration ( transitionDuration )
261
318
. attr ( {
262
- transform : d => `translate(${ source . y } ,${ source . x } )`
319
+ transform : d => {
320
+ const position = findParentNodePosition ( previousNodePositionsById , d . id , ( n ) => nodePositionsById [ n . id ] )
321
+ const futurePosition = position && nodePositionsById [ position . id ] || nodePositionsById . root
322
+ return `translate(${ futurePosition . y } ,${ futurePosition . x } )`
323
+ }
263
324
} )
264
325
. remove ( )
265
326
@@ -278,13 +339,11 @@ export default function(DOMNode, options = {}) {
278
339
. attr ( {
279
340
'class' : 'link' ,
280
341
d : d => {
281
- let o = {
282
- x : source . x0 ,
283
- y : source . y0
284
- }
342
+ const position = findParentNodePosition ( nodePositionsById , d . target . id , ( n ) => previousNodePositionsById [ n . id ] )
343
+ const previousPosition = position && previousNodePositionsById [ position . id ] || previousNodePositionsById . root
285
344
return diagonal ( {
286
- source : o ,
287
- target : o
345
+ source : previousPosition ,
346
+ target : previousPosition
288
347
} )
289
348
}
290
349
} )
@@ -298,17 +357,16 @@ export default function(DOMNode, options = {}) {
298
357
} )
299
358
300
359
// transition exiting nodes to the parent's new position
301
- link . exit ( ) . transition ( )
360
+ link . exit ( )
361
+ . transition ( )
302
362
. duration ( transitionDuration )
303
363
. attr ( {
304
364
d : d => {
305
- let o = {
306
- x : source . x ,
307
- y : source . y
308
- }
365
+ const position = findParentNodePosition ( previousNodePositionsById , d . target . id , ( n ) => nodePositionsById [ n . id ] )
366
+ const futurePosition = position && nodePositionsById [ position . id ] || nodePositionsById . root
309
367
return diagonal ( {
310
- source : o ,
311
- target : o
368
+ source : futurePosition ,
369
+ target : futurePosition
312
370
} )
313
371
}
314
372
} )
@@ -318,10 +376,7 @@ export default function(DOMNode, options = {}) {
318
376
node . property ( '__oldData__' , null )
319
377
320
378
// stash the old positions for transition
321
- nodes . forEach ( d => {
322
- d . x0 = d . x
323
- d . y0 = d . y
324
- } )
379
+ previousNodePositionsById = nodePositionsById
325
380
}
326
381
}
327
382
}
0 commit comments