Skip to content

Instantly share code, notes, and snippets.

@robschmuecker
Last active July 22, 2024 12:38
Show Gist options
  • Save robschmuecker/7880033 to your computer and use it in GitHub Desktop.
Save robschmuecker/7880033 to your computer and use it in GitHub Desktop.
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>
@sarfrajimpinge
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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

Great job!!!

@rakeshadak137
Copy link

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

@lawrenceong001
Copy link

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
Copy link

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

@belhadj-haythem
Copy link

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
Copy link

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) ) {

@SeungBJun
Copy link

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

@sushantgupta1206
Copy link

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?

@erjankum
Copy link

Hi! I need this code but without drag and drop. Only zoom and pan. I find the same code but there is a limit in width and heigth. I need that my nodes open in unlimited space. Please, can you help me?

@priyankashet
Copy link

I have try to change the view of the tree from top to bottom but all other functionalities are like drag and drop,delete stopped working. If i click a node automatically it will dragged to other node, can i get the top to bottom view of the tree with all functionalities, can you please help me...

@rsaungc
Copy link

rsaungc commented Feb 16, 2018

Hi, is there a way to limit tree depth? I only want to expand first 2 depth by default. Thanks.

@andymel123
Copy link

Hi Rob!
One thing I just saw:
The function signature of pan is function pan(domNode, direction) but in your recursive call you call it with pan(domNode, speed, direction);

So this should lead to problems as speed will be used as direction in recursive calls

Thanks for this inspiring example!!

@mdickinsonuu
Copy link

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?

I'm having this issue as well? What is the size used for? Looking at the JSON data the size doesn't appear to relate to anything so does it need to remain in the data or can it be removed?

@sudha53633
Copy link

Hey Rob,

As so many using this plugin , im also one. Great Help.

Would you please help me out to change the nodes from circles to boxes(rectangle with tittle) ?

Regards,
Sudhakar G

@Prachi481992
Copy link

Hi Rob,

Thank you so much for this plugin.

I am currently trying to implement a similar solution in my application but for me, my data for creating the tree comes from a database in the backend and once the editing is done I would like to update the changes done in the database.

I needed a bit of help from you to achieve this as I am quite a newbie in d3js. My questions are as follows:

  1. Is it possible to list out all the changes (only drag and drop) in the form of an object which can then be sent to backend?

  2. If not, can you suggest a way to implement this solution?

Could you please help me out?

@sameeparora
Copy link

@robschmuecker Possible to export the updated "flare.json" after a user edits the nodes? @SeungBJun @ivandgreat

@sayakdutta
Copy link

can someone please tell me how to create the tree.whwn I run index.html on browser,all I get is a blank page

@connect2pritam
Copy link

can someone please tell me how to create the tree.whwn I run index.html on browser,all I get is a blank page

Open debugger in Chrome and see the error or open the index.html file on the "Microsoft Edge" browser and see the graph.

@arezki12
Copy link

arezki12 commented Jan 7, 2020

hello, I can have the code update please ?? because with that it gives me a blank page

@autonomobil
Copy link

The blank page seems to be a problem with CVE-2019-11730: "Same-origin policy treats all files in a directory as having the same-origin". So the flare.json is can not be accessed.

Also interesting:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSRequestNotHttp

To enable in Firefox: about:config set privacy.file_unique_origin to false

See: https://support.mozilla.org/de/questions/1264400

@rahul-996
Copy link

rahul-996 commented Mar 19, 2020

Hi,
How can I create a tree if one node has the same multi-parent and multi-children nodes?

@farokhpour
Copy link

can i use icon instead of circles in this libary?

@HoplaGeiss
Copy link

It would be great to see the same with version 4 of d3.js! Adapting the drag & drop to v4 is especially hard.

@john012343210
Copy link

Any way after editing the graph, export the whole graph information in some format like json?

@adnredwan77
Copy link

Hi all, I need your help if you can,
How can I make the tree work from bottom to up

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