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!
Related
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>
I have a vertical D3JS tree where the nodes are rectangles. I would like to append a circle with the italic letter "i" to the top right corner of the rectangle node to indicate that the node has additional information. I managed to append the italic letter "i" only, however, I am not able to put it in a circle and still keep the node clickable. Also I use cloning for downloading the tree as a SVG file and would like to remove this appended circle and the letter "i" from the cloned group element before downloading and printing.
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 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.
// .append("svg:g")
.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);
// IAH 20/01/2019 Filter to put tooltip only on nodes that have information associated with it
nodeEnter.filter(function(d) {
return (d.remark != 'null')
})
.append("text")
.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("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 svgElement = document.querySelector('svg');
var svgWH = svgElement.getBBox();
var canvasWidth = svgWH.width;
var canvasHeight = svgWH.height;
var groupElement = document.getElementById('fg');
groupElement.setAttribute("transform", "translate(0,0)");
const bb = groupElement.getBBox();
// clone the svg to avoid destroying it while appending to the svg namespace
let clonedGroupElement = groupElement.cloneNode(true);
var svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgContent.setAttribute('viewBox', ' ' + bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + bb.height);
svgContent.setAttribute("width", "100%");
svgContent.setAttribute("height", "100%");
svgContent.setAttribute("preserveAspectRatio", "xMidYMid meet");
// try to remove the information icon before downloading
var rmText = document.getElementById('infoText');
console.log(rmText);
svgContent.appendChild(clonedGroupElement); // use the cloned nodes
var dl = document.createElement('a');
document.body.appendChild(dl);
dl.setAttribute("href", svgDataURL(svgContent)); // function svgDataURL expects a node
dl.setAttribute("download", "test.svg");
dl.click();
dl.remove();
svgContent.removeChild(clonedGroupElement);
};
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.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>
I created a dummy attribute ignore-export and was able to use querySelectorAll() to select all elements with that attribute and remove them. Note that the node.remove() function is not supported in IE, but there is a polyfill available at MDN.
I didn't have any problems with the nodes not responding when I drew the circle, but if you experience it, let me know.
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 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.
// .append("svg:g")
.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);
// IAH 20/01/2019 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("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 svgElement = document.querySelector('svg');
var svgWH = svgElement.getBBox();
var canvasWidth = svgWH.width;
var canvasHeight = svgWH.height;
var groupElement = document.getElementById('fg');
groupElement.setAttribute("transform", "translate(0,0)");
const bb = groupElement.getBBox();
// clone the svg to avoid destroying it while appending to the svg namespace
let clonedGroupElement = groupElement.cloneNode(true);
var svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgContent.setAttribute('viewBox', ' ' + bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + bb.height);
svgContent.setAttribute("width", "100%");
svgContent.setAttribute("height", "100%");
svgContent.setAttribute("preserveAspectRatio", "xMidYMid meet");
// try to remove the information icon before downloading
clonedGroupElement.querySelectorAll('[export-ignore]').forEach(function(node) {
node.remove();
});
svgContent.appendChild(clonedGroupElement); // use the cloned nodes
var dl = document.createElement('a');
document.body.appendChild(dl);
dl.setAttribute("href", svgDataURL(svgContent)); // function svgDataURL expects a node
dl.setAttribute("download", "test.svg");
dl.click();
dl.remove();
svgContent.removeChild(clonedGroupElement);
};
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
text-anchor: middle;
}
.link {
/*
fill: none;
stroke: #ccc;
stroke-width: 2px;*/
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
I have a d3js tree which I am able to download, I want to add a title, Arabesque background and Arabesque border from external .svg files for a better visual effect.
I have 3 external files: title_3.png (later I will use an svg equivalent), background-F4EED7.svg, and border_1.svg. I need to center the title to the middle of the graph and allow the height of the title to be resized according to the length and depth of the generated tree.
What would be the best way of achieving this?
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];
});
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("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 svgElement = document.querySelector('svg');
var svgWH = svgElement.getBBox();
var canvasWidth = svgWH.width;
var canvasHeight = svgWH.height;
var groupElement = document.getElementById('fg');
groupElement.setAttribute("transform", "translate(0,0)");
const bb = groupElement.getBBox();
//Title of the tree
var svgimg = document.createElementNS('http://www.w3.org/2000/svg', 'image');
svgimg.setAttributeNS(null, 'height', "30");
svgimg.setAttributeNS(null, 'width', bb.width / 2);
svgimg.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'http://localhost/images/Title3.png');
svgimg.setAttributeNS(null, 'x', '-' + rectW / 2);
svgimg.setAttributeNS(null, 'y', '-' + rectH / 2);
svgimg.setAttributeNS(null, 'visibility', 'visible');
//Background SVG pattern
svgBgImg.style.backgroundImage = "url('http://localhost/images/background-F4EED7.svg')";
// clone the svg to avoid destroying it while appending to the svg namespace
let clonedGroupElement = groupElement.cloneNode(true);
var svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgContent.setAttribute('viewBox', ' ' + bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + bb.height);
svgContent.setAttribute("width", "100%");
svgContent.setAttribute("height", "100%");
svgContent.setAttribute("preserveAspectRatio", "xMidYMid meet");
// try to remove the information icon before downloading
clonedGroupElement.querySelectorAll('[export-ignore]').forEach(function(node) {
node.remove();
});
clonedGroupElement.style.borderImageSource = "url('http://localhost/images/border_1.svg')";
clonedGroupElement.append(svgimg);
clonedGroupElement.style.borderImageSource = "url('http://localhost/images/border_1.svg')";
svgContent.appendChild(clonedGroupElement); // use the cloned nodes
var dl = document.createElement('a');
document.body.appendChild(dl);
dl.setAttribute("href", svgDataURL(svgContent)); // function svgDataURL expects a node
dl.setAttribute("download", "test.svg");
dl.click();
dl.remove();
svgContent.removeChild(clonedGroupElement);
};
<style>.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;*/
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.4/d3.min.js"></script>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tree Example</title>
</head>
<body>
I am try to upgrade this working version of d3.js flow-chart in jsfiddle:- https://jsfiddle.net/zgv9ajn4/7/ from Version 3 to Version 4/5.
I went through the documentation and the console error messages and rectified the errors one after the other. Below are some other things I also had in mind during this version upgrade.
I am not able to see any diagram after making the changes, I don't even see any console errors too, Only an empty screen.
Here is the jsfiddle for the version4 :- http://jsfiddle.net/5d02grL7/1/
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
function generateEmptyDecisionBox(condition) {
return condition === 'False' ? [{
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}];
}
function generateEmptyActionBox(condition) {
return condition === 'False' ? [{
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}];
}
var selectedNode;
var selectedLink;
var treeData = {
"name": "Root",
"type": "decision",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "decision",
"condition": "false",
"value": "4",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4"
}],
},
}],
},
}
]
};
var i = 0,
duration = 1000,
rectW = 120,
rectH = 60;
var treeMap = d3.tree().size([150, 90]);
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(d) {
console.log("linkFunc", d);
var source = {
x: d.parent.x + rectW / 2,
y: d.parent.y + (rectH / 2)
};
var target = {
x: d.x + (rectW / 2),
y: d.y + 3,
};
// 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 && d.data.type) {
// Child is to the right of the parent
result += ' H' + (inflection.x - radius);
} else if (d.data.type) {
result += ' H' + (inflection.x + radius);
} else {
return;
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
}
// END OF LINK FUNC //
/* var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + rectW / 2, d.y + rectH / 2];
}); */
// DRAW TREE //
var svg = d3.select(".tree-diagram").append("svg").attr("width", 1000).attr("height", 1000)
.call(zm = d3.zoom().scaleExtent([1, 3]).on("zoom", redraw)).append("g")
.attr("transform", "translate(" + 350 + "," + 20 + ")");
// ADD ARROW TO THE BOTTOM POINTING TO THE NEXT DECISION.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
//necessary so that zoom knows where to zoom and unzoom from
/* zm.translate([350, 20]); */
root = d3.hierarchy(treeData, function(d) {
return d.children;
});
root.x0 = 0;
root.y0 = 0;
update(root);
d3.select(".tree-diagram").style("height", "1000px");
// END OF DRAW TREEE //
function update(source) {
var treeData = treeMap(root);
const treeRoot = d3.hierarchy(root);
d3.tree(treeRoot);
// var treeData = treeMap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
console.log(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 90;
});
// 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('transform', 'translate(' + source.x0 + ', ' + source.y0 + ')')
.attr("class", "node")
.on("click", click)
// .on("blur", onNodeBlur);
nodeEnter.append('path')
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.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('path.myPaths')
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.style("fill", function(d) {
return "lightsteelblue";
});
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("path")
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeExit.select("text");
// Update the links…
var link = svg.selectAll(".link")
.data(links, function(d) {
console.log(d);
return d.id;
}).classed('link1', true);
// Enter any new links at the parent's previous position.
var linkEnter = link.enter()
.insert("g", "g")
.attr("class", "link");
linkEnter.append('path')
.on('click', function(d, i) {
selectedLink = d;
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x;
var y;
if (d.parent.x < d.x) {
// Child is to the right of the parent
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
} else {
x = bbox.x;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
}
})
.on('blur', function(d, i) {
plusButton
.classed('hide', true);
}).attr("marker-end", "url(#end)");
// Add Link Texts.
linkEnter.append('text');
link.select('path')
.attr("d", linkFunc);
link.select('text')
.text(function(d, i) {
if (d.parent.x < d.x) {
return 'True';
} else {
return 'False';
}
})
.attr('transform', function(d) {
console.log(d);
if (d.parent.x < d.x && d.data.type) {
console.log("comes in here for source < target");
return 'translate(' + (d.x + rectW / 2) + ',' + (d.parent.y + rectH) + ')';
} else if (d.data.type) {
return 'translate(' + (d.parent.x + rectW / 2) + ',' + (d.y + rectH) + ')';
} else {
return;
}
});
//LinkUpdate
var linkUpdate = linkEnter.merge(link);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition links to their new position.
// 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;
});
}
// ON CLICK OF NODES
function click(d) {
if (d.type === 'action') {
return;
}
selectedNode = d;
var m = d.x;
var h = d.y;
var m = d.x + 110;
var h = d.y + 35;
diamondImage
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x + 110;
var h = d.y;
rectangleShape
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y + 35;
diamondImageFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y;
rectangleShapeFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
}
/* function onNodeBlur(){
diamondImage
.classed('hide', true);
rectangleShape
.classed('hide', true);
diamondImageFalse
.classed('hide',true);
rectangleShapeFalse
.classed('hide',true);
}
*/
//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 + ")");
}
// oN CALL
function addElement(d, truthy) {
console.log(d);
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
function drawDiamond(centroid) {
// Start at the top
console.log(centroid);
console.log("rectH", rectH, rectW)
// 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
console.log(centroid);
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';
console.log(result);
return result;
}
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("hello");
/* addElement(selectedLink.source); */
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
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('conditionImage', true)
.classed('hide', true)
.on('click', function() {
removeAllButtonElements();
})
rectangleShape
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('conditionSvg', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'True');
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
console.log("rectangle clicked");
removeAllButtonElements();
})
rectangleShapeFalse
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'False');
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 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);
}
.node {
cursor: pointer;
outline: none !important;
}
.node text {
font: 10px sans-serif;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
/* outline: none; */
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.conditionalSvg {
/* outline: none; */
display: none;
}
.hide {
/* display: none; */
opacity: 0 !important;
/* pointer-events: none; */
}
.link:hover {
outline: none !important;
cursor: pointer;
stroke-width: 3px;
}
.link path {
/* outline: none !important; */
fill: none;
stroke: darkgray;
stroke-width: 2px;
}
.link path:hover {
cursor: pointer;
stroke-width: 4px;
}
.link text {
font: 10px sans-serif;
}
.colorBlue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div class="tree-diagram"></div>
When I write v4 code, the steps I take in my head are:
Select the nodes;
Apply the data;
Get the .exit() selection and apply their specific changes;
Get the .enter() selection and apply their specific changes. Use this to append nodes and set constant attributes (that do not rely on d);
Get the existing selection, apply their specific changes;
Then .merge() with the .enter() selection and apply common changes - those that do rely on d.
For your links, you forgot to do the last step; adding the merge() line made the links be drawn.
// Add Link Texts.
linkEnter.append('text');
// Merge the new and the existing links before setting `d` and `text` on all of them
link = linkEnter.merge(link);
link.select('path')
.attr("d", linkFunc);
Now, for your nodes, the data structure actually changed. All your metadata (name, type, etc) were now no longer properties of d, but of d.data. So changing if (d.type === 'decision') { to if (d.data.type === 'decision') { made it work.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120,
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
function generateEmptyDecisionBox(condition) {
return condition === 'False' ? [{
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}];
}
function generateEmptyActionBox(condition) {
return condition === 'False' ? [{
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}];
}
var selectedNode;
var selectedLink;
var treeData = {
"name": "Root",
"type": "decision",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "decision",
"condition": "false",
"value": "4",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4",
}],
},
}],
},
},
],
};
var i = 0,
duration = 1000,
rectW = 120,
rectH = 60;
var treeMap = d3.tree()
.size([150, 90]);
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(d) {
console.log("linkFunc", d);
var source = {
x: d.parent.x + rectW / 2,
y: d.parent.y + (rectH / 2),
};
var target = {
x: d.x + (rectW / 2),
y: d.y + 3,
};
// 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 && d.data.type) {
// Child is to the right of the parent
result += ' H' + (inflection.x - radius);
} else if (d.data.type) {
result += ' H' + (inflection.x + radius);
} else {
return;
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
};
// END OF LINK FUNC //
/* var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + rectW / 2, d.y + rectH / 2];
}); */
// DRAW TREE //
var svg = d3.select(".tree-diagram")
.append("svg")
.attr("width", 1000)
.attr("height", 1000)
.call(zm = d3.zoom()
.scaleExtent([1, 3])
.on("zoom", redraw))
.append("g")
.attr("transform", "translate(" + 350 + "," + 20 + ")");
// ADD ARROW TO THE BOTTOM POINTING TO THE NEXT DECISION.
svg.append("svg:defs")
.selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter()
.append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
//necessary so that zoom knows where to zoom and unzoom from
/* zm.translate([350, 20]); */
root = d3.hierarchy(treeData, function(d) {
return d.children;
});
root.x0 = 0;
root.y0 = 0;
update(root);
d3.select(".tree-diagram")
.style("height", "1000px");
// END OF DRAW TREEE //
function update(source) {
var treeData = treeMap(root);
const treeRoot = d3.hierarchy(root);
d3.tree(treeRoot);
// var treeData = treeMap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants()
.slice(1);
console.log(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 90;
});
// 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('transform', 'translate(' + source.x0 + ', ' + source.y0 + ')')
.attr("class", "node")
.on("click", click);
// .on("blur", onNodeBlur);
nodeEnter.append('path')
.attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.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('path.myPaths')
.attr("d", function(d) {
if (d.data.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.data.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z';
}
});
var nodeExit = node.exit()
.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// Update the links…
var link = svg.selectAll(".link")
.data(links, function(d) {
return d.id;
})
.classed('link1', true);
// Enter any new links at the parent's previous position.
var linkEnter = link.enter()
.insert("g", "g")
.attr("class", "link");
linkEnter.append('path')
.on('click', function(d, i) {
selectedLink = d;
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x;
var y;
if (d.parent.x < d.x) {
// Child is to the right of the parent
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
} else {
x = bbox.x;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
}
})
.on('blur', function(d, i) {
plusButton
.classed('hide', true);
})
.attr("marker-end", "url(#end)");
// Add Link Texts.
linkEnter.append('text');
// Merge the new and the existing links before setting `d` and `text` on all of them
link = linkEnter.merge(link);
link.select('path')
.attr("d", linkFunc);
link.select('text')
.text(function(d, i) {
if (d.parent.x < d.x) {
return 'True';
} else {
return 'False';
}
})
.attr('transform', function(d) {
console.log(d);
if (d.parent.x < d.x && d.data.type) {
console.log("comes in here for source < target");
return 'translate(' + (d.x + rectW / 2) + ',' + (d.parent.y + rectH) + ')';
} else if (d.data.type) {
return 'translate(' + (d.parent.x + rectW / 2) + ',' + (d.y + rectH) + ')';
} else {
return;
}
});
//LinkUpdate
var linkUpdate = linkEnter.merge(link);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition links to their new position.
// 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;
});
}
// ON CLICK OF NODES
function click(d) {
if (d.data.type === 'action') {
return;
}
selectedNode = d;
var m = d.x;
var h = d.y;
var m = d.x + 110;
var h = d.y + 35;
diamondImage
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x + 110;
var h = d.y;
rectangleShape
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y + 35;
diamondImageFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y;
rectangleShapeFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
}
/* function onNodeBlur(){
diamondImage
.classed('hide', true);
rectangleShape
.classed('hide', true);
diamondImageFalse
.classed('hide',true);
rectangleShapeFalse
.classed('hide',true);
}
*/
//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 + ")");
}
// oN CALL
function addElement(d, truthy) {
console.log(d);
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
function drawDiamond(centroid) {
// Start at the top
console.log(centroid);
console.log("rectH", rectH, rectW);
// 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
console.log(centroid);
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';
console.log(result);
return result;
}
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("hello");
/* addElement(selectedLink.source); */
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
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('conditionImage', true)
.classed('hide', true)
.on('click', function() {
removeAllButtonElements();
});
rectangleShape
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('conditionSvg', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'True');
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
console.log("rectangle clicked");
removeAllButtonElements();
});
rectangleShapeFalse
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'False');
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 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);
}
// draw elements end ..
.node {
cursor: pointer;
outline: none !important;
}
.node text {
font: 10px sans-serif;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
/* outline: none; */
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.conditionalSvg {
/* outline: none; */
display: none;
}
.hide {
/* display: none; */
opacity: 0 !important;
/* pointer-events: none; */
}
.link:hover {
outline: none !important;
cursor: pointer;
stroke-width: 3px;
}
.link path {
/* outline: none !important; */
fill: none;
stroke: darkgray;
stroke-width: 2px;
}
.link path:hover {
cursor: pointer;
stroke-width: 4px;
}
.link text {
font: 10px sans-serif;
}
.colorBlue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div class="tree-diagram"></div>
I am working on getting a treeview working with some conditional logics. I currently have the following working fiddle: https://jsfiddle.net/zgv9ajn4/4/
From the jsfiddle you can see the below image. On selecting the node with the name "division" I am showing certain orange svgs, and on click of the right-bottom svg I am adding a new child node to the "division" node.
Before Adding child Node and on clicking of the node "division":
After Adding the Child Node:-
As you can see there are some hanging links/nodes and they are not getting removed.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
function generateEmptyDecisionBox(condition) {
return condition === 'False' ? [{
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}];
}
var selectedNode;
var selectedLink;
var root = {
"name": "Root",
"type": "decision",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "decision",
"condition": "false",
"value": "4",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4"
}],
},
}],
},
}
]
};
var i = 0,
duration = 500,
rectW = 120,
rectH = 60;
var tree = d3.layout.tree().nodeSize([150, 90]);
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(d) {
console.log("linkFunc", d);
var source = {
x: d.source.x + rectW / 2,
y: d.source.y + (rectH / 2)
};
var target = {
x: d.target.x + (rectW / 2),
y: d.target.y + 3,
};
// 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 && d.target.type) {
// Child is to the right of the parent
result += ' H' + (inflection.x - radius);
} else if (d.target.type) {
result += ' H' + (inflection.x + radius);
} else {
return;
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
}
// END OF LINK FUNC //
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + rectW / 2, d.y + rectH / 2];
});
// DRAW TREE //
var svg = d3.select(".tree-diagram").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 + ")");
// ADD ARROW TO THE BOTTOM POINTING TO THE NEXT DECISION.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);
root.x0 = 0;
root.y0 = 0;
update(root);
d3.select(".tree-diagram").style("height", "1000px");
// END OF DRAW TREEE //
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 * 90;
});
// 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('transform', 'translate(' + source.x0 + ', ' + source.y0 + ')')
.attr("class", "node")
.on("click", click)
// .on("blur", onNodeBlur);
nodeEnter.append('path')
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + (d.x) + "," + (d.y) + ")";
});
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("path")
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeExit.select("text");
// Update the links…
var link = svg.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
}).classed('link1', true);
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert("g", "g")
.attr("class", "link");
linkEnter.append('path')
.attr("d", linkFunc)
.on('click', function(d, i) {
selectedLink = d;
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x;
var y;
if (d.source.x < d.target.x) {
// Child is to the right of the parent
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
} else {
x = bbox.x;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
}
})
.on('blur', function(d, i) {
plusButton
.classed('hide', true);
}).attr("marker-end", "url(#end)");
// Add Link Texts.
linkEnter.append('text')
.text(function(d, i) {
if (d.source.x < d.target.x) {
return 'True';
} else {
return 'False';
}
}).attr('transform', function(d) {
console.log(d);
/* var bbox = this.getBBox();
x= bbox.x + bbox.width;
y= bbox.y; */
if (d.source.x < d.target.x && d.target.type) {
console.log("comes in here for source < target");
return 'translate(' + (d.source.x + rectW) + ',' + (d.source.y + rectH) + ')';
} else if (d.target.type) {
return 'translate(' + (d.source.x - rectW / 2) + ',' + (d.source.y + rectH) + ')';
} else {
return;
}
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition links to their new position.
// 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;
});
}
// ON CLICK OF NODES
function click(d) {
if (d.type === 'action') {
return;
}
selectedNode = d;
var m = d.x;
var h = d.y;
var m = d.x + 110;
var h = d.y + 35;
diamondImage
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x + 110;
var h = d.y;
rectangleShape
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y + 35;
diamondImageFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y;
rectangleShapeFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
}
/* function onNodeBlur(){
diamondImage
.classed('hide', true);
rectangleShape
.classed('hide', true);
diamondImageFalse
.classed('hide',true);
rectangleShapeFalse
.classed('hide',true);
}
*/
//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 + ")");
}
// oN CALL
function addElement(d, truthy) {
console.log(d);
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
function drawDiamond(centroid) {
// Start at the top
console.log(centroid);
console.log("rectH", rectH, rectW)
// 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
console.log(centroid);
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';
console.log(result);
return result;
}
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("hello");
/* addElement(selectedLink.source); */
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
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('conditionImage', true)
.classed('hide', true)
.on('click', function() {
removeAllButtonElements();
})
rectangleShape
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('conditionSvg', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'True');
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
console.log("rectangle clicked");
removeAllButtonElements();
})
rectangleShapeFalse
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'False');
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 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);
}
// draw elements end ..
.node {
cursor: pointer;
outline: none !important;
}
.node text {
font: 10px sans-serif;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
/* outline: none; */
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.conditionalSvg {
outline: none;
display: none;
}
.hide {
display: none;
opacity: 0 !important;
pointer-events: none;
}
.link:hover {
ouline: none !important;
cursor: pointer;
stroke-width: 3px;
}
.link path {
outline: none !important;
fill: none;
stroke: darkgray;
stroke-width: 2px;
}
.link path:hover {
cursor: pointer;
stroke-width: 4px;
}
.link text {
font: 10px sans-serif;
}
.colorBlue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>
Although the new node is being added and the old nodes are being transitioned to the latest positions, the old nodes/links are not being removed.
Note that your nodes are moving fine, just the links are not. That is because you find them using path.link, but the real structure is g.link path, since you first add a g element with class link, and then a path. You could fix that by changing path.link to g.link, but then you need to select the path and text separately.
I fixed this by calling .attr('d', linkFunc) on all links, not just the new ones, and by calling .text() and .attr('transform' on all texts.
Finally, I changed the text transform to use the x of the target, since a target determines where the line goes vertical, not the source.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
function generateEmptyDecisionBox(condition) {
return condition === 'False' ? [{
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}];
}
var selectedNode;
var selectedLink;
var root = {
"name": "Root",
"type": "decision",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "decision",
"condition": "false",
"value": "4",
"children": {
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"children": [{
"name": "distinction",
"type": "action",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"condition": "false",
"value": "4"
}],
},
}],
},
}
]
};
var i = 0,
duration = 500,
rectW = 120,
rectH = 60;
var tree = d3.layout.tree().nodeSize([150, 90]);
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(d) {
console.log("linkFunc", d);
var source = {
x: d.source.x + rectW / 2,
y: d.source.y + (rectH / 2)
};
var target = {
x: d.target.x + (rectW / 2),
y: d.target.y + 3,
};
// 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 && d.target.type) {
// Child is to the right of the parent
result += ' H' + (inflection.x - radius);
} else if (d.target.type) {
result += ' H' + (inflection.x + radius);
} else {
return;
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
}
// END OF LINK FUNC //
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + rectW / 2, d.y + rectH / 2];
});
// DRAW TREE //
var svg = d3.select(".tree-diagram").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 + ")");
// ADD ARROW TO THE BOTTOM POINTING TO THE NEXT DECISION.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);
root.x0 = 0;
root.y0 = 0;
update(root);
d3.select(".tree-diagram").style("height", "1000px");
// END OF DRAW TREEE //
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 * 90;
});
// 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('transform', 'translate(' + source.x0 + ', ' + source.y0 + ')')
.attr("class", "node")
.on("click", click)
// .on("blur", onNodeBlur);
nodeEnter.append('path')
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + (d.x) + "," + (d.y) + ")";
});
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("path")
.attr("d", function(d) {
if (d.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z'
}
}).attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeExit.select("text");
// Update the links…
var link = svg.selectAll(".link")
.data(links, function(d) {
return d.target.id;
}).classed('link1', true);
// Enter any new links at the parent's previous position.
var linkEnter = link.enter()
.insert("g", "g")
.attr("class", "link");
linkEnter.append('path')
.on('click', function(d, i) {
selectedLink = d;
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x;
var y;
if (d.source.x < d.target.x) {
// Child is to the right of the parent
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
} else {
x = bbox.x;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
}
})
.on('blur', function(d, i) {
plusButton
.classed('hide', true);
}).attr("marker-end", "url(#end)");
// Add Link Texts.
linkEnter.append('text');
link.select('path')
.attr("d", linkFunc);
link.select('text')
.text(function(d, i) {
if (d.source.x < d.target.x) {
return 'True';
} else {
return 'False';
}
})
.attr('transform', function(d) {
console.log(d);
if (d.source.x < d.target.x && d.target.type) {
console.log("comes in here for source < target");
return 'translate(' + (d.target.x + rectW / 2) + ',' + (d.source.y + rectH) + ')';
} else if (d.target.type) {
return 'translate(' + (d.target.x + rectW / 2) + ',' + (d.source.y + rectH) + ')';
} else {
return;
}
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition links to their new position.
// 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;
});
}
// ON CLICK OF NODES
function click(d) {
if (d.type === 'action') {
return;
}
selectedNode = d;
var m = d.x;
var h = d.y;
var m = d.x + 110;
var h = d.y + 35;
diamondImage
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x + 110;
var h = d.y;
rectangleShape
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y + 35;
diamondImageFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
var m = d.x - 20;
var h = d.y;
rectangleShapeFalse
.attr('transform', 'translate(' + m + ', ' + h + ')')
.classed('hide', false);
}
/* function onNodeBlur(){
diamondImage
.classed('hide', true);
rectangleShape
.classed('hide', true);
diamondImageFalse
.classed('hide',true);
rectangleShapeFalse
.classed('hide',true);
}
*/
//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 + ")");
}
// oN CALL
function addElement(d, truthy) {
console.log(d);
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
function drawDiamond(centroid) {
// Start at the top
console.log(centroid);
console.log("rectH", rectH, rectW)
// 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
console.log(centroid);
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';
console.log(result);
return result;
}
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("hello");
/* addElement(selectedLink.source); */
console.log("Clicked on Diamond");
console.log("set hide to true");
removeAllButtonElements();
});
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('conditionImage', true)
.classed('hide', true)
.on('click', function() {
removeAllButtonElements();
})
rectangleShape
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('conditionSvg', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'True');
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
console.log("rectangle clicked");
removeAllButtonElements();
})
rectangleShapeFalse
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
console.log("Clicked on Diamond");
console.log("set hide to true");
addElement(selectedNode, 'False');
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 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);
}
// draw elements end ..
.node {
cursor: pointer;
outline: none !important;
}
.node text {
font: 10px sans-serif;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
/* outline: none; */
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.conditionalSvg {
outline: none;
display: none;
}
.hide {
display: none;
opacity: 0 !important;
pointer-events: none;
}
.link:hover {
ouline: none !important;
cursor: pointer;
stroke-width: 3px;
}
.link path {
outline: none !important;
fill: none;
stroke: darkgray;
stroke-width: 2px;
}
.link path:hover {
cursor: pointer;
stroke-width: 4px;
}
.link text {
font: 10px sans-serif;
}
.colorBlue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>