SHOW:
|
|
- or go back to the newest paste.
1 | /*Copyright (c) 2013-2016, Rob Schmuecker | |
2 | All rights reserved. | |
3 | ||
4 | Redistribution and use in source and binary forms, with or without | |
5 | modification, are permitted provided that the following conditions are met: | |
6 | ||
7 | * Redistributions of source code must retain the above copyright notice, this | |
8 | list of conditions and the following disclaimer. | |
9 | ||
10 | * Redistributions in binary form must reproduce the above copyright notice, | |
11 | this list of conditions and the following disclaimer in the documentation | |
12 | and/or other materials provided with the distribution. | |
13 | ||
14 | * The name Rob Schmuecker may not be used to endorse or promote products | |
15 | derived from this software without specific prior written permission. | |
16 | ||
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, | |
21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | |
22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, | |
26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.*/ | |
27 | ||
28 | ||
29 | // Get JSON data | |
30 | treeJSON = d3.json("flare.json", function (error, treeData) { | |
31 | ||
32 | // Calculate total nodes, max label length | |
33 | var totalNodes = 0; | |
34 | var maxLabelLength = 0; | |
35 | // variables for drag/drop | |
36 | var selectedNode = null; | |
37 | var draggingNode = null; | |
38 | // panning variables | |
39 | var panSpeed = 200; | |
40 | var panBoundary = 20; // Within 20px from edges will pan when dragging. | |
41 | // Misc. variables | |
42 | var i = 0; | |
43 | var duration = 750; | |
44 | var root; | |
45 | ||
46 | // size of the diagram | |
47 | var viewerWidth = $(document).width(); | |
48 | var viewerHeight = $(document).height(); | |
49 | ||
50 | var tree = d3.layout.tree() | |
51 | .size([viewerHeight, viewerWidth]); | |
52 | ||
53 | // QUA SOTTO CAMBIA L'ORIENTAMENTO DEGLI ARCHETTI! | |
54 | // define a d3 diagonal projection for use by the node paths later on. | |
55 | var diagonal = d3.svg.diagonal() | |
56 | .projection(function (d) { | |
57 | return [d.x, d.y]; | |
58 | }); | |
59 | ||
60 | // A recursive helper function for performing some setup by walking through all nodes | |
61 | ||
62 | function visit(parent, visitFn, childrenFn) { | |
63 | if (!parent) | |
64 | return; | |
65 | ||
66 | visitFn(parent); | |
67 | ||
68 | var children = childrenFn(parent); | |
69 | if (children) { | |
70 | var count = children.length; | |
71 | for (var i = 0; i < count; i++) { | |
72 | visit(children[i], visitFn, childrenFn); | |
73 | } | |
74 | } | |
75 | } | |
76 | ||
77 | // Call visit function to establish maxLabelLength | |
78 | visit(treeData, function (d) { | |
79 | totalNodes++; | |
80 | maxLabelLength = Math.max(d.name.length, maxLabelLength); | |
81 | ||
82 | }, function (d) { | |
83 | return d.children && d.children.length > 0 ? d.children : null; | |
84 | }); | |
85 | ||
86 | ||
87 | // sort the tree according to the node names | |
88 | ||
89 | function sortTree() { | |
90 | tree.sort(function (a, b) { | |
91 | return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1; | |
92 | }); | |
93 | } | |
94 | // Sort the tree initially incase the JSON isn't in a sorted order. | |
95 | sortTree(); | |
96 | ||
97 | // TODO: Pan function, can be better implemented. | |
98 | ||
99 | function pan(domNode, direction) { | |
100 | var speed = panSpeed; | |
101 | if (panTimer) { | |
102 | clearTimeout(panTimer); | |
103 | translateCoords = d3.transform(svgGroup.attr("transform")); | |
104 | if (direction == 'left' || direction == 'right') { | |
105 | translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed; | |
106 | translateY = translateCoords.translate[1]; | |
107 | } else if (direction == 'up' || direction == 'down') { | |
108 | translateX = translateCoords.translate[0]; | |
109 | translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed; | |
110 | } | |
111 | scaleX = translateCoords.scale[0]; | |
112 | scaleY = translateCoords.scale[1]; | |
113 | scale = zoomListener.scale(); | |
114 | svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); | |
115 | d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")"); | |
116 | zoomListener.scale(zoomListener.scale()); | |
117 | zoomListener.translate([translateX, translateY]); | |
118 | panTimer = setTimeout(function () { | |
119 | pan(domNode, speed, direction); | |
120 | }, 50); | |
121 | } | |
122 | } | |
123 | ||
124 | // Define the zoom function for the zoomable tree | |
125 | ||
126 | function zoom() { | |
127 | svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); | |
128 | } | |
129 | ||
130 | ||
131 | // define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents | |
132 | var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); | |
133 | ||
134 | function initiateDrag(d, domNode) { | |
135 | draggingNode = d; | |
136 | d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none'); | |
137 | d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show'); | |
138 | d3.select(domNode).attr('class', 'node activeDrag'); | |
139 | ||
140 | svgGroup.selectAll("g.node").sort(function (a, b) { // select the parent and sort the path's | |
141 | if (a.id != draggingNode.id) | |
142 | return 1; // a is not the hovered element, send "a" to the back | |
143 | else | |
144 | return -1; // a is the hovered element, bring "a" to the front | |
145 | }); | |
146 | // if nodes has children, remove the links and nodes | |
147 | if (nodes.length > 1) { | |
148 | // remove link paths | |
149 | links = tree.links(nodes); | |
150 | nodePaths = svgGroup.selectAll("path.link") | |
151 | .data(links, function (d) { | |
152 | return d.target.id; | |
153 | }).remove(); | |
154 | // remove child nodes | |
155 | nodesExit = svgGroup.selectAll("g.node") | |
156 | .data(nodes, function (d) { | |
157 | return d.id; | |
158 | }).filter(function (d, i) { | |
159 | if (d.id == draggingNode.id) { | |
160 | return false; | |
161 | } | |
162 | return true; | |
163 | }).remove(); | |
164 | } | |
165 | ||
166 | // remove parent link | |
167 | parentLink = tree.links(tree.nodes(draggingNode.parent)); | |
168 | svgGroup.selectAll('path.link').filter(function (d, i) { | |
169 | if (d.target.id == draggingNode.id) { | |
170 | return true; | |
171 | } | |
172 | return false; | |
173 | }).remove(); | |
174 | ||
175 | dragStarted = null; | |
176 | } | |
177 | ||
178 | // define the baseSvg, attaching a class for styling and the zoomListener | |
179 | var baseSvg = d3.select("#tree-container").append("svg") | |
180 | .attr("width", viewerWidth) | |
181 | .attr("height", viewerHeight) | |
182 | .attr("class", "overlay") | |
183 | .call(zoomListener); | |
184 | ||
185 | ||
186 | // Define the drag listeners for drag/drop behaviour of nodes. | |
187 | dragListener = d3.behavior.drag() | |
188 | .on("dragstart", function (d) { | |
189 | if (d == root) { | |
190 | return; | |
191 | } | |
192 | dragStarted = true; | |
193 | nodes = tree.nodes(d); | |
194 | d3.event.sourceEvent.stopPropagation(); | |
195 | // it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none'); | |
196 | }) | |
197 | .on("drag", function (d) { | |
198 | if (d == root) { | |
199 | return; | |
200 | } | |
201 | if (dragStarted) { | |
202 | domNode = this; | |
203 | initiateDrag(d, domNode); | |
204 | } | |
205 | ||
206 | // get coords of mouseEvent relative to svg container to allow for panning | |
207 | relCoords = d3.mouse($('svg').get(0)); | |
208 | if (relCoords[0] < panBoundary) { | |
209 | panTimer = true; | |
210 | pan(this, 'left'); | |
211 | } else if (relCoords[0] > ($('svg').width() - panBoundary)) { | |
212 | ||
213 | panTimer = true; | |
214 | pan(this, 'right'); | |
215 | } else if (relCoords[1] < panBoundary) { | |
216 | panTimer = true; | |
217 | pan(this, 'up'); | |
218 | } else if (relCoords[1] > ($('svg').height() - panBoundary)) { | |
219 | panTimer = true; | |
220 | pan(this, 'down'); | |
221 | } else { | |
222 | try { | |
223 | clearTimeout(panTimer); | |
224 | } catch (e) { | |
225 | ||
226 | } | |
227 | } | |
228 | ||
229 | d.x0 += d3.event.dy; | |
230 | d.y0 += d3.event.dx; | |
231 | var node = d3.select(this); | |
232 | node.attr("transform", "translate(" + d.x0 + "," + d.y0 + ")"); | |
233 | updateTempConnector(); | |
234 | }).on("dragend", function (d) { | |
235 | if (d == root) { | |
236 | return; | |
237 | } | |
238 | domNode = this; | |
239 | if (selectedNode) { | |
240 | // now remove the element from the parent, and insert it into the new elements children | |
241 | var index = draggingNode.parent.children.indexOf(draggingNode); | |
242 | if (index > -1) { | |
243 | draggingNode.parent.children.splice(index, 1); | |
244 | } | |
245 | if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') { | |
246 | if (typeof selectedNode.children !== 'undefined') { | |
247 | selectedNode.children.push(draggingNode); | |
248 | } else { | |
249 | selectedNode._children.push(draggingNode); | |
250 | } | |
251 | } else { | |
252 | selectedNode.children = []; | |
253 | selectedNode.children.push(draggingNode); | |
254 | } | |
255 | // Make sure that the node being added to is expanded so user can see added node is correctly moved | |
256 | expand(selectedNode); | |
257 | sortTree(); | |
258 | endDrag(); | |
259 | } else { | |
260 | endDrag(); | |
261 | } | |
262 | }); | |
263 | ||
264 | function endDrag() { | |
265 | selectedNode = null; | |
266 | d3.selectAll('.ghostCircle').attr('class', 'ghostCircle'); | |
267 | d3.select(domNode).attr('class', 'node'); | |
268 | // now restore the mouseover event or we won't be able to drag a 2nd time | |
269 | d3.select(domNode).select('.ghostCircle').attr('pointer-events', ''); | |
270 | updateTempConnector(); | |
271 | if (draggingNode !== null) { | |
272 | update(root); | |
273 | centerNode(draggingNode); | |
274 | draggingNode = null; | |
275 | } | |
276 | } | |
277 | ||
278 | // Helper functions for collapsing and expanding nodes. | |
279 | ||
280 | function collapse(d) { | |
281 | if (d.children) { | |
282 | d._children = d.children; | |
283 | d._children.forEach(collapse); | |
284 | d.children = null; | |
285 | } | |
286 | } | |
287 | ||
288 | function expand(d) { | |
289 | if (d._children) { | |
290 | d.children = d._children; | |
291 | d.children.forEach(expand); | |
292 | d._children = null; | |
293 | } | |
294 | } | |
295 | ||
296 | var overCircle = function (d) { | |
297 | selectedNode = d; | |
298 | updateTempConnector(); | |
299 | }; | |
300 | var outCircle = function (d) { | |
301 | selectedNode = null; | |
302 | updateTempConnector(); | |
303 | }; | |
304 | ||
305 | // Function to update the temporary connector indicating dragging affiliation | |
306 | var updateTempConnector = function () { | |
307 | var data = []; | |
308 | if (draggingNode !== null && selectedNode !== null) { | |
309 | // have to flip the source coordinates since we did this for the existing connectors on the original tree | |
310 | data = [{ | |
311 | source: { | |
312 | x: selectedNode.y0, | |
313 | y: selectedNode.x0 | |
314 | }, | |
315 | target: { | |
316 | x: draggingNode.y0, | |
317 | y: draggingNode.x0 | |
318 | } | |
319 | }]; | |
320 | } | |
321 | var link = svgGroup.selectAll(".templink").data(data); | |
322 | ||
323 | link.enter().append("path") | |
324 | .attr("class", "templink") | |
325 | .attr("d", d3.svg.diagonal()) | |
326 | .attr('pointer-events', 'none'); | |
327 | ||
328 | link.attr("d", d3.svg.diagonal()); | |
329 | ||
330 | link.exit().remove(); | |
331 | }; | |
332 | ||
333 | // Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children. | |
334 | ||
335 | function centerNode(source) { | |
336 | scale = zoomListener.scale(); | |
337 | x = -source.y0; | |
338 | y = -source.x0; | |
339 | x = x * scale + viewerWidth / 2; | |
340 | y = y * scale + viewerHeight / 2; | |
341 | d3.select('g').transition() | |
342 | .duration(duration) | |
343 | .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")"); | |
344 | zoomListener.scale(scale); | |
345 | zoomListener.translate([x, y]); | |
346 | } | |
347 | ||
348 | // Toggle children function | |
349 | ||
350 | function toggleChildren(d) { | |
351 | if (d.children) { | |
352 | d._children = d.children; | |
353 | d.children = null; | |
354 | } else if (d._children) { | |
355 | d.children = d._children; | |
356 | d._children = null; | |
357 | } | |
358 | return d; | |
359 | } | |
360 | ||
361 | // Toggle children on click. | |
362 | ||
363 | function click(d) { | |
364 | if (d3.event.defaultPrevented) | |
365 | return; // click suppressed | |
366 | ||
367 | console.log(d3.select(this).select('text').html()); | |
368 | ||
369 | d3.select(this).append('span').attr('class', 'ok3'); | |
370 | ||
371 | document.getElementById('ok1').innerHTML = d3.select(this).select('text').html(); | |
372 | document.getElementById('ok1').style.visibility = 'visible'; | |
373 | d = toggleChildren(d); | |
374 | update(d); | |
375 | centerNode(d); | |
376 | } | |
377 | ||
378 | function update(source) { | |
379 | // Compute the new height, function counts total children of root node and sets tree height accordingly. | |
380 | // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed | |
381 | // This makes the layout more consistent. | |
382 | var levelWidth = [1]; | |
383 | var childCount = function (level, n) { | |
384 | ||
385 | if (n.children && n.children.length > 0) { | |
386 | if (levelWidth.length <= level + 1) | |
387 | levelWidth.push(0); | |
388 | ||
389 | levelWidth[level + 1] += n.children.length; | |
390 | n.children.forEach(function (d) { | |
391 | childCount(level + 1, d); | |
392 | }); | |
393 | } | |
394 | }; | |
395 | childCount(0, root); | |
396 | var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line | |
397 | tree = tree.size([newHeight, viewerWidth]); | |
398 | ||
399 | // Compute the new tree layout. | |
400 | var nodes = tree.nodes(root).reverse(), | |
401 | links = tree.links(nodes); | |
402 | ||
403 | // Set widths between levels based on maxLabelLength. | |
404 | nodes.forEach(function (d) { | |
405 | d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px | |
406 | // alternatively to keep a fixed scale one can set a fixed depth per level | |
407 | // Normalize for fixed-depth by commenting out below line | |
408 | // d.y = (d.depth * 500); //500px per level. | |
409 | }); | |
410 | ||
411 | // Update the nodes… | |
412 | node = svgGroup.selectAll("g.node") | |
413 | .data(nodes, function (d) { | |
414 | return d.id || (d.id = ++i); | |
415 | }); | |
416 | ||
417 | // Enter any new nodes at the parent's previous position. | |
418 | var nodeEnter = node.enter().append("g") | |
419 | .call(dragListener) | |
420 | .attr("class", "node") | |
421 | .attr("transform", function (d) { | |
422 | return "translate(" + source.x0 + "," + source.y0 + ")"; | |
423 | }) | |
424 | .on('click', click); | |
425 | ||
426 | nodeEnter.append("circle") | |
427 | .attr('class', 'nodeCircle') | |
428 | .attr("r", 0) | |
429 | .style("fill", function (d) { | |
430 | return d._children ? "lightsteelblue" : "#fff"; | |
431 | }); | |
432 | ||
433 | nodeEnter.append("text") | |
434 | .attr("x", function (d) { | |
435 | return d.children || d._children ? -10 : 10; | |
436 | }) | |
437 | .attr("dy", ".35em") | |
438 | .attr('class', 'nodeText') | |
439 | .attr("text-anchor", function (d) { | |
440 | return d.children || d._children ? "end" : "start"; | |
441 | }) | |
442 | .text(function (d) { | |
443 | return d.name; | |
444 | }) | |
445 | .style("fill-opacity", 0); | |
446 | ||
447 | // phantom node to give us mouseover in a radius around it | |
448 | nodeEnter.append("circle") | |
449 | .attr('class', 'ghostCircle') | |
450 | .attr("r", 30) | |
451 | .attr("opacity", 0.2) // change this to zero to hide the target area | |
452 | .style("fill", "red") | |
453 | .attr('pointer-events', 'mouseover') | |
454 | .on("mouseover", function (node) { | |
455 | overCircle(node); | |
456 | }) | |
457 | .on("mouseout", function (node) { | |
458 | outCircle(node); | |
459 | }); | |
460 | ||
461 | // Update the text to reflect whether node has children or not. | |
462 | node.select('text') | |
463 | .attr("x", function (d) { | |
464 | return d.children || d._children ? -10 : 10; | |
465 | }) | |
466 | .attr("text-anchor", function (d) { | |
467 | return d.children || d._children ? "end" : "start"; | |
468 | }) | |
469 | .text(function (d) { | |
470 | return d.name; | |
471 | }); | |
472 | ||
473 | // Change the circle fill depending on whether it has children and is collapsed | |
474 | node.select("circle.nodeCircle") | |
475 | .attr("r", 4.5) | |
476 | .style("fill", function (d) { | |
477 | return d._children ? "lightsteelblue" : "#fff"; | |
478 | }); | |
479 | ||
480 | // Transition nodes to their new position. | |
481 | var nodeUpdate = node.transition() | |
482 | .duration(duration) | |
483 | .attr("transform", function (d) { | |
484 | return "translate(" + d.x + "," + d.y + ")"; | |
485 | }); | |
486 | ||
487 | // Fade the text in | |
488 | nodeUpdate.select("text") | |
489 | .style("fill-opacity", 1); | |
490 | ||
491 | // Transition exiting nodes to the parent's new position. | |
492 | var nodeExit = node.exit().transition() | |
493 | .duration(duration) | |
494 | .attr("transform", function (d) { | |
495 | return "translate(" + source.x + "," + source.y + ")"; | |
496 | }) | |
497 | .remove(); | |
498 | ||
499 | nodeExit.select("circle") | |
500 | .attr("r", 0); | |
501 | ||
502 | nodeExit.select("text") | |
503 | .style("fill-opacity", 0); | |
504 | ||
505 | // Update the links… | |
506 | var link = svgGroup.selectAll("path.link") | |
507 | .data(links, function (d) { | |
508 | return d.target.id; | |
509 | }); | |
510 | ||
511 | // Enter any new links at the parent's previous position. | |
512 | link.enter().insert("path", "g") | |
513 | .attr("class", "link") | |
514 | .attr("d", function (d) { | |
515 | var o = { | |
516 | x: source.x0, | |
517 | y: source.y0 | |
518 | }; | |
519 | return diagonal({ | |
520 | source: o, | |
521 | target: o | |
522 | }); | |
523 | }); | |
524 | ||
525 | // Transition links to their new position. | |
526 | link.transition() | |
527 | .duration(duration) | |
528 | .attr("d", diagonal); | |
529 | ||
530 | // Transition exiting nodes to the parent's new position. | |
531 | link.exit().transition() | |
532 | .duration(duration) | |
533 | .attr("d", function (d) { | |
534 | var o = { | |
535 | x: source.x, | |
536 | y: source.y | |
537 | }; | |
538 | return diagonal({ | |
539 | source: o, | |
540 | target: o | |
541 | }); | |
542 | }) | |
543 | .remove(); | |
544 | ||
545 | // Stash the old positions for transition. | |
546 | nodes.forEach(function (d) { | |
547 | d.x0 = d.x; | |
548 | d.y0 = d.y; | |
549 | }); | |
550 | } | |
551 | ||
552 | // Append a group which holds all nodes and which the zoom Listener can act upon. | |
553 | var svgGroup = baseSvg.append("g"); | |
554 | ||
555 | // Define the root | |
556 | root = treeData; | |
557 | root.x0 = viewerHeight / 2; | |
558 | root.y0 = 0; | |
559 | ||
560 | // Layout the tree initially and center on the root node. | |
561 | update(root); | |
562 | centerNode(root); | |
563 | }); |