How to update a tree graph in D3 - javascript

When I'm working with a tree graph in d3, I get the behavior I want up through adding a node in a tree graph. However, when I'm adding a node to the graph, the node (and path) get placed, but the nodes aren't balanced out and the previous nodes do not move.
I've posted a working version of the entire code here: https://codepen.io/auser/pen/mwwVJL
The JS code (for completeness sake is):
const pathGraph = (eleName, treeData, opts = {}) => {
var margin = { top: 40, right: 90, bottom: 50, left: 90 },
width = 660 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const treemap = d3.tree().size([width, height]);
const svg = d3
.select(eleName)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const updateData = newData => {
// assigns the data to a hierarchy using parent-child relationships
let nodes = treemap(d3.hierarchy(newData));
const color = d3.scaleOrdinal(d3.schemeCategory10).domain(d3.range(0, 8));
// adds the links between the nodes
const link = g
.selectAll(".link")
.data(nodes.descendants().slice(1))
.enter()
.append("path")
.attr("class", "link")
.style("stroke-width", 1)
.attr("d", function(d) {
return (
"M" + d.x + "," + d.y + "C" + d.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + d.parent.y
);
});
link.exit().remove();
// adds each node as a group
const node = g
.selectAll(".node")
.data(nodes.descendants())
.enter()
.append("g")
.attr("class", function(d) {
return "node" + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.exit().remove();
const parentTree = (d) => {
let nodeLinks = []
while(d.parent) {
nodeLinks.push(d)
d = d.parent
}
nodeLinks = nodeLinks.concat(d)
return nodeLinks
}
const activeLink = (d, o) => {
if (d === o || d.parent === o) return true;
}
node
.on('mouseover', function(d) {
const data = d3.select(this)
const linkedNodes = parentTree(d)
link
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.style('stroke', o => activeLink(d, o) ? 'red' : '#333')
.transition(500)
rect
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.transition(500)
})
.on('mouseout', d => {
const data = d3.select(this);
// console.log('d ->', d)
link
.style('stroke-width', 1)
.style('stroke', '#333')
rect
.style('stroke-width', 1)
})
// adds the circle to the node
const rect = node
.append("rect")
.attr("height", 50)
.attr("width", 50)
.style("fill", (d, i) => color(i))
.attr("x", "-0.7em");
// adds the text to the node
node
.append("text")
.attr("dy", ".52em")
.attr("y", function(d) {
return d.children ? -18 : 20;
})
.attr("dx", "-.2em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.name;
});
};
updateData(treeData);
return updateData;
};
const data = {
"name": "Root",
"children": [
{
"name": "A",
"children": [
{ "name": "B" },
{ "name": "C" }
]
},
{ "name": "D" },
{ "name": "E",
"children":[
{ "name": "F"}
] }
]
};
const mount = document.querySelector('#treea')
const updateData = pathGraph(mount, data)
setTimeout(function() {
data.children[2].children.push({ name: "H" })
updateData(data, { update: true })
}, 2000)
.tree .node rect, .tree .node circle {
fill: blue;
rounding: 5px;
}
.tree .link {
fill: none;
stroke: #222;
stroke-opacity: 1;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg id="treea" class="tree"></svg>
Any help would be greatly appreciated...

You need a proper "enter", "update" and "exit" selections. Like this:
//this is the update selection
const link = g
.selectAll(".link")
.data(nodes.descendants().slice(1));
//this is the enter selection, up to the 'merge'
link.enter()
.append("path")
.attr("class", "link")
.merge(link)//from now one, update + enter
.style("stroke-width", 1)
.attr("d", function(d) {
return (
"M" + d.x + "," + d.y + "C" + d.x + "," +
(d.y + d.parent.y) / 2 + " " + d.parent.x + "," +
(d.y + d.parent.y) / 2 + " " + d.parent.x + "," + d.parent.y
);
});
//this is the exit selection
link.exit().remove();
Here is the updated Codepen: https://codepen.io/anon/pen/RggVPO?editors=0010
And here the Stack snippet:
const pathGraph = (eleName, treeData, opts = {}) => {
var margin = { top: 40, right: 90, bottom: 50, left: 90 },
width = 660 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
const treemap = d3.tree().size([width, height]);
const svg = d3
.select(eleName)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const updateData = newData => {
// assigns the data to a hierarchy using parent-child relationships
let nodes = treemap(d3.hierarchy(newData));
const color = d3.scaleOrdinal(d3.schemeCategory20).domain(d3.range(0, 8));
// adds the links between the nodes
const link = g
.selectAll(".link")
.data(nodes.descendants().slice(1));
link.enter()
.append("path")
.attr("class", "link")
.merge(link)
.style("stroke-width", 1)
.attr("d", function(d) {
return (
"M" + d.x + "," + d.y + "C" + d.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + d.parent.y
);
}).lower();
link.exit().remove();
const node = g
.selectAll(".node")
.data(nodes.descendants())
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
const nodeEnter = node.enter()
.append("g")
.attr("class", function(d) {
return "node " + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.exit().remove();
const parentTree = (d) => {
let nodeLinks = []
while(d.parent) {
nodeLinks.push(d)
d = d.parent
}
nodeLinks = nodeLinks.concat(d)
return nodeLinks
}
const activeLink = (d, o) => {
if (d === o || d.parent === o) return true;
}
node
.on('mouseover', function(d) {
const data = d3.select(this)
const linkedNodes = parentTree(d)
link
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.style('stroke', o => activeLink(d, o) ? 'red' : '#333')
.transition(500)
rect
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.transition(500)
})
.on('mouseout', d => {
const data = d3.select(this);
// console.log('d ->', d)
link
.style('stroke-width', 1)
.style('stroke', '#333')
rect
.style('stroke-width', 1)
})
// adds the circle to the node
const rect = nodeEnter
.append("rect")
.attr("height", 50)
.attr("width", 50)
.style("fill", (d, i) => color(i))
.style('padding', 5)
.attr("rx", 6)
.attr("ry", 6)
.attr("x", "-0.7em");
// adds the text to the node
nodeEnter
.append("text")
.attr("dy", "0.8em")
.attr("y", function(d) {
return d.children ? -18 : 20;
})
.attr("dx", "0.8em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.name;
});
node.select("rect").attr("x", "-0.7em");
};
updateData(treeData);
return updateData;
};
const data = {
"name": "Root",
"children": [
{
"name": "A",
"children": [
{ "name": "B" },
{ "name": "C" }
]
},
{ "name": "D" },
{ "name": "E",
"children":[
{ "name": "F"}
] }
]
};
const mount = document.querySelector('#treea')
const updateData = pathGraph(mount, data)
setTimeout(function() {
data.children[2].children.push({ name: "H" })
updateData(data, { update: true })
}, 2000)
.tree .node rect, .tree .node circle {
fill: blue;
rounding: 5px;
}
.tree .link {
fill: none;
stroke: #222;
stroke-opacity: 1;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg id="treea" class="tree"></svg>
PS: Things are more complicated to the nodes, because you have groups with rectangles and texts. I quickly changed the selections, but I suggest you to refactor that part of the code accordingly.

Related

How to align embedded external svg file to the viewbox

I managed to insert an external svg file as "svg/xml", to my canvas. When I click the "download file" button, the file is loaded and is displayed, but I am not able to align it correctly to be inside the view box. It displays at the lower left corner outside the view box. I am not able to figure out the alignment issue. I want to display the "title" centered at the top before the root node.
var treeData = [{
"name": "Top Level",
"parent": "null",
"remark": "yes",
"children": [{
"name": "Level 2: A",
"parent": "Top Level",
"remark": "yes",
"children": [{
"name": "Son of A",
"parent": "Level 2: A",
"remark": "null"
},
{
"name": "Daughter of A",
"parent": "Level 2: A",
"remark": "null"
}
]
},
{
"name": "Level 2: B",
"parent": "Top Level",
"remark": "null"
}
]
}];
//*************************************************************************************
// 1. Create the button
var button = document.createElement("button");
button.innerHTML = "download file";
//*************************************************************************************
// ************** Generate the tree diagram *****************
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var i = 0,
rectW = 100,
rectH = 30,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
//swap x and y for vertical
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + rectW / 2, d.y + rectH / 2];
// .projection(function(d) { return [d.x, d.y]; });
});
var svgBgImg = document.createElementNS('http://www.w3.org/2000/svg', 'image');
svgBgImg.setAttributeNS(null, 'height', '100%');
svgBgImg.setAttributeNS(null, 'width', '100%');
svgBgImg.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'http://localhost/images/background-F4EED7.svg');
svgBgImg.setAttributeNS(null, 'x', '0');
svgBgImg.setAttributeNS(null, 'y', '0');
svgBgImg.setAttributeNS(null, 'visibility', 'visible');
//var gElement = document.createElement('svg:g');
var gElement = document.createElementNS(d3.ns.prefix.svg, 'g');
gElement.setAttribute("id", "fg");
console.log(gElement);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append(function() {
return gElement;
}) //The argument to .append() has to be a function, you can't just pass element to it.
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;
update(root);
d3.select(self.frameElement).style("height", "500px");
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.
//swap x and y for vertical
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.on("click", click);
nodeEnter.append("rect")
//nodeEnter.append("circle")
// .attr("r", 1e-6)
.attr("width", rectW)
.attr("height", rectH)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.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);
// Filter to put tooltip only on nodes that have information associated with it
var nodesWithInfo = nodeEnter.filter(function(d) {
return (d.remark != 'null')
})
nodesWithInfo
.append("circle")
.attr("export-ignore", true)
.attr("cx", 96)
.attr("cy", 7)
.attr("r", 10);
nodesWithInfo
.append("text")
.attr("export-ignore", true)
.attr("id", "infoText")
.attr("x", 96)
.attr("y", 7)
.attr("dy", ".30em")
.style("font-style", "italic")
.style("font-family", "serif")
.style("font-weight", "bold")
.text("i");
// Transition nodes to their new position.
//swap x and y for vertical layout
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
//nodeUpdate.select("circle")
nodeUpdate.select("rect")
.attr("width", rectW)
.attr("height", rectH)
.style("stroke", "black")
// .attr("r", 10)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeUpdate.select("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
//vertical switch x and y positions
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
//nodeExit.select("circle")
nodeExit.select("rect")
.attr("width", rectW)
.attr("height", rectH)
.style("stroke", "black");
// .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.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("stroke", "#ccc")
.attr("fill", "none")
.attr("stroke-width", "2px")
.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;
});
}
// 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);
}
//*************************************************************************************
// 2. Append button somewhere
var body = document.getElementsByTagName("body")[0];
body.appendChild(button)
.setAttribute("transform", "translate(" + 0 + "," + 0 + ")");
//*************************************************************************************
function svgDataURL(svg) {
var svgAsXML = (new XMLSerializer).serializeToString(svg);
var dataURL = "data:image/svg+xml," + encodeURIComponent(svgAsXML);
return dataURL;
}
button.onclick = function() {
var groupElement = document.getElementById('fg');
const bb = groupElement.getBBox();
//load the title image
var titlePosX = bb.width / 6 + bb.x + 25; //added offeset of 25 to align to center as much as possible
var imageWidth = bb.width / 4;
var imageHeight = imageWidth * .33;
var newVboxH = bb.height + imageHeight + 10;
console.log('newVboxH ', newVboxH);
console.log('titlePosX ' + titlePosX + ' imageHeight ', +imageHeight + ' imageWidth ' + imageWidth);
groupElement.setAttribute("transform", "translate(0," + (imageHeight + 10) + ")scale(.995,.995)"); //scale down by .05% to fit nicely
svg.attr('viewBox', ' ' + bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + newVboxH);
svg.attr("width", "100%");
svg.attr("height", "100%");
svg.attr("preserveAspectRatio", "xMidYMid meet");
//how to put the border around the cloned SVG element
var borderRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
borderRect.setAttribute("id", "bRect");
borderRect.setAttribute("width", bb.width);
borderRect.setAttribute("height", newVboxH);
borderRect.setAttribute("x", bb.x);
borderRect.setAttribute("y", bb.y);
borderRect.setAttribute("style", "fill:blue;stroke:pink;stroke-width:5;fill-opacity:0.1;stroke-opacity:0.9");
d3.xml("https://upload.wikimedia.org/wikipedia/commons/8/8c/SVG_logo_h.svg").mimeType("image/svg+xml").get(function(error, xml) {
if (error) throw error;
xml.documentElement.setAttribute("transform", "translate(0,0)scale(.995,.995)");
xml.documentElement.setAttribute("transform", "translate(0," + (imageHeight + 10) + ")scale(.995,.995)"); //scale down by .05% to fit nicely
xml.documentElement.setAttribute('width', imageWidth);
groupElement.appendChild(xml.documentElement);
groupElement.appendChild(borderRect);
// bring back to original position
groupElement.setAttribute("transform", 'translate(' + margin.left + ',' + margin.top + ')scale(.7)');
});
};
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1px;
}
.node text {
font: 12px sans-serif;
}
.link {
/*
fill: none;
stroke: #ccc;
stroke-width: 2px;*/
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tree Example</title>
</head>

D3 Tree Chart with left and right nodes on every nodes

I have a d3 chart that have both left and right paths/nodes. now what i'm trying to do is that on click of a node i want to append the same data ( same tree with left and right nodes ) and this new tree will be populated/centerd according to the clicked nodes x and y values, so i tried to add a new g with the x and y values i got from the object clicked.
like this
var g = svg.append("g")
.attr("transform", "translate(" + d.x * 2 + "," + d.y + ")");
drawTree2(left, "left", d);
drawTree2(right, "right", d);
but its not working, please help
var data = {
"name": "Root",
"children": [{
"name": "Branch 1"
}, {
"name": "Branch 2",
}, {
"name": "Branch 3"
}, {
"name": "Branch 4",
}, {
"name": "Branch 5"
},
{
"name": "Branch 6"
}, {
"name": "Branch 7",
}, {
"name": "Branch 8"
}, {
"name": "Branch 9",
}, {
"name": "Branch 10"
}
]
};
var split_index = Math.round(data.children.length / 2)
// Left data
var data1 = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children.slice(0, split_index)))
};
// Right data
var data2 = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
// Create d3 hierarchies
var right = d3.hierarchy(data1);
var left = d3.hierarchy(data2);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + ",0)");
// Render both trees
drawTree(right, "right")
drawTree(left, "left")
function drawTree(root, pos) {
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
// Create new default tree layout
var tree = d3.tree()
// Set the size
// Remember the tree is rotated
// so the height is used as the width
// and the width as the height
.size([height, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
// Set both root nodes to be dead center vertically
nodes[0].x = height / 2
// Create links
var link = g.selectAll(".link." + pos)
.data(links)
.join(
enter => enter.append("path"),
update => update,
exit => exit.remove()
)
.attr("class", "link " + pos)
.attr("d", function(d) {
return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
});
// Create nodes
var node = g.selectAll(".node." + pos)
.data(nodes)
.join(
enter => {
const n = enter
.append("g")
.on("click", (e, d) => {
drawSecondTree(d);
});
n.append("circle").attr("r", 2.5);
n.append("text").attr("y", -10).style("text-anchor", "middle");
return n;
},
update => update,
exit => exit.remove()
)
.attr("class", function(d) {
return "node " + pos + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.select("text")
.text(function(d) {
return d.data.name
});
}
function drawSecondTree(d) {
var g = svg.append("g")
.attr("transform", "translate(" + d.x * 2 + "," + d.y + ")");
drawTree2(left, "left", d);
drawTree2(right, "right", d);
function drawTree2(root, pos, d) {
console.log(d.x, d.y);
//return false;
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
// Create new default tree layout
var tree = d3.tree()
// Set the size
// Remember the tree is rotated
// so the height is used as the width
// and the width as the height
.size([height, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
// Set both root nodes to be dead center vertically
nodes[0].x = d.y;
// Create links
var link = g.selectAll(".link." + pos)
.data(links)
.join(
enter => enter.append("path"),
update => update,
exit => exit.remove()
)
.attr("class", "link " + pos)
.attr("d", function(d) {
return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
});
// Create nodes
var node = g.selectAll(".node." + pos)
.data(nodes)
.join(
enter => {
const n = enter
.append("g")
.on("click", (e, d) => toggle(d, pos, pos === "left" ? left : right));
n.append("circle").attr("r", 2.5);
n.append("text").attr("y", -10).style("text-anchor", "middle");
return n;
},
update => update,
exit => exit.remove()
)
.attr("class", function(d) {
return "node " + pos + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.select("text")
.text(function(d) {
return d.data.name
});
}
}
.node circle {
fill: #999;
}
.node text {
font: 12px sans-serif;
}
.node--internal circle {
fill: #555;
}
.link {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width="900" height="600"></svg>
I think I understand what you're trying to get at.
It's hard to explain because the way this is setup is a bit confusing. Basically, the main issue is the way you are creating the new g element's "center point" (the translate(x,y) part). The reason it's confusing is because you are switching X and Y coordinates in certain places. Maybe you can't get around that with how you want the map to look, which is fine, it's just hard to follow along.
I made the following changes in the drawSecondTree function (note the "added" and "updated" items):
function drawSecondTree(d) {
var gX = (width / 2) + d.y; // ********** added
var gY = d.x - gX; // ********** added
var g = svg.append("g")
.attr("transform", "translate(" + gX + "," + gY + ")"); // ********** updated
drawTree2(left, "left", d);
drawTree2(right, "right", d);
function drawTree2(root, pos, d) {
console.log(d.x, d.y);
//return false;
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
// Create new default tree layout
var tree = d3.tree()
// Set the size
// Remember the tree is rotated
// so the height is used as the width
// and the width as the height
.size([height, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
// Set both root nodes to be dead center vertically
// nodes[0].x = d.y;
nodes[0].x = (width / 2) + d.y; // ********** updated
// Create links
var link = g.selectAll(".link." + pos)
.data(links)
.join(
enter => enter.append("path"),
update => update,
exit => exit.remove()
)
.attr("class", "link " + pos)
.attr("d", function(d) {
return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
});
// Create nodes
var node = g.selectAll(".node." + pos)
.data(nodes)
.join(
enter => {
const n = enter
.append("g")
.on("click", (e, d) => toggle(d, pos, pos === "left" ? left : right));
n.append("circle").attr("r", 2.5);
n.append("text").attr("y", -10).style("text-anchor", "middle");
return n;
},
update => update,
exit => exit.remove()
)
.attr("class", function(d) {
return "node " + pos + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.select("text")
.text(function(d) {
return d.data.name
});
}
}
And here's a codepen of the full code working (with overlap of the nodes).
I would also recommend looking into using the viewBox of the svg element and figuring out how to zoom out when you add more nodes by clicking on them.

D3.js draw diamonds and connecting links in a decision tree

I am working on the below layout ( decision tree) using D3 where I need to draw diamond shapes for the nodes that are "decisions" in a flow chart and rest of the nodes are actions ( rectangles).
Logically, all the nodes that have children are diamond shapes. Below is a UX Visualisation.
I've come up with a jsfiddle for the top to bottom D3 chart here : https://jsfiddle.net/p6vrmnu0/3/
But all the svg elements are currently rectangles and all the lines are now connected by "curved" links, I would like svgs to be diamonds for "decision" nodes and the links to be similar to the above image originating from the diamond's two corners and ending at at the top of the rhombus if the next is decision or top of rectangle if the next is an action.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
var emptyDecisionBox = {
"name": "newDecision",
"id": "newId",
"value": "notSure",
"condition": "true",
};
var selectedNode;
var root = {
"name": "Root",
"children": [{
"name": "analytics",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "true",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4"
}],
},
{
"name": "division",
"type": "action",
"value": "a-b",
"children": [],
}
]
};
var i = 0,
duration = 750,
rectW = 60,
rectH = 30;
var tree = d3.layout.tree().nodeSize([70, 40]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + rectW / 2, d.y + rectH / 2];
});
var svg = d3.select("body").append("svg").attr("width", 1000).attr("height", 1000)
.call(zm = d3.behavior.zoom().scaleExtent([1, 3]).on("zoom", redraw)).append("g")
.attr("transform", "translate(" + 350 + "," + 20 + ")");
//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);
// new part
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("CLICKED");
});
plusButton
.append('rect')
.attr('transform', 'translate(-8, -8)') // center the button inside the `g`
.attr('width', 16)
.attr('height', 16)
.attr('rx', 2);
plusButton
.append('path')
.attr('d', 'M-6 0 H6 M0 -6 V6');
var rectangleShape = svg.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
removeAllButtonElements();
})
rectangleShape
.append('rect')
.attr('width', 40)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('button', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
addElement(selectedNode);
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 20 0 40 20 20 40 0 20 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("rectangle clicked");
removeAllButtonElements();
})
rectangleShapeFalse
.append('rect')
.attr('width', 40)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('button', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 20 0 40 20 20 40 0 20 Z')
.style("fill", 'orange');
function removeAllButtonElements() {
plusButton.classed('hide', true);
diamondImage.classed('hide', true);
rectangleShape.classed('hide', true);
diamondImageFalse.classed('hide', true);
rectangleShapeFalse.classed('hide', true);
}
// new part ends.
root.x0 = 0;
root.y0 = height / 2;
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
/* root.children.forEach(collapse); */
update(root);
d3.select("#body").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.x0 + "," + source.y0 + ")";
})
.on("click", click);
nodeEnter.append("rect")
.attr("width", rectW)
.attr("height", rectH)
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
/* nodeEnter.append('path').attr("d", d3.svg.symbol().type( function(d) { return "circle" }) );
*/
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodeUpdate.select("rect")
.attr("width", rectW)
.attr("height", rectH)
.attr("stroke", "black")
.attr("stroke-width", 1)
.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.x + "," + source.y + ")";
})
.remove();
nodeExit.select("rect")
.attr("width", rectW)
.attr("height", rectH)
//.attr("width", bbox.getBBox().width)""
//.attr("height", bbox.getBBox().height)
.attr("stroke", "black")
.attr("stroke-width", 1);
nodeExit.select("text");
// 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("x", rectW / 2)
.attr("y", rectH / 2)
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
.on('mouseenter', function(d, i) {
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x = bbox.x + bbox.width / 2,
y = bbox.y + bbox.height / 2;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
})
.on('mouseleave', function(d, i) {
plusButton
.classed('hide', true);
});
// 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;
});
}
// 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);
} */
function click(d) {
console.log(d);
selectedNode = d;
/* if(d.children && d.children.length < 1){
return;
} */
var x = d.x;
var y = d.y + 40;
/* plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
plusButton
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false); */
var m = d.x + 50;
var h = d.y + 20;
diamondImage
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x + 60;
var h = d.y - 10;
rectangleShape
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 40;
var h = d.y + 20;
diamondImageFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 40;
var h = d.y - 10;
rectangleShapeFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
}
//Redraw for zoom
function redraw() {
//console.log("here", d3.event.translate, d3.event.scale);
svg.attr("transform",
"translate(" + d3.event.translate + ")" +
" scale(" + d3.event.scale + ")");
}
function addElement(d) {
console.log(d);
d.children = [];
d.children.push(emptyDecisionBox);
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: #ccc;
stroke-width: 1.5px;
}
body {
overflow: hidden;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.hide {
display: none;
opacity: 0 !important;
pointer-events: none;
}
.link:hover {
cursor: pointer;
stroke-width: 8px;
}
.scale {
/* transform: scale(0.4); */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>
For the line shape, I recommend to just write a shape function yourself. I wrote one that goes horizontal, then makes a small curve, then goes down. It doesn't take the width of the shape into account, but that shouldn't matter, since it's behind the box after all.
For the box shapes, I'd do something similar. Instead of using rects, use a path, and write the shape of the path using an if statement. This is always a very valuable resource.
Just to get you started:
function drawDiamond(centroid) {
// Start at the top
var result = 'M' + centroid.x + ',' + (centroid.y - rectH / 2);
// Go right
result += 'L' + (centroid.x + rectW / 2) + ',' + centroid.y;
// Bottom
result += 'L' + centroid.x + ',' + (centroid.y + rectH / 2);
// Left
result += 'L' + (centroid.x - rectW / 2) + ',' + centroid.y;
// Close the shape
result += 'Z';
return result;
}
function drawRect(centroid) {
// Start at the top left
var result = 'M' + (centroid.x - rectW / 2) + ',' + (centroid.y - rectH / 2);
// Go right
result += 'h' + rectW;
// Go down
result += 'v' + rectH;
// Left
result += 'h-' + rectW;
// Close the shape
result += 'Z';
return result;
}
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
var emptyDecisionBox = {
"name": "newDecision",
"id": "newId",
"value": "notSure",
"condition": "true",
};
var selectedNode;
var root = {
"name": "Root",
"children": [{
"name": "analytics",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "true",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4"
}],
},
{
"name": "division",
"type": "action",
"value": "a-b",
"children": [],
}
]
};
var i = 0,
duration = 750,
rectW = 60,
rectH = 30;
var tree = d3.layout.tree().nodeSize([70, 40]);
var linkFunc = function(d) {
var source = {
x: d.source.x,
y: d.source.y + (rectH / 2)
};
var target = {
x: d.target.x + (rectW / 2),
y: d.target.y
};
// This is where the line bends
var inflection = {
x: target.x,
y: source.y
};
var radius = 5;
var result = "M" + source.x + ',' + source.y;
if (source.x < target.x) {
// Child is to the right of the parent
result += ' H' + (inflection.x - radius);
} else {
result += ' H' + (inflection.x + radius);
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
}
var svg = d3.select("body").append("svg").attr("width", 1000).attr("height", 1000)
.call(zm = d3.behavior.zoom().scaleExtent([1, 3]).on("zoom", redraw)).append("g")
.attr("transform", "translate(" + 350 + "," + 20 + ")");
//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);
// new part
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("CLICKED");
});
plusButton
.append('rect')
.attr('transform', 'translate(-8, -8)') // center the button inside the `g`
.attr('width', 16)
.attr('height', 16)
.attr('rx', 2);
plusButton
.append('path')
.attr('d', 'M-6 0 H6 M0 -6 V6');
var rectangleShape = svg.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
removeAllButtonElements();
})
rectangleShape
.append('rect')
.attr('width', 40)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('button', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
addElement(selectedNode);
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 20 0 40 20 20 40 0 20 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("rectangle clicked");
removeAllButtonElements();
})
rectangleShapeFalse
.append('rect')
.attr('width', 40)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('button', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 20 0 40 20 20 40 0 20 Z')
.style("fill", 'orange');
function removeAllButtonElements() {
plusButton.classed('hide', true);
diamondImage.classed('hide', true);
rectangleShape.classed('hide', true);
diamondImageFalse.classed('hide', true);
rectangleShapeFalse.classed('hide', true);
}
// new part ends.
root.x0 = 0;
root.y0 = height / 2;
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
/* root.children.forEach(collapse); */
update(root);
d3.select("#body").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.x0 + "," + source.y0 + ")";
})
.on("click", click);
nodeEnter.append("rect")
.attr("width", rectW)
.attr("height", rectH)
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
/* nodeEnter.append('path').attr("d", d3.svg.symbol().type( function(d) { return "circle" }) );
*/
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodeUpdate.select("rect")
.attr("width", rectW)
.attr("height", rectH)
.attr("stroke", "black")
.attr("stroke-width", 1)
.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.x + "," + source.y + ")";
})
.remove();
nodeExit.select("rect")
.attr("width", rectW)
.attr("height", rectH)
//.attr("width", bbox.getBBox().width)""
//.attr("height", bbox.getBBox().height)
.attr("stroke", "black")
.attr("stroke-width", 1);
nodeExit.select("text");
// 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("x", rectW / 2)
.attr("y", rectH / 2)
.attr("d", linkFunc)
.on('mouseenter', function(d, i) {
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x = bbox.x + bbox.width / 2,
y = bbox.y + bbox.height / 2;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
})
.on('mouseleave', function(d, i) {
plusButton
.classed('hide', true);
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", linkFunc)
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
function click(d) {
console.log(d);
selectedNode = d;
var x = d.x;
var y = d.y + 40;
var m = d.x + 50;
var h = d.y + 20;
diamondImage
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x + 60;
var h = d.y - 10;
rectangleShape
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 40;
var h = d.y + 20;
diamondImageFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 40;
var h = d.y - 10;
rectangleShapeFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
}
//Redraw for zoom
function redraw() {
//console.log("here", d3.event.translate, d3.event.scale);
svg.attr("transform",
"translate(" + d3.event.translate + ")" +
" scale(" + d3.event.scale + ")");
}
function addElement(d) {
console.log(d);
d.children = [];
d.children.push(emptyDecisionBox);
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: #ccc;
stroke-width: 1.5px;
}
body {
overflow: hidden;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.hide {
display: none;
opacity: 0 !important;
pointer-events: none;
}
.link:hover {
cursor: pointer;
stroke-width: 8px;
}
.scale {
/* transform: scale(0.4); */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>
Now, there are some problems with your code in general.
You set the attributes of items you're going to delete, or items that have already been set. Remember that every node that is on the page has already been entered once, so when you set their width/height/stroke, there is no reason to ever set it again, unless it changes. Similarly, there is no reason to use nodeUpdate.select('rect') and then set all the values all over again, just remove that code, it does nothing and leads to clutter.
You also don't select by class, but by tag. That will give you problems as soon as you start with paths. Use classes instead!

How to display parent elements in a nested treemap?

This is my data : https://api.myjson.com/bins/b0m6s
I want to create a nested treemap that shows the parent element and the child elements inside it, like this example : https://bl.ocks.org/mbostock/911ad09bdead40ec0061
Here is my treemap code currently :
var defaultColors = d3.scale.ordinal().range(["#00AEEF", "#8DC63F", "#FFC20E", "#F06EAA", "#AE9986", "#009BA5", "#00A651", "#F7941D", "#B656AB", "#ABA000", "#F3716D", "#8D7B6B", "#EF413D", "#AD772B", "#878787"]);
var treemap;
var h_pad = 2, // 2 pixels vertical padding
v_pad = 4; // 4 pixels of horizontal padding (2 px at each side)
var canvas = d3
.select(id)
.append("svg")
.attr("class", "chart")
.attr("width", cfg.width + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.height + cfg.margin.top + cfg.margin.bottom)
.attr("viewBox", "0 0 960 500")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("id", "canvas")
var innercanvas = canvas
.append("g")
.attr("class", "innercanvas")
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")");
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding(.25)
.sticky(true)
.nodes(data);
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", function (d, i) {
return 'newcell _' + i // i provides a unique identifier for each node
+ ' cell-level-' + d.depth // cell-level-0 for root, cell-level-1, cell-level-2, etc
+ (d.name ? ' ' + safe_name(d.name) : '') // if d.name exists, use the 'safe' version
+ (!d.children
? ' leaf' // d has no children => it's a leaf node
: (d.depth === 0
? ' root' // d.depth = 0 => it's the root node
: ' internal ')); // has children, depth > 0 => internal node
})
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("id", "rectangle")
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children && d.parent ? defaultColors(d.name) : cfg.color ? cfg.color(d.name) : null;
})
.attr("stroke", "#000000")
.attr('pointer-events', 'all');
cells
.append("text")
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.y + d.dy / 2;
})
.attr("text-anchor", "middle")
.text(function (d) { return d.parent ? d.name : '' })
cells
.append('title')
.text(function (d) {
if (d.parent) {
return categoryKey + " : " + d.parent.name + "\n" + groupKey + " : " + d.name + "\n" + sizeKey + " : " + toCommas(d.value.toFixed(2))
}
return d.name;
});
Wrapping the texts into multiple lines
function groupAddText(selection) {
var v_pad = 2, // vertical padding
h_pad = 4 // horizontal padding
selection.selectAll('.leaf text')
.classed('groupOversize', function (d) {
if (!d.name) {
return false;
}
var bbox = this.getBBox();
if (d.dx <= bbox.width + h_pad || d.dy <= bbox.height + v_pad) {
d3.select(this).node().textContent = "";
var lines = wordwrap2(d.name, d.dx).split('\n');
for (var i = 0; i < lines.length; i++) {
d3.select(this)
.append("tspan")
.attr("dy", 15)
.attr("x", d.x + d.dx / 2)
.text(lines[i]);
}
d3.selectAll(".groupOversize").attr("y", function (d) {
return (d.y + d.dy / 2) - 20;
})
return true;
}
return false;
});
}
function wordwrap2(str, width, brk, cut) {
brk = brk || '\n';
width = width || 75;
cut = cut || false;
if (!str) { return str; }
var regex = '.{1,' + width + '}(\\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\\S+?(\\s|$)');
return str.match(RegExp(regex, 'g')).join(brk);
}
This produces the following treemap visualization :
As you can see, I separated my cells into 3 classes, root for the root node, internal for the parents and leaf for the children. Right now, it is just showing the children rects. How do I show the parent elements with the children nested inside them?
I want something like this :
[![enter image description here][2]][2]
You can view the parent cells by adding padding to your treemap:
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding(20) // 20px padding all around
.sticky(true)
.nodes(data);
or
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding([20,5,5,5]) // 20px top, 5px sides and bottom
.sticky(true)
.nodes(data);
I made a little demo showing the effects of altering the padding in a d3 treemap here - although note that that is d3 v5, so the options are slightly different.
Here's a demo with your code:
var data = {"children":[{"name":"Central","children":[{"name":"Cellophane Tape","value":"419141.4728"},{"name":"File Separator","value":"327285.0157"},{"name":"Hard Cover File","value":"422707.1194"},{"name":"Highlighter","value":"488978.5362"},{"name":"Office Chair","value":"453843.621"},{"name":"Pencil","value":"416819.1027"},{"name":"Tape Dispenser","value":"393290.5862"},{"name":"File Cabinet","value":"424647.6003"},{"name":"Plastic Comb Binding","value":"230299.6657"},{"name":"White Board Markers","value":"383157.5055"},{"name":"Binder","value":"415871.6793"},{"name":"Eraser","value":"477885.9162"},{"name":"Pen","value":"444834.4362"},{"name":"Pen Set","value":"434495.1303"},{"name":"Desk","value":"247046.3919"}]},{"name":"East","children":[{"name":"Pencil","value":"441970.1055"},{"name":"White Board Markers","value":"416822.5561"},{"name":"Eraser","value":"393738.4951"},{"name":"Hard Cover File","value":"407371.1911"},{"name":"Office Chair","value":"382574.6347"},{"name":"Tape Dispenser","value":"481960.7562"},{"name":"Cellophane Tape","value":"441438.7362"},{"name":"File Cabinet","value":"333187.8858"},{"name":"Binder","value":"462926.3793"},{"name":"File Separator","value":"441311.7555"},{"name":"Plastic Comb Binding","value":"330059.7762"},{"name":"Highlighter","value":"399332.0562"},{"name":"Pen","value":"492374.2362"},{"name":"Pen Set","value":"477206.7762"},{"name":"Desk","value":"254464.9453"}]},{"name":"North","children":[{"name":"Office Chair","value":"459306.6555"},{"name":"Pencil","value":"465763.0477"},{"name":"Eraser","value":"441687.1652"},{"name":"File Cabinet","value":"463598.5893"},{"name":"File Separator","value":"430346.1162"},
{"name":"Hard Cover File","value":"346325.0175"},{"name":"Highlighter","value":"223199.4072"},{"name":"Tape Dispenser","value":"311201.7216"},{"name":"Plastic Comb Binding","value":"445513.5762"},{"name":"Binder","value":"453219.921"},{"name":"White Board Markers","value":"334737.9189"},{"name":"Cellophane Tape","value":"372554.952"},{"name":"Pen","value":"435830.2872"},{"name":"Pen Set","value":"460001.8962"},{"name":"Desk","value":"260294.2303"}]},{"name":"South","children":[{"name":"Pencil","value":"457331.6055"},{"name":"Tape Dispenser","value":"442628.4555"},{"name":"Cellophane Tape","value":"468037.3351"},{"name":"Eraser","value":"341469.2127"},{"name":"File Cabinet","value":"408198.2058"},{"name":"File Separator","value":"416543.8893"},{"name":"Office Chair","value":"466438.7227"},{"name":"Plastic Comb Binding","value":"436440.1272"},{"name":"White Board Markers","value":"437968.1344"},{"name":"Highlighter","value":"411905.4555"},{"name":"Binder","value":"456806.1151"},{"name":"Hard Cover File","value":"493053.3762"},{"name":"Pen","value":"413820.3762"},{"name":"Pen Set","value":"488299.3962"},{"name":"Desk","value":"264499.5623"}]},{"name":"West","children":[{"name":"Pencil","value":"458648.3055"},{"name":"Cellophane Tape","value":"299045.7162"},{"name":"File Cabinet","value":"386045.352"},{"name":"File Separator","value":"435098.0403"},{"name":"Highlighter","value":"457454.0701"},{"name":"Office Chair","value":"262021.1055"},{"name":"Plastic Comb Binding","value":"413222.1555"},{"name":"Eraser","value":"449997.2978"},{"name":"Hard Cover File","value":"364335.5793"},{"name":"Binder","value":"467389.3801"},{"name":"Tape Dispenser","value":"394066.5845"},{"name":"White Board Markers","value":"408833.4789"},{"name":"Pen","value":"481281.6162"},{"name":"Pen Set","value":"398652.9162"},{"name":"Desk","value":"229482.2954"}]}]};
data.name = 'root'
var defaultColors = d3.scale.ordinal().range(["#00AEEF", "#8DC63F", "#FFC20E", "#F06EAA", "#AE9986", "#009BA5", "#00A651", "#F7941D", "#B656AB", "#ABA000", "#F3716D", "#8D7B6B", "#EF413D", "#AD772B", "#878787"]);
var treemap;
var h_pad = 2, // 2 pixels vertical padding
v_pad = 4; // 4 pixels of horizontal padding (2 px at each side)
var id = 'treemap';
var cfg = { width: 960, height: 500, margin: { left: 10, right: 10, bottom: 10, top: 10 }, color: d3.scale.category20() }
var canvas = d3
.select('#' + id)
.append("svg")
.attr("class", "chart")
.attr("width", cfg.width + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.height + cfg.margin.top + cfg.margin.bottom)
.attr("viewBox", "0 0 960 500")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("id", "canvas")
var innercanvas = canvas
.append("g")
.attr("class", "innercanvas")
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")");
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding([20,5,5,5])
.sticky(true)
.nodes(data);
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", function (d, i) {
return 'newcell _' + i // i provides a unique identifier for each node
+ ' cell-level-' + d.depth // cell-level-0 for root, cell-level-1, cell-level-2, etc
+ (d.name ? ' ' + d.name : '') // if d.name exists, use the 'safe' version
+ (!d.children
? ' leaf' // d has no children => it's a leaf node
: (d.depth === 0
? ' root' // d.depth = 0 => it's the root node
: ' internal ')); // has children, depth > 0 => internal node
})
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("id", function(d,i){ return 'rect_' + i; })
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children && d.parent ? defaultColors(d.name) : cfg.color ? cfg.color(d.name) : null;
})
.attr("stroke", "#000000")
.attr('pointer-events', 'all');
cells.append("clipPath")
.attr("id", function(d,i) { return "clip_" + i ; })
.append("use")
.attr("xlink:href", function(d,i) {
return "#rect_" + i;
});
cells
.append("text")
.attr("clip-path", function(d,i) { return "url(#clip_" + i })
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.children ? d.y + 12 : d.y + d.dy / 2 ;
})
.attr("text-anchor", "middle")
.text(function (d) { return d.name })
cells
.append('title')
.text(function (d) {
if (d.parent) {
return "categoryKey : " + d.parent.name + "\ngroupKey : " + d.name + "\nsizeKey : " + d.value.toFixed(2)
}
return d.name;
});
svg text {
font-size: 10px;
}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script><div id="treemap"></div>

