Create a gist now

Instantly share code, notes, and snippets.

D3.js Drag and Drop, Zoomable, Panning, Collapsible Tree with auto-sizing.

This example pulls together various examples of work with trees in D3.js.

The panning functionality can certainly be improved in my opinion and I would be thrilled to see better solutions contributed.

One can do all manner of housekeeping or server related calls on the drop event to manage a remote tree dataset for example.

Dragging can be performed on any node other than root (flare). Dropping can be done on any node.

Panning can either be done by dragging an empty part of the SVG around or dragging a node towards an edge.

Zooming is performed by either double clicking on an empty part of the SVG or by scrolling the mouse-wheel. To Zoom out hold shift when double-clicking.

Expanding and collapsing of nodes is achieved by clicking on the desired node.

The tree auto-calculates its sizes both horizontally and vertically so it can adapt between many nodes being present in the view to very few whilst making the view managable and pleasing on the eye.

For any help/queries, http://www.robschmuecker.com @robschmuecker or robert.schmuecker at gmail dot com

/*Copyright (c) 2013-2016, Rob Schmuecker
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name Rob Schmuecker may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.*/
// Get JSON data
treeJSON = d3.json("flare.json", function(error, treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
// panning variables
var panSpeed = 200;
var panBoundary = 20; // Within 20px from edges will pan when dragging.
// Misc. variables
var i = 0;
var duration = 750;
var root;
// size of the diagram
var viewerWidth = $(document).width();
var viewerHeight = $(document).height();
var tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// sort the tree according to the node names
function sortTree() {
tree.sort(function(a, b) {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
});
}
// Sort the tree initially incase the JSON isn't in a sorted order.
sortTree();
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
} else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function() {
pan(domNode, speed, direction);
}, 50);
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
function initiateDrag(d, domNode) {
draggingNode = d;
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll("g.node").sort(function(a, b) { // select the parent and sort the path's
if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
else return -1; // a is the hovered element, bring "a" to the front
});
// if nodes has children, remove the links and nodes
if (nodes.length > 1) {
// remove link paths
links = tree.links(nodes);
nodePaths = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
}).remove();
// remove child nodes
nodesExit = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id;
}).filter(function(d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
// remove parent link
parentLink = tree.links(tree.nodes(draggingNode.parent));
svgGroup.selectAll('path.link').filter(function(d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
}).remove();
dragStarted = null;
}
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
// Define the drag listeners for drag/drop behaviour of nodes.
dragListener = d3.behavior.drag()
.on("dragstart", function(d) {
if (d == root) {
return;
}
dragStarted = true;
nodes = tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
// 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');
})
.on("drag", function(d) {
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d, domNode);
}
// get coords of mouseEvent relative to svg container to allow for panning
relCoords = d3.mouse($('svg').get(0));
if (relCoords[0] < panBoundary) {
panTimer = true;
pan(this, 'left');
} else if (relCoords[0] > ($('svg').width() - panBoundary)) {
panTimer = true;
pan(this, 'right');
} else if (relCoords[1] < panBoundary) {
panTimer = true;
pan(this, 'up');
} else if (relCoords[1] > ($('svg').height() - panBoundary)) {
panTimer = true;
pan(this, 'down');
} else {
try {
clearTimeout(panTimer);
} catch (e) {
}
}
d.x0 += d3.event.dy;
d.y0 += d3.event.dx;
var node = d3.select(this);
node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")");
updateTempConnector();
}).on("dragend", function(d) {
if (d == root) {
return;
}
domNode = this;
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
} else {
selectedNode._children.push(draggingNode);
}
} else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
// Make sure that the node being added to is expanded so user can see added node is correctly moved
expand(selectedNode);
sortTree();
endDrag();
} else {
endDrag();
}
});
function endDrag() {
selectedNode = null;
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
d3.select(domNode).attr('class', 'node');
// now restore the mouseover event or we won't be able to drag a 2nd time
d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
updateTempConnector();
if (draggingNode !== null) {
update(root);
centerNode(draggingNode);
draggingNode = null;
}
}
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
var overCircle = function(d) {
selectedNode = d;
updateTempConnector();
};
var outCircle = function(d) {
selectedNode = null;
updateTempConnector();
};
// Function to update the temporary connector indicating dragging affiliation
var updateTempConnector = function() {
var data = [];
if (draggingNode !== null && selectedNode !== null) {
// have to flip the source coordinates since we did this for the existing connectors on the original tree
data = [{
source: {
x: selectedNode.y0,
y: selectedNode.x0
},
target: {
x: draggingNode.y0,
y: draggingNode.x0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal());
link.exit().remove();
};
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
// alternatively to keep a fixed scale one can set a fixed depth per level
// Normalize for fixed-depth by commenting out below line
// d.y = (d.depth * 500); //500px per level.
});
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.call(dragListener)
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 0);
// phantom node to give us mouseover in a radius around it
nodeEnter.append("circle")
.attr('class', 'ghostCircle')
.attr("r", 30)
.attr("opacity", 0.2) // change this to zero to hide the target area
.style("fill", "red")
.attr('pointer-events', 'mouseover')
.on("mouseover", function(node) {
overCircle(node);
})
.on("mouseout", function(node) {
outCircle(node);
});
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
});
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
{
"name": "flare",
"children": [{
"name": "analytics",
"children": [{
"name": "cluster",
"children": [{
"name": "AgglomerativeCluster",
"size": 3938
}, {
"name": "CommunityStructure",
"size": 3812
}, {
"name": "HierarchicalCluster",
"size": 6714
}, {
"name": "MergeEdge",
"size": 743
}]
}, {
"name": "graph",
"children": [{
"name": "BetweennessCentrality",
"size": 3534
}, {
"name": "LinkDistance",
"size": 5731
}, {
"name": "MaxFlowMinCut",
"size": 7840
}, {
"name": "ShortestPaths",
"size": 5914
}, {
"name": "SpanningTree",
"size": 3416
}]
}, {
"name": "optimization",
"children": [{
"name": "AspectRatioBanker",
"size": 7074
}]
}]
}, {
"name": "animate",
"children": [{
"name": "Easing",
"size": 17010
}, {
"name": "FunctionSequence",
"size": 5842
}, {
"name": "interpolate",
"children": [{
"name": "ArrayInterpolator",
"size": 1983
}, {
"name": "ColorInterpolator",
"size": 2047
}, {
"name": "DateInterpolator",
"size": 1375
}, {
"name": "Interpolator",
"size": 8746
}, {
"name": "MatrixInterpolator",
"size": 2202
}, {
"name": "NumberInterpolator",
"size": 1382
}, {
"name": "ObjectInterpolator",
"size": 1629
}, {
"name": "PointInterpolator",
"size": 1675
}, {
"name": "RectangleInterpolator",
"size": 2042
}]
}, {
"name": "ISchedulable",
"size": 1041
}, {
"name": "Parallel",
"size": 5176
}, {
"name": "Pause",
"size": 449
}, {
"name": "Scheduler",
"size": 5593
}, {
"name": "Sequence",
"size": 5534
}, {
"name": "Transition",
"size": 9201
}, {
"name": "Transitioner",
"size": 19975
}, {
"name": "TransitionEvent",
"size": 1116
}, {
"name": "Tween",
"size": 6006
}]
}, {
"name": "data",
"children": [{
"name": "converters",
"children": [{
"name": "Converters",
"size": 721
}, {
"name": "DelimitedTextConverter",
"size": 4294
}, {
"name": "GraphMLConverter",
"size": 9800
}, {
"name": "IDataConverter",
"size": 1314
}, {
"name": "JSONConverter",
"size": 2220
}]
}, {
"name": "DataField",
"size": 1759
}, {
"name": "DataSchema",
"size": 2165
}, {
"name": "DataSet",
"size": 586
}, {
"name": "DataSource",
"size": 3331
}, {
"name": "DataTable",
"size": 772
}, {
"name": "DataUtil",
"size": 3322
}]
}, {
"name": "display",
"children": [{
"name": "DirtySprite",
"size": 8833
}, {
"name": "LineSprite",
"size": 1732
}, {
"name": "RectSprite",
"size": 3623
}, {
"name": "TextSprite",
"size": 10066
}]
}, {
"name": "flex",
"children": [{
"name": "FlareVis",
"size": 4116
}]
}, {
"name": "physics",
"children": [{
"name": "DragForce",
"size": 1082
}, {
"name": "GravityForce",
"size": 1336
}, {
"name": "IForce",
"size": 319
}, {
"name": "NBodyForce",
"size": 10498
}, {
"name": "Particle",
"size": 2822
}, {
"name": "Simulation",
"size": 9983
}, {
"name": "Spring",
"size": 2213
}, {
"name": "SpringForce",
"size": 1681
}]
}, {
"name": "query",
"children": [{
"name": "AggregateExpression",
"size": 1616
}, {
"name": "And",
"size": 1027
}, {
"name": "Arithmetic",
"size": 3891
}, {
"name": "Average",
"size": 891
}, {
"name": "BinaryExpression",
"size": 2893
}, {
"name": "Comparison",
"size": 5103
}, {
"name": "CompositeExpression",
"size": 3677
}, {
"name": "Count",
"size": 781
}, {
"name": "DateUtil",
"size": 4141
}, {
"name": "Distinct",
"size": 933
}, {
"name": "Expression",
"size": 5130
}, {
"name": "ExpressionIterator",
"size": 3617
}, {
"name": "Fn",
"size": 3240
}, {
"name": "If",
"size": 2732
}, {
"name": "IsA",
"size": 2039
}, {
"name": "Literal",
"size": 1214
}, {
"name": "Match",
"size": 3748
}, {
"name": "Maximum",
"size": 843
}, {
"name": "methods",
"children": [{
"name": "add",
"size": 593
}, {
"name": "and",
"size": 330
}, {
"name": "average",
"size": 287
}, {
"name": "count",
"size": 277
}, {
"name": "distinct",
"size": 292
}, {
"name": "div",
"size": 595
}, {
"name": "eq",
"size": 594
}, {
"name": "fn",
"size": 460
}, {
"name": "gt",
"size": 603
}, {
"name": "gte",
"size": 625
}, {
"name": "iff",
"size": 748
}, {
"name": "isa",
"size": 461
}, {
"name": "lt",
"size": 597
}, {
"name": "lte",
"size": 619
}, {
"name": "max",
"size": 283
}, {
"name": "min",
"size": 283
}, {
"name": "mod",
"size": 591
}, {
"name": "mul",
"size": 603
}, {
"name": "neq",
"size": 599
}, {
"name": "not",
"size": 386
}, {
"name": "or",
"size": 323
}, {
"name": "orderby",
"size": 307
}, {
"name": "range",
"size": 772
}, {
"name": "select",
"size": 296
}, {
"name": "stddev",
"size": 363
}, {
"name": "sub",
"size": 600
}, {
"name": "sum",
"size": 280
}, {
"name": "update",
"size": 307
}, {
"name": "variance",
"size": 335
}, {
"name": "where",
"size": 299
}, {
"name": "xor",
"size": 354
}, {
"name": "_",
"size": 264
}]
}, {
"name": "Minimum",
"size": 843
}, {
"name": "Not",
"size": 1554
}, {
"name": "Or",
"size": 970
}, {
"name": "Query",
"size": 13896
}, {
"name": "Range",
"size": 1594
}, {
"name": "StringUtil",
"size": 4130
}, {
"name": "Sum",
"size": 791
}, {
"name": "Variable",
"size": 1124
}, {
"name": "Variance",
"size": 1876
}, {
"name": "Xor",
"size": 1101
}]
}, {
"name": "scale",
"children": [{
"name": "IScaleMap",
"size": 2105
}, {
"name": "LinearScale",
"size": 1316
}, {
"name": "LogScale",
"size": 3151
}, {
"name": "OrdinalScale",
"size": 3770
}, {
"name": "QuantileScale",
"size": 2435
}, {
"name": "QuantitativeScale",
"size": 4839
}, {
"name": "RootScale",
"size": 1756
}, {
"name": "Scale",
"size": 4268
}, {
"name": "ScaleType",
"size": 1821
}, {
"name": "TimeScale",
"size": 5833
}]
}, {
"name": "util",
"children": [{
"name": "Arrays",
"size": 8258
}, {
"name": "Colors",
"size": 10001
}, {
"name": "Dates",
"size": 8217
}, {
"name": "Displays",
"size": 12555
}, {
"name": "Filter",
"size": 2324
}, {
"name": "Geometry",
"size": 10993
}, {
"name": "heap",
"children": [{
"name": "FibonacciHeap",
"size": 9354
}, {
"name": "HeapNode",
"size": 1233
}]
}, {
"name": "IEvaluable",
"size": 335
}, {
"name": "IPredicate",
"size": 383
}, {
"name": "IValueProxy",
"size": 874
}, {
"name": "math",
"children": [{
"name": "DenseMatrix",
"size": 3165
}, {
"name": "IMatrix",
"size": 2815
}, {
"name": "SparseMatrix",
"size": 3366
}]
}, {
"name": "Maths",
"size": 17705
}, {
"name": "Orientation",
"size": 1486
}, {
"name": "palette",
"children": [{
"name": "ColorPalette",
"size": 6367
}, {
"name": "Palette",
"size": 1229
}, {
"name": "ShapePalette",
"size": 2059
}, {
"name": "SizePalette",
"size": 2291
}]
}, {
"name": "Property",
"size": 5559
}, {
"name": "Shapes",
"size": 19118
}, {
"name": "Sort",
"size": 6887
}, {
"name": "Stats",
"size": 6557
}, {
"name": "Strings",
"size": 22026
}]
}, {
"name": "vis",
"children": [{
"name": "axis",
"children": [{
"name": "Axes",
"size": 1302
}, {
"name": "Axis",
"size": 24593
}, {
"name": "AxisGridLine",
"size": 652
}, {
"name": "AxisLabel",
"size": 636
}, {
"name": "CartesianAxes",
"size": 6703
}]
}, {
"name": "controls",
"children": [{
"name": "AnchorControl",
"size": 2138
}, {
"name": "ClickControl",
"size": 3824
}, {
"name": "Control",
"size": 1353
}, {
"name": "ControlList",
"size": 4665
}, {
"name": "DragControl",
"size": 2649
}, {
"name": "ExpandControl",
"size": 2832
}, {
"name": "HoverControl",
"size": 4896
}, {
"name": "IControl",
"size": 763
}, {
"name": "PanZoomControl",
"size": 5222
}, {
"name": "SelectionControl",
"size": 7862
}, {
"name": "TooltipControl",
"size": 8435
}]
}, {
"name": "data",
"children": [{
"name": "Data",
"size": 20544
}, {
"name": "DataList",
"size": 19788
}, {
"name": "DataSprite",
"size": 10349
}, {
"name": "EdgeSprite",
"size": 3301
}, {
"name": "NodeSprite",
"size": 19382
}, {
"name": "render",
"children": [{
"name": "ArrowType",
"size": 698
}, {
"name": "EdgeRenderer",
"size": 5569
}, {
"name": "IRenderer",
"size": 353
}, {
"name": "ShapeRenderer",
"size": 2247
}]
}, {
"name": "ScaleBinding",
"size": 11275
}, {
"name": "Tree",
"size": 7147
}, {
"name": "TreeBuilder",
"size": 9930
}]
}, {
"name": "events",
"children": [{
"name": "DataEvent",
"size": 2313
}, {
"name": "SelectionEvent",
"size": 1880
}, {
"name": "TooltipEvent",
"size": 1701
}, {
"name": "VisualizationEvent",
"size": 1117
}]
}, {
"name": "legend",
"children": [{
"name": "Legend",
"size": 20859
}, {
"name": "LegendItem",
"size": 4614
}, {
"name": "LegendRange",
"size": 10530
}]
}, {
"name": "operator",
"children": [{
"name": "distortion",
"children": [{
"name": "BifocalDistortion",
"size": 4461
}, {
"name": "Distortion",
"size": 6314
}, {
"name": "FisheyeDistortion",
"size": 3444
}]
}, {
"name": "encoder",
"children": [{
"name": "ColorEncoder",
"size": 3179
}, {
"name": "Encoder",
"size": 4060
}, {
"name": "PropertyEncoder",
"size": 4138
}, {
"name": "ShapeEncoder",
"size": 1690
}, {
"name": "SizeEncoder",
"size": 1830
}]
}, {
"name": "filter",
"children": [{
"name": "FisheyeTreeFilter",
"size": 5219
}, {
"name": "GraphDistanceFilter",
"size": 3165
}, {
"name": "VisibilityFilter",
"size": 3509
}]
}, {
"name": "IOperator",
"size": 1286
}, {
"name": "label",
"children": [{
"name": "Labeler",
"size": 9956
}, {
"name": "RadialLabeler",
"size": 3899
}, {
"name": "StackedAreaLabeler",
"size": 3202
}]
}, {
"name": "layout",
"children": [{
"name": "AxisLayout",
"size": 6725
}, {
"name": "BundledEdgeRouter",
"size": 3727
}, {
"name": "CircleLayout",
"size": 9317
}, {
"name": "CirclePackingLayout",
"size": 12003
}, {
"name": "DendrogramLayout",
"size": 4853
}, {
"name": "ForceDirectedLayout",
"size": 8411
}, {
"name": "IcicleTreeLayout",
"size": 4864
}, {
"name": "IndentedTreeLayout",
"size": 3174
}, {
"name": "Layout",
"size": 7881
}, {
"name": "NodeLinkTreeLayout",
"size": 12870
}, {
"name": "PieLayout",
"size": 2728
}, {
"name": "RadialTreeLayout",
"size": 12348
}, {
"name": "RandomLayout",
"size": 870
}, {
"name": "StackedAreaLayout",
"size": 9121
}, {
"name": "TreeMapLayout",
"size": 9191
}]
}, {
"name": "Operator",
"size": 2490
}, {
"name": "OperatorList",
"size": 5248
}, {
"name": "OperatorSequence",
"size": 4190
}, {
"name": "OperatorSwitch",
"size": 2581
}, {
"name": "SortOperator",
"size": 2023
}]
}, {
"name": "Visualization",
"size": 16540
}]
}]
}
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
.node {
cursor: pointer;
}
.overlay{
background-color:#EEE;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font-size:10px;
font-family:sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.templink {
fill: none;
stroke: red;
stroke-width: 3px;
}
.ghostCircle.show{
display:block;
}
.ghostCircle, .activeDrag .ghostCircle{
display: none;
}
</style>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="dndTree.js"></script>
<body>
<div id="tree-container"></div>
</body>
</html>
@dreamfalcon

Hi,
Great code, I'm starting with D3 and this helped building a tree.
Small question, is possible to change the tree orientation without have to alter all x, y?

My testing code, http://jsfiddle.net/dreamfalcon/2EuN5/
Thx

@ljbrown238

Could not possibly thank you enough for this excellent sample.
MANY THANKS!!!!!!
ljb

@udaykadaboina

Thanks Rob! I was wondering if I can reduce the link length. I tried looking into the code but wasn't sure which variable was used to set its size.

Thanks in advance.

@xypaul
xypaul commented Feb 19, 2014

@udaykadaboina there is a variable at the top called - maxLabelLength, which changes the spacing between nodes horizontally.

@lguerard

Hi.

  1. Just to know, where are the data coming from ? Probably a database but I can't find where there's a link to it ...
  2. Is there a way to make something similar but only showing the user's choices. Like he choses a node, then link another to that one and showing results depending on theses choices ? It would be a drag and drop that could implement a SQL query ...

I'm not sure I was really clear so please contact me concerning any questions.
Thanks ! :)

