我正在研究构建垂直流程图的D3.js V4 / 5实现。我通过单击“决策”框并将其对应的“菱形” /“矩形”形状添加到流程图的决策框中。
来自Mike Bostocks,请点击此处添加/删除节点:-https://github.com/d3/d3-hierarchy/issues/139 我遵循的步骤1是:“修改数据(或传递其他子访问器函数以进行过滤后,通过调用d3.hierarchy派生一棵全新的树”。
因此,当用户尝试添加新节点时,我正在修改实际的树/子级,计算层次结构并调用update()方法。像下面这样
JS小提琴:-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);
}
实际问题:
一切正常,直到我尝试将2个决策节点添加到一个决策节点及其下面的更多决策节点为止。连接节点的链接穿过另一个兄弟节点。
要复制小提琴中的问题:
要添加新节点,请单击出现在节点上的橙色菱形。
分别在左侧和右侧添加2个同级节点(1个动作/矩形和1个决策节点)。为决策节点添加2个决策节点,为这2个决策节点添加2个决策节点。
下面的图片可以使您更加清晰。如您所见,添加所有节点后,左侧突出显示的路径经过“ New Action”节点,而不是停留在较早的位置。另外,当添加更多孩子时,兄弟姐妹之间的距离会增加很多。
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>
也只想确认在addActionOrDecision
中添加节点的方式,请让我知道这是否是一个好方法。我基本上是从实际数据中找到父节点,并将一个新创建的节点的副本添加到父节点的子节点(在实际的treeData中)。
编辑:-这是我不断添加更多节点时的样子,左侧的节点与右侧的节点混合在一起,并且节点/链接混乱了。
关于UX的外观应该如何:-(向链接添加弯曲,并相应地向左或向右调整整棵树?)
编辑:-
Modified JsFiddle to show the issue during the initial launch :-
http://jsfiddle.net/c3yz4bj0/3/
答案 0 :(得分:1)
我通过编写自定义tree.separation()
函数解决了这个问题。它与default one非常相似,但是不同之处在于,如果两个节点中只有一个具有任何子节点,它将节点分开。这样可以防止重叠。通常,如果两个节点都具有子节点,则这些子节点将成为它们不重叠的原因,但是有时这是行不通的。
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>
编辑,将链接包装在节点周围是正确的,因为将子节点添加到重叠节点应始终触发重新布局。我仅使用直角将链接粗略地包裹起来。您可以使用Q
围绕linkFunc
当前包含的拐点进行逻辑处理。