Link Paths disappears on re-call same tree function - D3 tree

I am able to draw tree structure with json data on initial load of the application.
Now my requirement is, on click of button I should be able to add more text/css to each nodes which is working fine.
I am actually using the same function used to draw tree initially, able to interchange data on trigger of button.
When I do so, on click the first level node (to expand to second level), first level paths disappears from g tag and selected node changes the position and lies in second level data.
Tired all possible ways, can anyone help to resolve this.
thanks in advance
Below is the tree function used
//To draw tree structure for the selected data
drawTree: function() {
var driverTree = this;
var id = "#" + this.getView().byId("chartArea").sId;
// ************** Generate the tree diagram *****************
var treeData = driverTree.treeModel.oData;
var radius = 40;
var width = $(id).width();
var height = window.innerHeight;
var maxLabel = 1500;
var i = 0,
duration = 450,
root;
var tree = d3.layout.tree()
.size([height, width])
.separation(function(a, b) {
return (a.parent == b.parent ? 1 : 2);
});
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
}
var slider = d3.scale.linear()
.domain([1, 100])
.range([1, 100])
.clamp(true);
var canvasWidth = radius * 2 + margin.left + margin.right,
canvasHeight = radius * 2 + margin.top + margin.bottom;
var color = d3.scale.category10();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
var pie = d3.layout.pie()
.value(function(d) {
if (isNaN(parseFloat(d.value)))
return 0;
else
return parseFloat(d.value);
})
.sort(null);
// arc object
var arc = d3.svg.arc()
.outerRadius(40)
.innerRadius(20);
//var id = "#" + chartArea.sId;
d3.select(id).select("svg").remove();
driverTree.zoomListener = d3.behavior.zoom()
.scaleExtent([0.1, 3])
.on("zoom", function(d) {
zoomed(d);
});
var svgGroup = d3.select(id).append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "tooltip")
.call(driverTree.zoomListener);
function zoomed(d) {
if (driverTree.hoizMode)
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")rotate(90,50,50)");
else
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
var svg = svgGroup.append("g")
.attr("transform", "translate(" + width + "," + (height / 2) / 2 + ") rotate(90,50,50)");
root = treeData;
root.x0 = height / 2;
root.y0 = 0;
var div = d3.select("body").append("div").attr("class", "tooltip").style("opacity", "0");
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
root.children.forEach(collapse);
//update(root);
//d3.select(self.frameElement).style("height", "800px");
var circumference_r = 35;
function update(source) {
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) * 263.6; // 25 pixels per line
tree = tree.size([newHeight, window.innerHeight]);
// 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("id", function(d) {
return "id" + d.title;
})
.attr("class", "node")
.attr("transform", function(d) {
if (driverTree.hoizMode)
return "translate(" + source.y0 + "," + source.x0 + ") rotate(-90)";
else
return "translate(" + source.y0 + "," + source.x0 + ") rotate(0)";
})
.on("mouseover", function(d) {
mouseover(d);
})
.on("mousemove", function(d) {
mousemove(d);
})
.on("mouseout", function(d) {
mouseout(d);
});
if (driverTree.editTree) {
nodeEnter.append("rect")
.attr("id", "hideButton")
.attr("width", function(d) {
if (d.id != "NODE1") return "40px";
})
.attr("transform", function(d) {
if (d.id != "NODE1") return "translate(-40,-70)";
})
.attr("rx", "5")
.attr("ry", "5")
.attr("height", function(d) {
if (d.id != "NODE1") return "25px";
})
.style("stroke", "#999faa")
.style("stroke-width", "2px")
.style("fill", function(d) {
if (d.isHidden) return "#000f2b";
else return "#8E94A1";
})
.on("click", function(d) {
d.isHidden = !d.isHidden;
if (d.isHidden) return this.style.fill = "#000f2b";
else return this.style.fill = "#8E94A1";
});
nodeEnter.append("text")
.attr("id", "hideText")
.attr("transform", "translate(-31,-54)")
.style("fill", "#eee")
.text(function(d) {
if (d.id != "NODE1") return "Hide";
})
.on("click", function(d) {
d.isHidden = !d.isHidden;
if (d.isHidden) return d3.select(this.parentNode).select("rect").style("fill", "#000f2b");
else return d3.select(this.parentNode).select("rect").style("fill", "#8E94A1");
});
}
nodeEnter.append("circle")
.attr("r", "40")
.on("click", click);
nodeEnter.append("text")
.attr("id", function(d) {
return "ct" + d.title;
})
.attr("x", "-8")
.attr("y", "4")
.text(function(d) {
return brevoVDT.util.Formatter.amountToMillions(d.value);
})
.on("click", click);
nodeEnter.append("text")
.attr("transform", "translate(55,-10)")
.text(function(d) {
return "Org Value - " + brevoVDT.util.Formatter.amountToMillions(d.value_org);
})
nodeEnter.append("text")
.attr("id", function(d) {
return "cv" + d.title;
})
.attr("transform", "translate(55,5)")
.text(function(d) {
return "Currenet Value - " + brevoVDT.util.Formatter.amountToMillions(d.value);
})
nodeEnter.append("text")
.attr("id", function(d) {
return "dv" + d.title;
})
.attr("transform", "translate(55,20)")
.text(function(d) {
return "Difference " + brevoVDT.util.Formatter.amountToMillions(parseFloat(d.value_org) - parseFloat(d.value));
})
.style("fill", function(d) {
var value = parseFloat(d.value_org) - parseFloat(d.value);
if (value > 0) return "green";
else if (value < 0) return "red";
else return "black";
})
/*nodeEnter.append("text")
.attr("transform", "translate(-50,58)")
.attr("text-anchor", "start")
.text(function(d) {
return d.title;
})*/
nodeEnter.append("rect")
.attr("width", "130px")
.attr("transform", "translate(48,-25)")
.attr("height", "50px")
.attr("rx", "15")
.attr("ry", "15")
.style("fill", "rgba(238, 238, 238, 0)")
.style("opacity", "0.9")
.style("stroke",
function(d) {
if (d.children == null && d._children == null)
return "black";
else
return "orange";
})
.style("stroke-width", "2");
nodeEnter.append("g").attr("class", "slicers");
nodeEnter.append("g").attr("class", "lines");
var pieNodes = nodeEnter.select(".slicers").selectAll("path")
.data(function(d, i) {
if (d.id == "NODE1") var value = 0;
else var value = d.parent.value;
value = value == 0 ? 1 : value;
return pie([d, {
value: value
}]);
})
.enter()
.append("svg:path")
.attr("class", "slice")
.attr("fill", function(d, i) {
return color(i);
})
.each(function(d) {
d;
})
.attr('d', arc)
//toolbar
function mouseover(d) {
div.transition()
.duration(500)
.style("opacity", 0.9)
.style("display", "block");
}
function mousemove(d) {
var name = d.name.length > 13 ? d.name.slice("0", "13") : d.name;
div.html("<b>" + d.title + "</b><table>" +
"<tr><td>" + name + ":</td><td>" + parseFloat(d.value).toFixed(1) + "</td></tr></table>")
.style("left", (d3.event.pageX + 50) + "px")
.style("top", (d3.event.pageY + 0) + "px");
}
function mouseout(d) {
div.transition()
.duration(500)
.style("display", "none");
}
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
if (driverTree.hoizMode)
return "translate(" + d.y + "," + d.x + ") rotate(-90)";
else
return "translate(" + d.y + "," + d.x + ") rotate(0)";
});
nodeUpdate.select("circle");
nodeUpdate.select("text");
nodeUpdate.select("rect");
nodeUpdate.select("foreignObject");
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
if (driverTree.hoizMode)
return "translate(" + source.y + "," + source.x + ") rotate(-90)";
else
return "translate(" + source.y + "," + source.x + ") rotate(0)";
})
.remove();
nodeExit.select("circle");
nodeExit.select("text");
nodeExit.select("rect");
nodeExit.select("foreignObject");
// 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 + 100
};
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 to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
var scale = driverTree.zoomListener.scale();
var x = -source.y0;
var y = -source.x0;
if (driverTree.hoizMode) {
d3.select("g").transition()
.duration(1)
.attr("transform", "translate(" + width + "," + (height / 2) / 2 + ")scale(" + scale + ")rotate(90,50,50)");
driverTree.zoomListener.translate([width, (height / 2) / 2]);
} else {
d3.select("g").transition()
.duration(1)
.attr("transform", "translate(" + height / 2 + "," + -width / 2 + ")scale(" + scale + ")");
driverTree.zoomListener.translate([height / 2, -width / 2]);
}
driverTree.zoomListener.scale(scale);
}
// 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);
centerNode(d)
}
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
update(root);
centerNode(root);
Below is my edit function code
// To make the tree editable
handleEdit: function(evt) {
var that = this;
this.editTree = !this.editTree;
this.treeModel.oData.x = 0;
this.treeModel.oData.y = 0;
this.treeModel.oData.x0 = 0;
this.treeModel.oData.y0 = 0;
this.treeModel.oData.children.forEach(function(d){
d.x = 0; d.y = 0; d.x0 = 0; d.y0 = 0;
var children = (d._children == undefined ? d.children : d._children );
children.forEach(function(a){
a.x = 0; a.y = 0; a.x0 = 0; a.y0 = 0;
});
});
this.drawTree();
},

Categories

Resources