@sahinyanlik

Amazing.

@robschmuecker
Owner

@ReddPool,
Apologies, I only just realised that I wasn't getting notified of the comments! The data is coming from the JSON file however it can come basically from any static or dynamic source.
In answer to your 2nd question, yes I am certain it is all possible. However it would be better if you could explain a little more.

@aymen-mouelhi

Really Great tutorial !
Do you know if it is possible to order elements of the tree at the same level? I mean, I want to drag and drop functionality in a node so that I can re-order the elements in the same node.

@robschmuecker
Owner

I'm sure that this would be possible. I would suggest you do an x and y position check on the dragged node (compared to other child nodes in the same parent) and then depending on where you dropped it it would insert itself at the new location of the array/json tree. However this would be an edge case whereby when dragging a child to a totally new parent you wouldn't have such ability unless you are in the drop-zone so-to-speak and since the drop-zone is currently quite small you would perhaps have to drop it and then re-order in a separate action.

@dvolvox
dvolvox commented Aug 5, 2014

Is there any way to convert any json source to d3 json?

@JariVHKinnunen

I'm a newbie, just great, thanks for the tutorial. Do you see a way to reverse the tree to JSON? If you drag'n'drop and rearrange the tree and you want to save that configuration?

@pozitron57

Thank you for this amazing work! It is absolutely fantastic!

