1
1
import React , { PropTypes } from 'react' ;
2
- import { layout } from 'd3' ;
2
+ import { layout , behavior , event , select } from 'd3' ;
3
3
import clone from 'clone' ;
4
4
import uuid from 'uuid' ;
5
5
@@ -13,45 +13,102 @@ export default class Tree extends React.Component {
13
13
super ( props ) ;
14
14
this . state = {
15
15
initialRender : true ,
16
- data : this . assignCustomProperties ( clone ( this . props . data ) ) ,
16
+ data : this . assignInternalProperties ( clone ( this . props . data ) ) ,
17
+ zoom : undefined ,
17
18
} ;
18
19
this . findTargetNode = this . findTargetNode . bind ( this ) ;
19
20
this . collapseNode = this . collapseNode . bind ( this ) ;
20
21
this . handleNodeToggle = this . handleNodeToggle . bind ( this ) ;
21
22
}
22
23
23
24
componentDidMount ( ) {
25
+ this . bindZoomListener ( ) ;
26
+
24
27
// TODO find better way of setting initialDepth, re-render here is suboptimal
25
28
this . setState ( { initialRender : false } ) ; // eslint-disable-line
26
29
}
27
30
28
31
componentWillReceiveProps ( nextProps ) {
32
+ // Clone new data & assign internal properties
29
33
if ( this . props . data !== nextProps . data ) {
30
- this . setState ( { data : this . assignCustomProperties ( clone ( nextProps . data ) ) } ) ;
34
+ this . setState ( {
35
+ data : this . assignInternalProperties ( clone ( nextProps . data ) ) ,
36
+ } ) ;
31
37
}
32
38
}
33
39
40
+
41
+ /**
42
+ * setInitialTreeDepth - Description
43
+ *
44
+ * @param {array } nodeSet Array of nodes generated by `generateTree`
45
+ * @param {number } initialDepth Maximum initial depth the tree should render
46
+ *
47
+ * @return {void }
48
+ */
34
49
setInitialTreeDepth ( nodeSet , initialDepth ) {
35
- console . log ( 'setInitialTreeDepth: ' , initialDepth ) ;
36
50
nodeSet . forEach ( ( n ) => {
37
51
n . _collapsed = n . depth >= initialDepth ;
38
52
} ) ;
39
53
}
40
54
41
- assignCustomProperties ( data ) {
55
+
56
+ /**
57
+ * bindZoomListener - If `props.zoomable`, binds a listener for
58
+ * "zoom" events to the SVG and sets scaleExtent to min/max
59
+ * specified in `props.scaleExtent`.
60
+ *
61
+ * @return {void }
62
+ */
63
+ bindZoomListener ( ) {
64
+ const { zoomable, scaleExtent } = this . props ;
65
+ const svg = select ( '.svg' ) ;
66
+
67
+ if ( zoomable ) {
68
+ this . setState ( { zoom : 'scale(1)' } ) ;
69
+ svg . call ( behavior . zoom ( )
70
+ . scaleExtent ( [ scaleExtent . min , scaleExtent . max ] )
71
+ . on ( 'zoom' , ( ) => {
72
+ this . setState ( { zoom : `scale(${ event . scale } )` } ) ;
73
+ } )
74
+ ) ;
75
+ }
76
+ }
77
+
78
+
79
+ /**
80
+ * assignInternalProperties - Assigns internal properties to each node in the
81
+ * `data` set that are required for tree manipulation and returns
82
+ * a new `data` array.
83
+ *
84
+ * @param {array } data Hierarchical tree data
85
+ *
86
+ * @return {array } `data` array with internal properties added
87
+ */
88
+ assignInternalProperties ( data ) {
42
89
return data . map ( ( node ) => {
43
90
node . id = uuid . v4 ( ) ;
44
91
node . _collapsed = false ;
45
92
// if there are children, recursively assign properties to them too
46
93
if ( node . children && node . children . length > 0 ) {
47
- node . children = this . assignCustomProperties ( node . children ) ;
94
+ node . children = this . assignInternalProperties ( node . children ) ;
48
95
node . _children = node . children ;
49
96
}
50
97
return node ;
51
98
} ) ;
52
99
}
53
100
54
- // TODO Refactor this into a more readable/reasonable recursive depth-first walk.
101
+
102
+ /**
103
+ * findTargetNode - Recursively walks a set of nodes (`nodeSet`) and its
104
+ * children until a `node.id` that matches `nodeId` param is found.
105
+ *
106
+ * @param {string } nodeId The `node.id` being searched for
107
+ * @param {array } nodeSet Array of `node` objects
108
+ *
109
+ * @return {object } Returns the targeted `node` object
110
+ */
111
+ // TODO Refactor this into a more readable/reasonable recursive depth-first walk.
55
112
findTargetNode ( nodeId , nodeSet ) {
56
113
const hits = nodeSet . filter ( ( node ) => node . id === nodeId ) ;
57
114
@@ -68,6 +125,15 @@ export default class Tree extends React.Component {
68
125
} ) [ 0 ] ;
69
126
}
70
127
128
+
129
+ /**
130
+ * collapseNode - Recursively sets the `_collapsed` property of
131
+ * the passed `node` object and its children to `true`.
132
+ *
133
+ * @param {object } node Node object with custom properties
134
+ *
135
+ * @return {void }
136
+ */
71
137
collapseNode ( node ) {
72
138
node . _collapsed = true ;
73
139
if ( node . _children && node . _children . length > 0 ) {
@@ -77,10 +143,29 @@ export default class Tree extends React.Component {
77
143
}
78
144
}
79
145
146
+
147
+ /**
148
+ * expandNode - Sets the `_collapsed` property of
149
+ * the passed `node` object to `false`.
150
+ *
151
+ * @param {type } node Node object with custom properties
152
+ *
153
+ * @return {void }
154
+ */
80
155
expandNode ( node ) {
81
156
node . _collapsed = false ;
82
157
}
83
158
159
+
160
+ /**
161
+ * handleNodeToggle - Finds the node matching `nodeId` and
162
+ * expands/collapses it, depending on the current state of
163
+ * its `_collapsed` property.
164
+ *
165
+ * @param {string } nodeId A node object's `id` field.
166
+ *
167
+ * @return {void }
168
+ */
84
169
handleNodeToggle ( nodeId ) {
85
170
if ( this . props . collapsible ) {
86
171
const data = clone ( this . state . data ) ;
@@ -92,6 +177,15 @@ export default class Tree extends React.Component {
92
177
}
93
178
}
94
179
180
+
181
+ /**
182
+ * generateTree - Generates tree elements (`nodes` and `links`) by
183
+ * grabbing the rootNode from `this.state.data[0]`.
184
+ * Restricts tree depth to `props.initial` if defined and this is
185
+ * the initial render of the tree.
186
+ *
187
+ * @return {object } Object containing `nodes` and `links` fields.
188
+ */
95
189
generateTree ( ) {
96
190
const { initialDepth } = this . props ;
97
191
const tree = layout . tree ( )
@@ -116,8 +210,16 @@ export default class Tree extends React.Component {
116
210
const { nodes, links } = this . generateTree ( ) ;
117
211
return (
118
212
< div className = "treeContainer" >
119
- < svg width = "100%" height = "100%" >
120
- < g transform = { `translate(${ translate . x } ,${ translate . y } )` } >
213
+ < svg
214
+ className = "svg"
215
+ width = "100%"
216
+ height = "100%"
217
+ transform = { this . state . zoom }
218
+ >
219
+ < g
220
+ className = "gWrapper"
221
+ transform = { `translate(${ translate . x } ,${ translate . y } )` }
222
+ >
121
223
122
224
{ nodes . map ( ( nodeData ) =>
123
225
< Node
@@ -153,6 +255,8 @@ Tree.defaultProps = {
153
255
pathFunc : 'diagonal' ,
154
256
collapsible : true ,
155
257
initialDepth : undefined ,
258
+ zoomable : true ,
259
+ scaleExtent : { min : 0.1 , max : 1 } ,
156
260
} ;
157
261
158
262
Tree . propTypes = {
@@ -171,4 +275,9 @@ Tree.propTypes = {
171
275
] ) ,
172
276
collapsible : PropTypes . bool ,
173
277
initialDepth : PropTypes . number ,
278
+ zoomable : PropTypes . bool ,
279
+ scaleExtent : PropTypes . shape ( {
280
+ min : PropTypes . number ,
281
+ max : PropTypes . number ,
282
+ } ) ,
174
283
} ;
0 commit comments