I would like to use the zoom listener (scale) in combination with translate to fit all nodes, texts and paths nicely into the viewport/d3 container.
I'm using the tree layout in combination with the force layout.
Is there a way to get the outer limits of all objects (sort of non existing rectangle around the objects with height/width and X+y position of the rectangle)? This would then allow me to use translate/scale to fit everything nicely.
I tried a few ways when trying to solve this. I tried getBoundingClientRect() and getBBox() through D3 but neither gave the correct coordinates.
So what I did was to loop through each circle and go into it's data. I had some logic to get the lowest left value, the highest right value, lowest top value and highest bottom value.
To do this I just used this logic :
var thisNodeData = allNodes[i].__data__;
var thisLeft = thisNodeData.x;
var thisRight = thisNodeData.x;
var thisTop = thisNodeData.y;
var thisBottom = thisNodeData.y;
if (i == 0) { //set it on first one
left = thisLeft;
right = thisRight;
top = thisTop;
bottom = thisBottom;
};
//overwrite values where needed
if (left > thisLeft) {
left = thisLeft
}
if (right < thisRight) {
right = thisRight
}
if (top > thisTop) {
top = thisTop
}
if (bottom < thisBottom) {
bottom = thisBottom
}
Now these left,right,bottom and top values will be the values of your rect. However, this way gets the centre points of each circle so to make up for this I made up a radius value but this can be found programatically :
So I used these to create a rectangle like so :
var circleRadius = 20;
var rectAttr = [{
x: top - circleRadius / 2,
y: left - circleRadius / 2,
width: bottom - top + circleRadius,
height: right - left + circleRadius,
}]
I must say, I messed about this these values. You would think that x
would be left, y would be top but this didn't get the correct outcome.
If anyone can tell me what I have done wrong here, will be
appreciated. But for now this works fine, it just doesn't seem like
the correct logic.
Now use rectAttr to create the boundary rectangle :
svg.selectAll('rectangle')
.data(rectAttr)
.enter() //.append('svg')
.append('rect')
.attr('x', function(d) {
return d.x;
})
.attr('y', function(d) {
return d.y;
})
.attr('width', function(d) {
return d.width;
})
.attr('height', function(d) {
return d.height;
})
.style('stroke', 'red').style('fill', 'none')
I have added this function to be called on click of a node so I can show you it working.
Updated fiddle : http://jsfiddle.net/thatOneGuy/JnNwu/916/
EDIT:
Now to zoom according to size.
What you have to do here is get the difference in scale of the new rect compared to the old.
First I got the biggest value from the rectangles width and height to give the correct scale like so :
var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale
var scale = Math.max(widthScale,heightScale);
And then used this scale in the translate. To zoom into the rectangle only you have to get the center point and adjust it accordingly like so :
var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;
return 'translate(' + transX + ','+ transY + ')scale('+scale+')' ;
I also added this line :
d3.select('#invisRect').remove();
Before creating a new one otherwise I would be getting the wrong rectangle when getting the translate dimensions above.
Final working fiddle : http://jsfiddle.net/thatOneGuy/JnNwu/919/
var json = {
"name": "Base",
"children": [{
"name": "Type A",
"children": [{
"name": "Section 1",
"children": [{
"name": "Child 1"
}, {
"name": "Child 2"
}, {
"name": "Child 3"
}]
}, {
"name": "Section 2",
"children": [{
"name": "Child 1"
}, {
"name": "Child 2"
}, {
"name": "Child 3"
}]
}]
}, {
"name": "Type B",
"children": [{
"name": "Section 1",
"children": [{
"name": "Child 1"
}, {
"name": "Child 2"
}, {
"name": "Child 3"
}]
}, {
"name": "Section 2",
"children": [{
"name": "Child 1"
}, {
"name": "Child 2"
}, {
"name": "Child 3"
}]
}]
}]
};
var width = 700;
var height = 650;
var maxLabel = 150;
var duration = 500;
var radius = 5;
var i = 0;
var root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + maxLabel + ",0)");
root = json;
root.x0 = height / 2;
root.y0 = 0;
root.children.forEach(collapse);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
var links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * maxLabel;
});
// Update the nodes…
var node = svg.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")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on("click", click);
nodeEnter.append("circle").attr('class', 'circleNode')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "white";
});
nodeEnter.append("text")
.attr("x", function(d) {
var spacing = computeRadius(d) + 5;
return d.children || d._children ? -spacing : spacing;
})
.attr("dy", "3")
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 0);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", function(d) {
return computeRadius(d);
})
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
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 = svg.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;
});
}
function computeRadius(d) {
if (d.children || d._children) return radius + (radius * nbEndNodes(d) / 10);
else return radius;
}
function nbEndNodes(n) {
nb = 0;
if (n.children) {
n.children.forEach(function(c) {
nb += nbEndNodes(c);
});
} else if (n._children) {
n._children.forEach(function(c) {
nb += nbEndNodes(c);
});
} else nb++;
return nb;
}
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
getBoundingBox();
}
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
update(root);
getBoundingBox();
function getBoundingBox() {
var left = 0,
right = 0,
top = 0,
bottom = 0;
var allNodes = document.getElementsByTagName('circle');
for (var i = 0; i < allNodes.length; i++) {
var thisNodeData = allNodes[i].__data__;
var thisLeft = thisNodeData.x;
var thisRight = thisNodeData.x;
var thisTop = thisNodeData.y;
var thisBottom = thisNodeData.y;
if (i == 0) { //set it on first one
left = thisLeft;
right = thisRight;
top = thisTop;
bottom = thisBottom;
};
//overwrite values where needed
if (left > thisLeft) {
left = thisLeft
}
if (right < thisRight) {
right = thisRight
}
if (top > thisTop) {
top = thisTop
}
if (bottom < thisBottom) {
bottom = thisBottom
}
}
var circleRadius = 20;
var rectAttr = [{
x: top - circleRadius / 2,
y: left - circleRadius / 2,
width: bottom - top + circleRadius,
height: right - left + circleRadius,
}]
d3.select('#invisRect').remove();
svg.selectAll('rectangle')
.data(rectAttr)
.enter() //.append('svg')
.append('rect').attr('id','invisRect')
.attr('x', function(d) {
return d.x;
})
.attr('y', function(d) {
return d.y;
})
.attr('width', function(d) {
return d.width;
})
.attr('height', function(d) {
return d.height;
})
.style('stroke', 'red').style('fill', 'none')
svg.attr('transform',function(d){
var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale
var scale = Math.max(widthScale,heightScale);
var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;
return 'translate(' + transX + ','+ transY + ')scale('+scale+')' ;
})
}
html {
font: 10px sans-serif;
}
svg {
border: 1px solid silver;
}
.node {
cursor: pointer;
}
.node circle {
stroke: steelblue;
stroke-width: 1.5px;
}
.link {
fill: none;
stroke: lightgray;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id=tree></div>
Related
I'm learning how to use D3. After fixing up the code from this example (https://jsfiddle.net/tgsen1bc/
) to run the newest version of D3, I managed to get the links to show but not the nodes or their labels.
EDIT: I got the nodes to appear :) My problem now is that some child elements of the <g>'s do not appear so the labels for the nodes don't appear. For example, in Chrome Dev Tools, the child elements of <foreignObject> (a child of <g>) do not appear on the window. I tried making <foreignObject> extremely large but the child elements still do not appear. I also tried replacing the <foreignObject> with a <div>, but the <div> ended up disappearing as well. I checked the parent css properties and it doesn't seem like anything should be blocking the child elements from appearing. The only child element that appears are the circle <svg> and <foreignObject> The code section for the label is:
// Add labels for the nodes
nodeEnter.append('foreignObject')
.attr("y", -30)
.attr("x", -5)
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.attr('width', 100)
.attr('height', 50)
.append('div') // doesn't show up on webpage
.attr("class", function (d) {
return "node-label" + " node-" + d.data.type
})
.classed("disabled", function (d) {
return d.enable !== undefined && !d.enable;
})
.append("span") // doesn't show up on webpage
.attr("class", "node-text")
.text(function (d) { // correct label in chrome dev tools
return d.data.name; // but does not show up on webpage
});
var treedata = {
"name": "PublisherNameLongName",
"id": "id1",
"type": "type0",
"addable": false,
"editable": false,
"removable": false,
"enableble": false,
"children": [{
"name": "Landing A",
"id": "id2",
"type": "type1",
"addable": true,
"editable": true,
"removable": true,
"enablable": true,
"enable": false,
"children": null
}]
}
// Set the dimensions and margins of the diagram
var margin = { top: 20, right: 20, bottom: 20, left: 20 },
width = 800 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom,
i = 0,
x = d3.scaleLinear().domain([0, width]).range([0, width]),
y = d3.scaleLinear().domain([0, height]).range([0, height]),
root;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var vis = d3.select("#root")
.append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
;
vis.append("rect")
.attr("class", "overlay")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("opacity", 0)
var tree = d3.tree().size([height, width]);
// Draws curved diagonal path from parent to child nodes
function diagonal(s, d) {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
}
root = d3.hierarchy(treedata, function (d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
// open or collaspe children of selected node
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
};
// Initialize the display to show a few nodes
// root.children.forEach(toggleAll);
update(root);
function update(source) {
// how long animations last
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var treeObj = tree(root)
var nodes = treeObj.descendants(),
links = treeObj.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 180;
});
/********************* NODES SECTION *********************/
// Update the nodes...
var node = vis.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")
.attr("class", "node")
.attr("id", function (d) {
return "node-" + d.id;
})
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on("click", function (d) {
toggle(d);
update(d);
});
// Add Circle for the nodes
nodeEnter.append("circle")
.attr("class", "circle-for-nodes")
.attr("r", 1e-6)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
// Add labels for the nodes
nodeEnter.append('foreignObject')
.attr("y", -30)
.attr("x", -5)
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.attr('width', 100)
.attr('height', 50)
.append('div')
.attr("class", function (d) {
return "node-label" + " node-" + d.data.type
})
.classed("disabled", function (d) {
return d.enable !== undefined && !d.enable;
})
.append("span")
.attr("class", "node-text")
.text(function (d) {
return d.data.name;
});
// Enable node button if enablable
nodeEnter.filter(function (d) {
return d.enablable;
})
.append("input", ".")
.attr("type", "checkbox")
.property("checked", function (d) {
return d.enable;
})
.on("change", toggleEnable, true)
.on("click", stopPropogation, true);
// Edit node button if editable
nodeEnter.filter(function (d) {
return d.editable;
})
.append("a")
.attr("class", "node-edit")
.on("click", onEditNode, true)
.append("i")
.attr("class", "fa fa-pencil");
// Add node button if addable
nodeEnter.filter(function (d) {
return d.addable;
})
.append("a")
.attr("class", "node-add")
.on("click", onAddNode, true)
.append("i")
.attr("class", "fa fa-plus");
// Remove node button if removable
nodeEnter.filter(function (d) {
return d.removable;
})
.append("a")
.attr("class", "node-remove")
.on("click", onRemoveNode, true)
.append("i")
.attr("class", "fa fa-times");
// UPDATE - merges all transitions together?
// var nodeUpdate = node;
var nodeUpdate = nodeEnter.merge(node);
// Transition nodes to their new position.
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Display node
nodeUpdate.select("circle.circle-for-nodes")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
// Display text
nodeUpdate.select(".node-text")
.style("fill-opacity", function (d) {
console.log(d)
return 1;
})
// Transition exiting ndoes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select("circle")
.attr("r", 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select("text")
.style("fill-opacity", 1e-6);
/********************* LINKS SECTION *********************/
// Update the links...
var link = vis.selectAll("path.link")
.data(links, function (d) { return d.id; });
// Enter any new links at the parent's previous position
var linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal(o, o);
})
// UPDATE - merges all transitions together?
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position.
linkUpdate.transition()
.duration(duration)
.attr("d", function (d) {
return diagonal(d, d.parent)
});
// Remove exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr("d", function (d) {
var o = { x: source.x, y: source.y };
return diagonal(o, o);
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// End of function update()
}
// Toggle children
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
// zoom in / out
function zoom(d) {
//vis.attr("transform", "transl3ate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
var nodes = vis.selectAll("g.node");
nodes.attr("transform", transform);
// Update the links...
var link = vis.selectAll("path.link");
link.attr("d", translate);
// Enter any new links at hte parent's previous position
//link.attr("d", function(d) {
// var o = {x: d.x0, y: d.y0};
// return diagonal({source: o, target: o});
// });
}
function transform(d) {
return "translate(" + x(d.y) + "," + y(d.x) + ")";
}
function translate(d) {
var sourceX = x(d.target.parent.y);
var sourceY = y(d.target.parent.x);
var targetX = x(d.target.y);
var targetY = (sourceX + targetX) / 2;
var linkTargetY = y(d.target.x0);
var result = "M" + sourceX + "," + sourceY + " C" + targetX + "," + sourceY + " " + targetY + "," + y(d.target.x0) + " " + targetX + "," + linkTargetY + "";
return result;
}
function onEditNode(d) {
var length = 9;
var id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, length);
addChildNode(d.id, {
"name": "new child node",
"id": id,
"type": "type2"
});
stopPropogation();
}
function onAddNode(d) {
var length = 9;
var id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, length);
addChildNode(d.id, {
"name": "new child node",
"id": id,
"type": "type2"
});
stopPropogation();
}
function onRemoveNode(d) {
var index = d.parent.children.indexOf(d);
if (index > -1) {
d.parent.children.splice(index, 1);
}
update(d.parent);
stopPropogation();
}
function addChildNode(parentId, newNode) {
var node = d3.select('#' + 'node-' + parentId);
var nodeData = node.datum();
if (nodeData.children === undefined && nodeData._children === undefined) {
nodeData.children = [newNode];
} else if (nodeData._children != null) {
nodeData._children.push(newNode);
toggle(nodeData);
} else if (nodeData.children != null) {
nodeData.children.push(newNode);
}
update(node);
stopPropogation();
}
function toggleEnable(d) {
d.enable = !d.enable;
var node = d3.select('#' + 'node-' + d.id + " .node-label")
.classed("disabled", !d.enable);
stopPropogation();
}
function stopPropogation() {
d3.event.stopPropagation();
}
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
.node circle {
cursor: pointer;
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node-label {
font-size: 12px;
padding: 3px 5px;
display: inline-block;
word-wrap: break-word;
max-width: 160px;
background: #d0dee7;
border-radius: 5px;
}
.node a:hover {
cursor: pointer;
}
.node a {
font-size: 10px;
margin-left: 5px
}
a.node-remove {
color: red;
}
input+.node-text {
margin-left: 5px;
}
.node-label.node-type1 {
background: coral;
}
.node-label.node-type2 {
background: lightblue;
}
.node-label.node-type3 {
background: yellow;
}
.node-label.disabled {
background: #e9e9e9;
color: #838383
}
.node text {
font-size: 11px;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
<!DOCTYPE html>
<meta charset="utf-8" />
<script src="https://d3js.org/d3.v5.js"></script>
<body>
<div id="root"></div>
</body>
Found my answer here: HTML element inside SVG not displayed
To summarize what Christopher said, <div></div> is not recognized as a xhtml paragraph because the namespace xhtml is not included in the foreignObject context (a foreignObject might contain anything (xml formated data for example). To specify the input as html, I needed to .append("xhtml:div")
I am working on an application where I want to show my hierarchical data in tree structure. This data keeps updating and I want to update tree as per newly received data. I have implemented it successfully using D3 V3 by merging the new data with new data.
I am now wanting to upgrade to d3 V5. So far I have been able to create the tree but I am unable to handle data update. Whenever I get new data, entire tree is recreated, I see that the enter() and exit() events are triggered each time for each node.
Note: Please ignore if the expand-collapse is not working, I am still working on this.
I have extracted the code created below working snippet to depict my problem. I do not want to recreate tree every time I receive new data, I only want to update the existing nodes if there is any change in the data, otherwise the nodes remain as is.
Where I am going wrong? Can you one suggest please?
// Code goes here
// find elements
var margin = {
top: 20,
right: 90,
bottom: 30,
left: 90
},
width = 660 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
i = 0,
duration = 750,
redius = 10;
var node = {};
var root = {};
var links = {};
var nodes = [];
var links = [];
// handle click and add class
$("#btnLoadData").click(function() {
refreshData();
});
function refreshData() {
var data = {
"name": "Central",
"connectionState": Math.random() > 0.5 ? "connected" : "disconnected",
"parent": null,
"envStatus": {
"cpu": 45.575,
"mem": 55.8,
"disk": 85.5
},
"subState": "",
"children": [{
"name": "UK-STORE1",
"connectionState": Math.random() > 0.5 ? "connected" : "disconnected",
"subState": "",
"envStatus": {
"cpu": 45.650000000000006,
"mem": 55.8,
"disk": 85.5
},
"children": [{
"name": "UK-TILL1",
"connectionState": Math.random() > 0.5 ? "connected" : "disconnected",
"subState": null,
"envStatus": {
"cpu": 46.025000000000006,
"mem": 55.8,
"disk": 85.5,
},
"children": null,
"stateChangedAt": "2020-02-08 20:59:35.226769"
},
{
"name": "UK-TILL2",
"connectionState": Math.random() > 0.5 ? "connected" : "disconnected",
"subState": null,
"envStatus": {
"cpu": 45.775000000000006,
"mem": 56.1,
"disk": 85.5
},
"children": null,
"stateChangedAt": "2020-02-08 20:59:35.226769"
}
],
"stateChangedAt": "2020-02-08 20:59:35.226769"
}]
}
buildRoot(data);
}
function buildRoot(newSource) {
root = d3.hierarchy(newSource, function(d) {
return d.children;
});
root.x = 0;
root.y = width / 2;
root.x0 = 0;
root.y0 = width / 2;
updateTree(root)
}
var svg = d3.select("#tree").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var mainG = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var treeLayout = d3.tree()
.size([width, height]);
//.nodeSize([100, 50]);
// Collapse the node and all it's children
function collapse(d) {
if (d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function updateTree(source) {
var treeData = treeLayout(root);
var newNodes = treeData.descendants();
_.merge(nodes, newNodes)
var newlinks = treeData.descendants().slice(1);
_.merge(links, newlinks)
nodes.forEach(function(d) {
dy = d.depth * 180
});
//links
var linkPaths = mainG.selectAll(".link")
.data(links, function(d) {
return d.id;
});
var linkEnter = linkPaths.enter().append("path")
.attr("class", "link")
.attr('d', function(d) {
var o = {
x: source.x0,
y: source.y0
}
return diagonal(o, o);
});
var linkUpdate = linkEnter.merge(linkPaths);
linkUpdate.transition()
.duration(duration)
.attr('d', function(d) {
return diagonal(d, d.parent)
});
var linkExit = linkPaths.exit()
.transition()
.duration(duration)
.attr('d', function(d) {
var o = {
y: source.y0,
x: source.x0
}
return diagonal(o, o)
})
.remove();
//Nodes
var node = mainG.selectAll('g.node')
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
//update nodes
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.on('click', click);
//Append circle
nodeEnter.append("circle")
.attr("class", "circle")
.attr("r", redius);
//Append circle
nodeEnter.append("text")
.attr("class", "nodeName")
.text(function(d) {
return d.data.name;
});
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodeUpdate.select("circle")
.attr("class", function(d) {
console.log(d.data.connectionState);
return d.data.connectionState == "connected" ? "circle-connected" : "circle-disconnected"
})
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
function diagonal(s, d) {
path = `M ${s.x} ${s.y}
C ${s.x} ${(d.y+ s.y)/2},
${s.x} ${(d.y+ s.y)/2 },
${d.x} ${d.y}`
return path;
}
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
updateTree(d);
}
} // update tree ends
refreshData()
/* Styles go here */
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
color: #fff;
}
button {
background: #0084ff;
border: none;
border-radius: 5px;
padding: 8px 14px;
font-size: 15px;
color: #fff;
}
svg {
background-color: forestgreen;
}
.node {
cursor: pointer;
}
.circle {
fill: #fff;
transition: fill 2s;
r: 10;
}
.circle-connected {
fill: #14A76C;
transition: fill 2s;
}
.circle-disconnected {
fill: #a72314;
transition: fill 2s;
}
.link {
fill: none;
stroke: #ccc;
}
<!DOCTYPE html>
<html>
<head>
<script data-require="lodash.js#4.17.4" data-semver="4.17.4" src="https://cdn.jsdelivr.net/npm/lodash#4.17.4/lodash.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="https://cdn.jsdelivr.net/npm/jquery#3.2.1/dist/jquery.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<div id="banner-message">
<p>D3 V5 Tree</p>
<button id="btnLoadData">Update data</button>
</div>
<div id="tree"></div>
<script src="script.js"></script>
</body>
</html>
.
So I'm trying to create a tree in D3 (Adapted from here) to show a series of nodes that are a specific color according to their value. The problem is that I am getting new data at set intervals that may change these values. I want the tree to update the colors accordingly when it recieves new information. I've tried a number of different methods that result in the entire tree redrawing itself, flying onto the screen, and auto expanding every nodes children. The desired effect that I am looking for is for the color of updated nodes to change, while the tree respects the status of collapsed/uncollapsed nodes that the user doesn't see the whole tree reset itself. Is this possible?
Here's what i have so far:
// Get JSON data
var treeData = {
"name": "rootAlert",
"alert": "true",
"children": [{
"name": "Child1",
"alert": "true",
"children": [{
"name": "Child1-1",
"alert": "false"
}, {
"name": "Child1-2",
"alert": "false"
}, {
"name": "Child1-3",
"alert": "true"
}]
}, {
"name": "Child2",
"alert": "false",
"children": [{
"name": "Child2-1",
"alert": "false"
}, {
"name": "Child2-2",
"alert": "false"
}, {
"name": "Child2-3",
"alert": "false"
}]
}, {
"name": "Child3",
"alert": "false"
}]
}
// 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);
}
}
}
function visit2(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;
});
// 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);
// 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);
// 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]);
}
function leftAlignNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = (x * scale) + 100;
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;
}
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
if (d._children != null) {
var isCollapsed = true
} else {
var isCollapsed = false;
}
d = toggleChildren(d);
update(d);
if (isCollapsed) {
leftAlignNode(d);
} else {
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 * 5)); //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);
// 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 + ")";
});
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { // alert(d.alert);
//console.log(d.name + ' is ' + d.alert)
if (d.alert == 'true') //if alert == true
return "red";
else return d._children ? "green" : "green";
});
// 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.
tree.nodes(root).forEach(function(n) {
toggle(n);
});
update(root);
leftAlignNode(root);
setInterval(function() {
//update the color of each node
}, 2000);
.node {
cursor: pointer;
}
.overlay {
background-color: #EEE;
}
.node circle {
fill: #fff;
stroke: gray;
stroke-width: 1.5px;
}
.node text {
font: 10px 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;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="tree-container"></div>
I've found a great example here
It does basically exactly what I want. However, instead of updating the tree when a selection is made, I want it to update automatically when new data is recieved. I'd also like to see it not in AngularJS. I've attempted to implement the same type of update function from that example, but mine still pos in from the top left, while the example is so smooth!
It may be best to re-render the visualization when your data pipeline is updated.
You'll have to save the state of your visualization, preserving all information that will be needed to re-render the visualization, so that your users are none the wiser.
This question already has answers here:
Where is d3.svg.diagonal()?
(5 answers)
Closed 5 years ago.
I've been trying to migrate from D3.js v3 to version 4. I've reviewed the changelog and updated all functions, but I'm unable to render the path from source to target nodes now that the diagonal function is removed.
I'm using a Parse Tree generated by a Python Script via HTML and d3.js. The Python Script generates an HMTL document, here it is running with D3.js version 3
function drawTree(){
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 1060 - margin.right - margin.left,
height = 600 - margin.top - margin.bottom;
var i = 0,
duration = 750,// animation duration
root;// stores the tree structure in json format
var tree = d3.layout.tree()
.size([height, width]);
var edge_weight = d3.scale.linear()
.domain([0, 100])
.range([0, 100]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
// adding the svg to the html structure
var svg = d3.select("div#viz").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var treeData =
{
"name": "Grandparent",
"size" : 100,
"children": [
{
"name": "Parent A",
"size": 70,
"children": [
{ "name": "Son of A",
"size" : 30,
"children": [
{ "name": "grandson of A",
"size" : 3},
{ "name": "grandson 2 of A",
"size" : 2},
{ "name": "grandson 3 of A",
"size" : 5},
{ "name": "grandaughter of A",
"size" : 20,
"children": [
{ "name": "great-grandson of A",
"size" : 15},
{ "name": "great-grandaughter of A",
"size" : 5}
]
}
],
},
{ "name": "Daughter of A" ,
"size" : 40
}
]
},
{ "name": "Parent B",
"size" : 30 }],
};
edge_weight.domain([0,treeData.size]);
// Assigns parent, children, height, depth
root = treeData;
root.x0 = height / 2;
root.y0 = 0;
root.children.forEach(collapse);
update(root);
d3.select(self.frameElement).style("height", "800px");
/**
* Updates the node.
* cloppases and expands the node bases on the structure of the source
* all 'children' nodes are expanded and '_children' nodes collapsed
* #param {json structure} source
*/
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = svg.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')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'node')
.attr('r', 1e-6)
.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("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", function(d){ console.log(">>>>>>>>", d);return edge_weight(d.size/2);})
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff"; });
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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
// Update the links…
var link = svg.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("stroke-width", function(d){
return 1;
})
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.attr("stroke", function(d){
return "lavender";});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", function(d){
/* calculating the top shift */
var source = {x: d.source.x - edge_weight(calculateLinkSourcePosition(d)), y: d.source.y};
var target = {x: d.target.x, y: d.target.y};
return diagonal({source: source, target: target});
})
.attr("stroke-width", function(d){
return edge_weight(d.target.size);
});
// 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;
});
}
/**
* Calculates the source y-axis position of the link.
* #param {json structure} link
*/
function calculateLinkSourcePosition(link) {
targetID = link.target.id;
var childrenNumber = link.source.children.length;
var widthAbove = 0;
for (var i = 0; i < childrenNumber; i++)
{
if (link.source.children[i].id == targetID)
{
// we are done
widthAbove = widthAbove + link.source.children[i].size/2;
break;
}else {
// keep adding
widthAbove = widthAbove + link.source.children[i].size
}
}
return link.source.size/2 - widthAbove;
}
/*
* Toggle children on click.
* #param {node} d
*/
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
/*
* Collapses the node d and all the children nodes of d
* #param {node} d
*/
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
/*
* Collapses the node in the tree
*/
function collapseAll() {
root.children.forEach(collapse);
update(root);
}
/*
* Expands the node d and all the children nodes of d
* #param {node} d
*/
function expand(d) {
if (d._children) {
d._children = null;
}
if (d.children) {
d.children.forEach(expand);
}
}
/*
* Expands all the nodes in the tree
*/
function expandAll() {
root.children.forEach(expand);
update(root);
}
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
/*stroke: steelblue;*/
opacity: 0.3;
/*stroke-width: 1.5px;*/
}
#levels{
margin-left: 120px;
}
<!DOCTYPE html>
<meta charset="utf-8">
<body onLoad="drawTree()">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.min.js"></script>
<button type="button" onclick="collapseAll()">Collapse All</button>
<button type="button" onclick="expandAll()">Expand All</button>
<div id="viz"></div>
</body>
And here's as far as I got with the v4 migration.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 900 - margin.right - margin.left,
height = 400 - margin.top - margin.bottom;
var i = 0,
duration = 750, // animation duration
root; // stores the tree structure in json format
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
var edge_weight = d3.scaleLinear()
.domain([0, 100])
.range([0, 100]);
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
// adding the svg to the html structure
var svg = d3.select("div#viz").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var treeData = {
"name": "Grandparent",
"size": 100,
"children": [{
"name": "Parent A",
"size": 70,
"children": [{
"name": "Son of A",
"size": 30,
"children": [{
"name": "grandson of A",
"size": 3
},
{
"name": "grandson 2 of A",
"size": 2
},
{
"name": "grandson 3 of A",
"size": 5
},
{
"name": "grandaughter of A",
"size": 20,
"children": [{
"name": "great-grandson of A",
"size": 15
},
{
"name": "great-grandaughter of A",
"size": 5
}
]
}
],
},
{
"name": "Daughter of A",
"size": 40
}
]
},
{
"name": "Parent B",
"size": 30
}
],
};
edge_weight.domain([0, treeData.size]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
root.children.forEach(collapse);
update(root);
d3.select(self.frameElement).style("height", "800px");
/**
* Updates the node.
* cloppases and expands the node bases on the structure of the source
* all 'children' nodes are expanded and '_children' nodes collapsed
* #param {json structure} source
*/
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 180;
});
// Update the nodes…
var node = svg.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')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'node')
.attr('r', 1e-6)
.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("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", function(d) {
return edge_weight(d.data.size / 2);
})
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links
var link = svg.selectAll("path.link")
.data(links, function(d) {
return d.id;
});
//.data(links, function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function(d) {
console.log("linkEnter", d);
var o = {
x: source.x,
y: source.y
}
console.log("o", o);
return diagonal(o, o)
})
.attr("stroke", function(d) {
return "cyan";
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", function(d) {
console.log("lala", d);
/* calculating the top shift */
var source = {
x: d.x - edge_weight(calculateLinkSourcePosition(d)),
y: d.y
};
var target = {
x: d.parent.x,
y: d.parent.y
};
return diagonal({
source: source,
target: target
});
})
.attr("stroke-width", function(d) {
return edge_weight(d.target.size);
});
// 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) {
console.log("stash", d);
d.x0 = d.x;
d.y0 = d.y;
});
}
/**
* Calculates the source y-axis position of the link.
* #param {json structure} link
*/
function calculateLinkSourcePosition(link) {
targetID = link.target.id;
var childrenNumber = link.source.children.length;
var widthAbove = 0;
for (var i = 0; i < childrenNumber; i++) {
if (link.source.children[i].id == targetID) {
// we are done
widthAbove = widthAbove + link.source.children[i].size / 2;
break;
} else {
// keep adding
widthAbove = widthAbove + link.source.children[i].size
}
}
return link.source.size / 2 - widthAbove;
}
/*
* Toggle children on click.
* #param {node} d
*/
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
/*
* Collapses the node d and all the children nodes of d
* #param {node} d
*/
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
/*
* Collapses the node in the tree
*/
function collapseAll() {
root.children.forEach(collapse);
update(root);
}
/*
* Expands the node d and all the children nodes of d
* #param {node} d
*/
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
}
if (d.children) {
d.children.forEach(expand);
}
}
/*
* Expands all the nodes in the tree
*/
function expandAll() {
root.children.forEach(expand);
update(root);
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
/*stroke: steelblue;*/
opacity: 0.3;
/*stroke-width: 1.5px;*/
}
#levels {
margin-left: 120px;
}
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<button type="button" onclick="collapseAll()">Collapse All</button>
<button type="button" onclick="expandAll()">Expand All</button>
<div id="viz"></div>
</body>
Most likely I'm just blind and you'll instantly see what I did wrong, but I've been staring at this for a while now...
Thanks in advance for any help!
I'm currently working on a similar project myself and figured this could be pretty helpful.. It seems you were really close, just needed to adjust a few minor paths to reach parent/child nodes..
Basically "source" is now "parent" and there is no "target". You can see most of the updates where commented out lines are v3 with v4 updates just below. For example:
link.target.id --> link.id
link.source.children.length --> link.parent.children.length
link.source.size --> link.parent.data.size
There's a few other miscellaneous updates throughout the code as well. The one thing I couldn't get working fully were the "Expand All/Collapse All" buttons. "Expand All" seems to work ok, but "Collapse All" seems to leave the link paths alone..
Here's a working fiddle: https://jsfiddle.net/jufra0b2/
I suspect there's something that can be done to the exit link but not sure. Anyways it's a step in the right direction. Hope you figure out the rest ok..
I want to add Donut chart in each node of D3 tree chart (replace circle node in tree with Donut Chart)
Below is my Code. I want to replace the circle node with Donut chart in each node with different data that sholud be read from sample json.
// Sample JSON Data
var jsondata = {
"name": "CERT",
"children": [{
"name": ""
}, {
"name": ""
}, {
"name": "DNS",
"children": [{
"name": "FW",
"children": [{
"name": ""
}, {
"name": ""
}, {
"name": "ADC",
"children": [{
"name": ""
}, {
"name": ""
}, {
"name": "SERVERS",
"children": [{
"name": ""
}, {
"name": ""
}]
}]
}]
}]
}, {
"name": "FW",
"children": [{
"name": ""
}, {
"name": ""
}, {
"name": "DNS",
"children": [{
"name": ""
}, {
"name": ""
}, {
"name": "ADC",
"children": [{
"name": "SERVERS"
}]
}]
}]
}]
};
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
//width = 960 - margin.right - margin.left, height = 800 - margin.top - margin.bottom;
width = screen.width;
height = 800 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.separation(function(a, b) {
return a.parent === b.parent ? 1 : 2;
})
.children(function(d) {
return d.children;
})
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
var line = d3.svg.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zm = d3.behavior.zoom().scaleExtent([1, 13]).on("zoom", redraw)).append("g")
.attr("transform", "translate(" + 350 + "," + 20 + ")");;
// put JSON data to root variable
root = jsondata;
root.x0 = height / 2;
root.y0 = 0;
//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);
update(root);
//d3.select(self.frameElement).style("height", "800px");
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 180;
});
// Update the nodes
var node = svg.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")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on("click", click)
.on("mouseover", function(d) {
var g = d3.select(this); // The node
// The class is used to remove the additional text later
var info = g.append('text')
.classed('info', true)
.attr('x', 30)
.attr('y', 10)
.text(function(d) {
return d.name;
});
})
// Remove the info text on mouse out.
.on("mouseout", function() {
d3.select(this).select('text.info').remove();
});
nodeEnter.append("circle")
.attr("r", 1e-6)
.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("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
// .duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", function(d) {
return d.children ? 20 : 10;
})
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Update the links…
var link = svg.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) {
return line([{
x: d.source.y,
y: d.source.x
}, {
x: d.target.y,
y: d.target.x
}]);
});
// Transition links to their new position.
link.transition()
//.duration(900)
.attr("d", function(d) {
return line([{
x: d.source.y,
y: d.source.x
}, {
x: d.target.y,
y: d.target.x
}]);
});
// Transition exiting nodes to the parent's new position.
link.exit().transition()
// .duration(duration)
.attr("d", function(d) {
return line([{
x: d.source.y,
y: d.source.x
}, {
x: d.target.y,
y: d.target.x
}]);
})
.remove();
// 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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
function elbow(d, i) {
return "M" + d.source.y + "," + d.source.x + "H" + d.target.y + "V" + d.target.x + (d.target.children ? "" : "h" + margin.right);
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
//redraw graph after zoom
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.3/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>