Is it possible to choose the expand level just after the page load? I have quite a big tree and I don't want every node to be open from the beginning, because it looks too messy.
Or maybe I can control each node's status on the page load? It would be excellent.

One more idea I'am trying to implement is the following: 1st click on collapsed element shows only child nodes which have their children, 2nd click show all childs, 3rd click collapse again.

@solutions018

Hi,
how do i make a call of ajax to update database after drag and drop...

@gigster99

TY for good example. Would like to open with fewer nodes. How to introduce this mbostock code (https://github.com/mbostock/d3/blob/gh-pages/talk/20111018/tree.html#L69-74) to open with fewer nodes? Similar to @pozitron57 question.

@sarfrajimpinge

Hi,

On Drag/Drop when user will drop the node on other node , I want to confirm something from him via pop-up or confirm box and based on that drop will be done . How can I do that ?

I have tried to call a bootbox dialog in dragend on("dragend", function(d) But that doesn't work for me. Is it possible or not to do what I want ?

@CrandellWS

Impressive. Recently answered a question about adding scrollbars to the tree where my answer was rudimentary, in suggesting to apply overflow:scoll to a containing div.
@sarfrajimpinge see https://gist.github.com/robschmuecker/7880033#gistcomment-1249798 which you may find helpful and let's move these comments to @ http://stackoverflow.com/q/32839957/1815624

@ivandgreat

Thanks for sharing it to us Rob.

It is possible to update the "flare.json" everytime the node was drag and drop to a different node..

Thanks again.

@dwyanelin

Hi,
May I ask a question about the path?
I change the height(x to x*1.6) of the node which d.depth for example is 5, then, the path will be wrong.
The wrong situation is: It is not smooth to connect to the new position, but is to connect the original position ahead, then make a turn to the new position. That seems weird. and I don't know how to fix it.
So, can anyone help me. thanks very much!!!

@HoplaGeiss

Hi,

thanks for this awesome example. I am trying to slightly modify your code to be able to drop a node between two children.
At the moment with your example when you drag and drop, the node is systematically pushed to the end of the parent's children array. I would like to be able to drop it wherever I want. May I ask you on an advise on how you would do this ?

The only solution I see is to have a shape above each node ( a point or a rectangle ) and if the user drops it on this zone, the node is added at this position, but aesthetically it is not so nice. i wonder if it would be possible to highlight the two closer node when the element is being dragged and if the use drops it than it will be added to the corresponding position. However I am not sure how and if this solution would work.

@RobertBakaric

This is very nice!!! Are there any special citation requests or just a name and link to this page?

Great job!!!

@rakeshadak137

Great job!
Is it possible to expand a graph at a perticular level rather than expanding it all at initial page load?

@lawrenceong001

tks for the example. when I collapsed the nodes, I noticed that the highlighted line to show multi-parent seems to stay on the screen. is it possible to collapse this too?

tks

@ilgiz-badamshin

pan(domNode, speed, direction);
should be
pan(domNode, direction);

@belhadj-haythem

Hello ,
when i download this zip file, the index.html page is not loading, there is a white page when click ,
Is there something else required ?

@beardedlinuxgeek

There is a small bug here. Line 241 checks if selectedNode.children or selectedNode._children is undefined. But, if you have previous collapsed and then expanded the selected node, the collapse/expand code sets children/_children to null. So it's not undefined, but it's also not an array. Line 241 should look like:

if ( (typeof selectedNode.children !== 'undefined' && selectedNode.children !== null) || (typeof selectedNode._children !== 'undefined' && selectedNode._children !== null) ) {

@SBJun
SBJun commented Nov 29, 2016

@ivandgreat - Did you end up figuring out how to update the flare.json file after a modification?

@sushantgupta1206

What is the importance of the "Size" in the json file? I tried to remove it with a different hierarchy and run but my code fails! Can someone please guide me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment