Reputation: 400
I am working on a D3.js V4/5 implementation of building a vertical flow-chart. I am adding a new node to the flow-chart's decision box by clicking on a "decision" box and it's corresponding "diamond"/"rect" shape.
From Mike Bostocks answer here for adding/removing nodes :- https://github.com/d3/d3-hierarchy/issues/139 I've followed step 1 which is :- "Derive an entirely new tree by calling d3.hierarchy after modifying your data (or passing in a different children accessor function to do filtering".
So when a user tries to add a new node I am modifying the actual tree/children , computing the hierarchy and calling the update() method. Something like below
JS Fiddle:- http://jsfiddle.net/rs3owt6g/6/
function updateAfterAddingNode() {
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = height/2;
root.y0 = 0;
update(root);
}
Actual Issue:
Everything seems to be working fine until the point when I try to add 2 decision nodes to a decision node and more decision nodes underneath it. The Links connecting the nodes passes through the other sibling node.
To replicate the issue in fiddle:
To add a new node click on the orange diamond which appears on click of a node.
Add 2 sibling nodes ( 1 action/rect and 1 decision node) to left and right respectively. For the decision node add 2 decision nodes and for these 2 decision nodes add another 2 decison nodes.
Below picture can give you a clarity. As you can see, the left highlighted path goes through the "New Action" node instead of staying put at the earlier location when all nodes are added. Also, the distance between siblings grows a lot more when more children are added.
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",
"id": "root",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"id": "child1",
"children": [{
"name": "distinction",
"type": "action",
"id": "child2",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"id": "child3",
"condition": "False",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"id": "child33",
"children":[{
"condition": "True",
"name": "division1",
"type": "decision",
"value": "a-b",
"id": "child44",
"children":[{
"condition": "True",
"name": "division1.1",
"type": "decision",
"value": "a-b",
"id": "child599",
"children":[{
"condition": "True",
"name": "division1.1.34",
"type": "decision",
"value": "a-b",
"id": "child234234",
"children":[{
"condition": "True",
"name": "division1.1.434",
"type": "decision",
"value": "a-b",
"id": "child35343",
"children":[],
}],
},{
"condition": "True",
"name": "division1.1.2",
"type": "decision",
"value": "a-b",
"id": "child77",
"children":[{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child1222",
"children":[],
},{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child66",
"children":[],
}],
}],
},{
"condition": "True",
"name": "NODE HAVING OVERLAP ISSUE",
"type": "decision",
"value": "a-b",
"id": "child9090",
"children":[],
}],
},
{
"condition": "True",
"name": "division2",
"type": "decision",
"value": "a-b",
"id": "child55",
"children":[{
"condition": "True",
"name": "division2.1",
"type": "decision",
"value": "a-b",
"id": "child88",
"children":[{
"condition": "True",
"name": "division2.1.1",
"type": "decision",
"value": "a-b",
"id": "child99",
"children":[],
}],
}],
},
],
},
],
}];
var i = 0,
duration = 1000,
rectW = 120,
rectH = 60;
var treeMap = d3.tree()
.nodeSize([150, 180]);
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(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 //
const zoomSvg = d3.select('.tree-diagram')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
const svg = zoomSvg.append('g')
.attr('transform', 'translate(' + 300 + ',' + 20 + ')');
const attachZoom = d3.select('svg');
attachZoom.call(d3.zoom().on('zoom', () => {
zoomSvg.attr('transform', d3.event.transform)
}))
// 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[0], 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) {
const 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);
// 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.data.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.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';
}
})
.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.data.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) {
if (d.parent.x < d.x && d.data.type) {
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;
if (!(d.data.children && d.data.children[0] && Object.keys(d.data.children[0]).length)) {
diamondImageFalse
.attr('transform', 'translate(' + (d.x - 20) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShapeFalse.attr('transform', 'translate(' + (d.x - 20) + ', ' + d.y + ')').classed('hide', false);
}
if (!(d.data.children && d.data.children[1] && Object.keys(d.data.children[1]).length)) {
diamondImage
.attr('transform', 'translate(' + (d.x + 110) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShape.attr('transform', 'translate(' + (d.x + 110) + ', ' + d.y + ')').classed('hide', false);
}
}
// oN CALL
function addElement(d, truthy) {
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
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 plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
/* addElement(selectedLink.source); */
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() {
addActionOrDecision(selectedNode, 'action', 'True');
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() {
addActionOrDecision(selectedNode, 'decision', '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() {
addActionOrDecision(selectedNode, 'action', 'False');
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() {
addActionOrDecision(selectedNode, 'decision', 'False');
// 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);
}
function addActionOrDecision(selectedNode, nodeType, conditionType) {
const parentNodeId = selectedNode.parent.data.id;
const selectedNodeId = selectedNode.data.id;
// find the selected node from the actual treeData
const foundRule = getNodeFromNodeId(treeData, selectedNodeId);
const newRuleId = Math.random();
const newNodeToAdd = {
"condition": conditionType,
"name": nodeType === 'decision' ? 'New Decision' : 'New Action',
"type": nodeType,
"value": "",
"id": newRuleId,
"parent": parentNodeId,
"children": [],
};
const clonedNewNode = { ...newNodeToAdd
};
if (conditionType === 'False' && foundRule.children) {
// foundRule.children[0] = newNodeToAdd;
foundRule.children.splice(0, 1, clonedNewNode);
if (!(foundRule.children[1] && Object.keys(foundRule.children[1]))) {
foundRule.children[1] = {};
}
} else {
// foundRule.children[1] = newNodeToAdd;
foundRule.children.splice(1, 1, clonedNewNode);
if (!(foundRule.children[0] && Object.keys(foundRule.children[0]))) {
founRule.children[0] = {};
}
}
// find the node and add a child to it.
updateTree();
}
function updateTree() {
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
update(root);
}
function getNodeFromNodeId(nodes, nodeId) {
for (const node of nodes) {
const currNode = node;
if (currNode) {
if (currNode.id === nodeId) {
return currNode;
} else if (currNode.children) {
const childResult = getNodeFromNodeId(currNode.children, nodeId);
if (childResult) {
return childResult;
}
}
}
}
return null;
}
.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>
Also just wanted to take a confirmation on the way the nodes are being added in the addActionOrDecision
let me know if that's a good way to do it. I am basically finding the parent node from the actual data and adding a copy of a newly created node to the parent node's children ( in the actual treeData ).
Edit :- This is how it looks when I keep adding more nodes , the left side's nodes are mixed up with the right side one's and the nodes/links are messed up.
UX on how it should probably look like :- (Add a bend to the link and adjust the entire tree to the left or right accordingly ? )
EDIT :-
Modified JsFiddle to show the issue during the initial launch :-
http://jsfiddle.net/c3yz4bj0/3/
Upvotes: 1
Views: 918
Reputation: 13129
I solved this by writing a custom tree.separation()
function. It's very similar to the default one, but differs in that it places nodes farther apart if only one of the two nodes has any children. This prevents the overlap. Normally, if two nodes both have children, those children will be the reason why they are not overlapping, but sometimes this doesn't 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",
"id": "root",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"id": "child1",
"children": [{
"name": "distinction",
"type": "action",
"id": "child2",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"id": "child3",
"condition": "False",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"id": "child33",
"children": [{
"condition": "True",
"name": "division1",
"type": "decision",
"value": "a-b",
"id": "child44",
"children": [{
"condition": "True",
"name": "division1.1",
"type": "decision",
"value": "a-b",
"id": "child599",
"children": [{
"condition": "True",
"name": "division1.1.34",
"type": "decision",
"value": "a-b",
"id": "child234234",
"children": [{
"condition": "True",
"name": "division1.1.434",
"type": "decision",
"value": "a-b",
"id": "child35343",
"children": [],
}],
}, {
"condition": "True",
"name": "division1.1.2",
"type": "decision",
"value": "a-b",
"id": "child77",
"children": [{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child1222",
"children": [],
}, {
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child66",
"children": [],
}],
}],
}, {
"condition": "True",
"name": "NODE HAVING OVERLAP ISSUE",
"type": "decision",
"value": "a-b",
"id": "child9090",
"children": [{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child909090",
"children": [],
}],
}],
},
{
"condition": "True",
"name": "division2",
"type": "decision",
"value": "a-b",
"id": "child55",
"children": [{
"condition": "True",
"name": "division2.1",
"type": "decision",
"value": "a-b",
"id": "child88",
"children": [{
"condition": "True",
"name": "division2.1.1",
"type": "decision",
"value": "a-b",
"id": "child99",
"children": [],
}],
}],
},
],
},
],
}];
var i = 0,
duration = 1000,
rectW = 120,
rectH = 60;
var treeMap = d3.tree()
.nodeSize([140, 120])
.separation(function(a, b) {
// If they have the same parent
if(a.parent === b.parent) {
// and are either both leaf nodes or both not leaf nodes
// or have only one child (which results in a straight line down)
if((a.children === undefined || a.children.length <= 1) ===
(b.children === undefined || b.children.length <= 1)) {
return 1;
}
// else, increase the size between them
return 2;
}
// If they have the same depth, mark them as such so we can avoid them later
if(a.depth === b.depth) {
a.data.avoidRight = b;
b.data.avoidLeft = a;
}
return 2;
});
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(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(!d.source.data.type) {
return;
}
if(source.x < target.x) {
// Child is to the right of the parent
if(d.source.data.avoidRight !== undefined && inflection.x > d.source.data.avoidRight.x) {
// There is some node that we should try to avoid first
result += ' H' + (d.source.data.avoidRight.x - 2 * radius);
result += ' V' + (d.source.data.avoidRight.y + rectH + radius);
inflection.y = d.source.data.avoidLeft.y + rectH + radius;
}
result += ' H' + (inflection.x - radius);
} else {
// Child is to the left of parent
if(d.source.data.avoidLeft !== undefined && inflection.x < d.source.data.avoidLeft.x + rectW) {
result += ' H' + (d.source.data.avoidLeft.x + rectW + 2 * radius);
result += ' V' + (d.source.data.avoidLeft.y + rectH + radius);
inflection.y = d.source.data.avoidLeft.y + rectH + radius;
}
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;
};
// END OF LINK FUNC //
const zoomSvg = d3.select('.tree-diagram')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
const svg = zoomSvg.append('g')
.attr('transform', 'translate(' + 300 + ',' + 20 + ')');
const attachZoom = d3.select('svg');
attachZoom.call(d3.zoom().on('zoom', () => {
zoomSvg.attr('transform', d3.event.transform)
}))
// 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[0], 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) {
const treeData = treeMap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.links();
// 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.data.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.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';
}
})
.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.source.data.id + " " + d.target.data.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.target.x < d.source.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.source.x < d.target.x) {
return 'True';
} else {
return 'False';
}
})
.attr('transform', function(d) {
if (d.source.x < d.target.x && d.target.data.type) {
return 'translate(' + (d.target.x + rectW / 2) + ',' + (d.source.y + rectH) + ')';
} else {
return null;
}
});
//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;
if (!(d.data.children && d.data.children[0] && Object.keys(d.data.children[0]).length)) {
diamondImageFalse
.attr('transform', 'translate(' + (d.x - 20) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShapeFalse.attr('transform', 'translate(' + (d.x - 20) + ', ' + d.y + ')').classed('hide', false);
}
if (!(d.data.children && d.data.children[1] && Object.keys(d.data.children[1]).length)) {
diamondImage
.attr('transform', 'translate(' + (d.x + 110) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShape.attr('transform', 'translate(' + (d.x + 110) + ', ' + d.y + ')').classed('hide', false);
}
}
// oN CALL
function addElement(d, truthy) {
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
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 plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
/* addElement(selectedLink.source); */
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() {
addActionOrDecision(selectedNode, 'action', 'True');
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() {
addActionOrDecision(selectedNode, 'decision', '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() {
addActionOrDecision(selectedNode, 'action', 'False');
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() {
addActionOrDecision(selectedNode, 'decision', 'False');
// 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);
}
function addActionOrDecision(selectedNode, nodeType, conditionType) {
const parentNodeId = selectedNode.parent.data.id;
const selectedNodeId = selectedNode.data.id;
// find the selected node from the actual treeData
const foundRule = getNodeFromNodeId(treeData, selectedNodeId);
const newRuleId = Math.random();
const newNodeToAdd = {
"condition": conditionType,
"name": nodeType === 'decision' ? 'New Decision' : 'New Action',
"type": nodeType,
"value": "",
"id": newRuleId,
"parent": parentNodeId,
"children": [],
};
const clonedNewNode = { ...newNodeToAdd
};
if (conditionType === 'False' && foundRule.children) {
// foundRule.children[0] = newNodeToAdd;
foundRule.children.splice(0, 1, clonedNewNode);
if (!(foundRule.children[1] && Object.keys(foundRule.children[1]))) {
foundRule.children[1] = {};
}
} else {
// foundRule.children[1] = newNodeToAdd;
foundRule.children.splice(1, 1, clonedNewNode);
if (!(foundRule.children[0] && Object.keys(foundRule.children[0]))) {
founRule.children[0] = {};
}
}
// find the node and add a child to it.
updateTree();
}
function updateTree() {
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
update(root);
}
function getNodeFromNodeId(nodes, nodeId) {
for (const node of nodes) {
const currNode = node;
if (currNode) {
if (currNode.id === nodeId) {
return currNode;
} else if (currNode.children) {
const childResult = getNodeFromNodeId(currNode.children, nodeId);
if (childResult) {
return childResult;
}
}
}
}
return null;
}
.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>
EDIT you were right about wrapping the links around the nodes, since adding a child to the overlapping node should always trigger a relayout. I wrapped the link around it roughly, using just straight corners. You could make that smoother using the Q
logic around inflections currently contained in linkFunc
.
Upvotes: 1