添加新节点时,D3.js节点链接溢出到同级节点中 [英] D3.js node links overflowing into sibling nodes when new nodes are being added

查看:76
本文介绍了添加新节点时,D3.js节点链接溢出到同级节点中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在研究构建垂直流程图的D3.js V4 / 5实现。我要通过在流程图的决策框中单击决策来添加一个新节点。框和对应的菱形 /矩形框


来自Mike Bostocks,请点击此处添加/删除节点:-


添加2个同级节点(1动作/矩形和1个决策节点)。为决策节点添加2个决策节点,并为这2个决策节点添加另外2个决策节点。


下面的图片可以使您更加清楚。如您所见,左侧突出显示的路径通过了新操作添加所有节点时,不要将其放置在较早的位置。另外,添加更多孩子时,兄弟姐妹之间的距离也会增加很多。



  var margin = {
顶部:20,
右侧:120,
底部:20,
左:120,
},
宽度= 960-margin.right-margin.left,
高度= 800-margin.top-margin.bottom;

函数generateEmptyDecisionBox(condition){
return condition ==='False'吗? [{{
name: newDecision,
id: newId,
type: decision,
value: notSure,
condition:`$ {condition}`,
},{}]:[{},{
name: newDecision,
id: newId,
type:决定,
value: notSure,
condition:`$ {condition}`,
}];
}

函数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,
孩子:[{
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,
值: 4,
孩子:[],
}],
},
{
condition: True,
名称:部门,
类型:决策,
值: ab,
id: child33 ,
children:[{
condition: True,
name: division1,
type: decision,
值: a- b,
id: child44,
children:[{
condition: True,
name: division1.1 ,
type:决定,
value: ab,
id: child599,
children:[{
条件:真,
名称: division1.1.34,
类型:决策,
值: ab,
id: child234234,
children:[{
condition: True,
name: division1.1.434,
type: decision,
value: ab,
id: child35343,
children:[],
}],
},{
condition:真,
name: division1.1.2,
type:决定,
value : ab,
id: child77,
children:[{
condition: True,
name: division1 .1.1,
类型:决策,
值: ab,
id: child1222,
children:[] ,
},{
condition: True,
name: division1.1.1,
type: decision,
value: ab,
id: child66,
children:[],
}],
}],
},{
条件:真实,
名称:节点有重叠问题,
类型:决策,
值: ab,
id: child9090,
children:[],
}],
},
{
condition: True ,
name: division2,
type: decision,
value: ab,
id: child55,
孩子:[{
condition: True,
name: division2.1,
type: decision,
value: ab,
id: child88,
children:[{
condition: True,
name : division2.1.1,
type:决定,
value: ab,
id: child99,
children :[],
}],
}],
},
],
},
],
}];
var i = 0,
持续时间= 1000,
rectW = 120,
rectH = 60;

var treeMap = d3.tree()
.nodeSize([150,180]);

//链接功能到绘制链接
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,
};

//这是直线弯曲
的地方var inflection = {
x:target.x,
y:source.y,
};
var radius = 5;

var结果= M + source.x +‘,’+ source.y;

if(source.x< target.x&&& d.data.type){
//子项位于父项
结果的右侧+ = 'H'+(inflection.x-半径);
}否则,如果(d.data.type){
结果+ =’H’+(inflection.x +半径);
}否则{返回

}

//在弯曲处稍微弯曲直线
结果+ ='Q'+ inflection.x +','+ inflection.y +''+弯曲。 x +','+(inflection.y +半径);

结果+ ='V'+ target.y;
的返回结果;
};

//链接功能结束//

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)
}) )


//将箭头添加到下一个决策点。

svg.append( svg:defs)
.selectAll( marker)
.data([ end])//不同的链接/路径类型可以在此处定义
.enter()
.append( svg:marker)//此部分添加了箭头
.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);

//必要,以便缩放知道从
/ * zm.translate([350,20])进行缩放的位置。 * /

root = d3.hierarchy(treeData [0],function(d){
return d.children;
});
root.x0 = 0;
root.y0 = 0;

更新(根);

d3.select(。tree-diagram)
.style( height, 1000px);

//绘制树的结尾//

函数update(source){
const treeData = treeMap(root);
const treeRoot = d3.hierarchy(root);
// d3.tree(treeRoot);
// var treeData = treeMap(root);

//计算新的树布局。
var个节点= treeData.descendants(),
个链接= treeData.descendants()
.slice(1);

//归一化为固定深度。
个node.forEach(function(d){
d.y = d.depth * 90;
});

//更新节点…
var node = svg.selectAll( g.node)
.data(nodes,function(d){
return d.data.id ||(d.id = ++ i);
});

//在父级的先前位置输入任何新节点。
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 ==='决定') {
返回'M 60 0 120 30 60 60 0 30 Z';
}否则if(d.data.type ==='action'){
返回'M 0 0 120 0 120 60 0 60 Z';
}否则{
返回'M -100 -10 -10 -10 -10 -10 -10 -10 -10Z';
}
} )
.attr( stroke-width,1)
.attr('class','myPaths')
.style( fill,function(d){
返回 lightsteelblue;
});

nodeEnter.append( text)
.attr( x,rectW / 2)
.attr( y,rectH / 2)
.attr( dy, .35em)
.attr( text-anchor, middle)
.text(function(d){
返回d.data。名称;
});

//更新
var nodeUpdate = nodeEnter.merge(node);

//过渡到节点的正确位置
nodeUpdate.transition()
.duration(duration)
.attr( transform,function(d ){
return translate( +(dx)+, +(dy)+);
});


nodeUpdate.select('path.myPaths')
.attr( d,function(d){
if(d.data.type = =='决策'){
返回'M 60 0 120 30 60 60 0 30 Z';
}否则if(d.data.type ==='action'){
返回'M 0 0 120 0 120 60 0 60 Z';
} else {
返回'M -100 -10 -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();

//更新链接…
var link = svg.selectAll(。link)
.data(links,function(d){
return d .data.id;
})
.classed('link1',true);


//在父母的上一个位置输入任何新链接。
var linkEnter = link.enter()
.insert( g, g)
.attr( class, link);

linkEnter.append('path')
.on('click',function(d,i){
selectedLink = d;
//使用本地SVG接口以获取到
的边界框//计算路径的中心

var bbox = this.getBBox();
var x;
var y;

if(d.parent.x< dx){
//子代位于父代的右边
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform','translate('+ x +','+ y +')')
.classed('hide ',false);

}否则{
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 (#结束));

//添加链接文本。
linkEnter.append(‘text’);

//合并新链接和现有链接,然后在所有链接上设置`d`和`text`
link = linkEnter.merge(link);

link.select(’path')
.attr( d,linkFunc);

link.select('text')
.text(function(d,i){
if(d.parent.x< dx){
返回'True';
} else {
返回'False';
}
})
.attr('transform',function(d){
if(d.parent.x< dx&&& d.data.type){
return'translate('+(dx + rectW / 2)+','+(d.parent。 y + rectH)+')';
}否则if(d.data.type){
return'translate('+(d.parent.x + rectW / 2)+','+ (dy + rectH)+')';
} else {
return;
}
});


// LinkUpdate
var linkUpdate = linkEnter.merge(link);

//过渡到其新职位的链接。
link.transition()
.duration(duration)
.attr( d,linkFunc);

//过渡到其新职位的链接。

//将退出节点过渡到父级的新位置。
link.exit()
.transition()
.duration(duration)
.attr( d,linkFunc)
.remove();

//存放旧的过渡职位。
个node.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
}


//点选
函数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('+(dx-20)+','+(dy + 35)+')')
.classed('隐藏',假);

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('+(dx + 110)+','+(dy + 35)+')' )
.classed('hide',false);

rectangleShape.attr(’transform’,‘translate(’+(d.x + 110)+’,’+ d.y +’)’)。classed('hide',false);

}
}

// o通话

函数addElement(d,真){
d.children =空值;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}

//绘制元素//

函数drawDiamond(centroid){
//从顶部
开始var result = 'M'+ centroid.x +','+(centroid.y-rectH / 2);

//向右移动
结果+ =‘L’+(centroid.x + rectW / 2)+’,’+ centroid.y;

//底部
结果+ =‘L’+ centroid.x +’,’+(centroid.y + rectH / 2);

//左
结果+ =‘L’+(centroid.x-rectW / 2)+’,’+ centroid.y;

//关闭形状
结果+ ='Z';

返回结果;
}

函数drawRect(centroid){
//从左上方开始
var结果='M'+(centroid.x-rectW / 2) +','+(centroid.y-rectH / 2);

//向右移
结果+ =‘h’+ rectW;

//向下
结果+ =‘v’+ rectH;

//左
结果+ =‘h-’+ rectW;

//关闭形状
结果+ ='Z';
的返回结果;
}

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)')//将按钮居中于` g`
.attr('width',16)
.attr('height',16)
.attr('rx',2);

plusButton
.append('path')
.attr('d','M-6 0 H6 M0 -6 V6');

varangleShape = svg.append('g')
.classed('conditionImage',true)
.classed('hide',true)
。 on('click',function(){
addActionOrDecision(selectedNode,'action','True');
removeAllButtonElements();
});

矩形形状
.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 (填充,橙色);


varangleShapeFalse = svg.append('g')
.classed('conditionImage',true)
.classed('hide',true)
.on('click',function(){
addActionOrDecision(selectedNode,'action','False');
removeAllButtonElements();
});

矩形ShapeFalse
.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 (填充,橙色);


函数removeAllButtonElements(){
plusButton.classed('hide',true);
diamondImage.classed('hide',true);
angleShape.classed('hide',true);
diamondImageFalse.classed('hide',true);
angleShapeFalse.classed('hide',true);
}


函数addActionOrDecision(selectedNode,nodeType,conditionType){

const parentNodeId = selectedNode.parent.data.id;
const selectedNodeId = selectedNode.data.id;

//从实际的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] = {};
}
}

//找到节点并为其添加子节点。
updateTree();

}

函数updateTree(){
root = d3.hierarchy(treeData [0],function(d){
return d.children ;
});
root.x0 =高度/ 2;
root.y0 = 0;

更新(根);

}

函数getNodeFromNodeId(nodes,nodeId){
for(节点的常量节点){
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;
}
}
}
}
返回null;
}

  .node {
光标:指针;
大纲:无!重要;
}

.node文本{
字体:10px sans-serif;
}

.button> path {
stroke:blue;
笔划宽度:1.5;
/ *大纲:无; * /
}

.button> rect {
fill:#ddd;
中风:灰色;
笔划宽度:1px;
}

.conditionalSvg {
/ *轮廓:无; * /
显示:无;
}

.hide {
/ * display:none; * /
不透明度:0!important;
/ *指针事件:无; * /
}

.link:hover {
outline:none!important;
cursor:指针;
笔划宽度:3px;
}

.link path {
/ *轮廓:无!重要; * /
fill:无;
中风:深灰色;
stroke-width:2px;
}

.link path:hover {
cursor:指针;
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中)。


编辑:-这是这样的看起来,当我继续添加更多节点时,左侧的节点与右侧的节点混合在一起,并且节点/链接被弄乱了。



关于它可能看起来如何的用户体验:-(向链接添加弯曲,并相应地将整个树向左或向右调整?)



编辑:-


已修改JsFiddle可以在首次启动期间显示该问题:-

解决方案

我通过编写自定义 tree.separation() 函数。它与默认值非常相似,但是不同之处在于,如果两个节点中只有一个具有任何子节点,它将节点分开。这样可以防止重叠。通常,如果两个节点都具有子节点,则这些子节点将成为它们不重叠的原因,但是有时这是行不通的。


  var margin = {
顶部:20,
右:120,
底部:20,
左:120,
},
宽度= 960-margin.right-margin.left,
高度= 800-margin.top-margin.bottom;

函数generateEmptyDecisionBox(condition){
return condition ==='False'吗? [{{
name: newDecision,
id: newId,
type: decision,
value: notSure,
condition:`$ {condition}`,
},{}]:[{},{
name: newDecision,
id: newId,
type:决定,
value: notSure,
condition:`$ {condition}`,
}];
}

函数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,
孩子:[{
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,
值: 4,
孩子:[],
}],
},
{
condition: True,
名称:部门,
类型:决策,
值: ab,
id: child33 ,
children:[{
condition: True,
name: division1,
type: decision,
value: ab,
id: child44,
children:[{
condition: True,
name: division1.1,
type:决定,
value: ab,
id: child599,
children: [{
condition: True,
name: division1.1.34,
type: decision,
value: ab ,
id: child234234,
children:[{
condition: True,
name: division1.1.434,
type:决定,
value: ab,
id: child35343,
children:[],
}],
},{
condition: True,
name: division1.1.2,
type: decision,
value: ab,
id : child77,
children:[{
condition: True,
name: division1.1.1,
type:决定,
值: ab,
id: child1222,
孩子:[],
},{
条件: True,
名称: division1.1.1,
类型:决策,
值: ab,
id: child66,
children:[],
}],
}],
},{
condition: True ,
名称:节点有重叠问题,
类型:决策,
值: ab,
id: child9090 ,
孩子:[{
condition: True,
name: division1.1.1,
type: decision,
value: ab,
id: child909090,
children:[],
}],
}],
},
{
condition: True,
name: division2,
type: decision,
value: ab,
id: child55,
children:[{
condition: True,
name: division2.1 ,
type:决定,
value: ab,
id: child88,
children:[{
条件:真,
名称:除法2.1.1,
类型:决策,
值: ab,
id: child99,
children:[],
}],
}],
},
],
},
],
}];
var i = 0,
持续时间= 1000,
rectW = 120,
rectH = 60;

var treeMap = d3.tree()
.nodeSize([140,120])
.separation(function(a,b){
//如果它们具有相同的父
if(a.parent === b.parent){
//都是叶子节点或都不是叶子节点
//或只有一个孩子(这导致直线向下)
if(((a.children === undefined || a.children.length< = 1)===
(b.children === undefined || b.children.length< = 1)){
return 1;
}
// //增加它们之间的大小
return 2;
}

//如果它们的深度相同,则将它们标记为相同深度,这样我们以后就可以避免它们
if(a.depth === b.depth){
a .data.avoidRight = b;
b.data.avoidLeft = a;
}
返回2;
});

//链接功能到绘制链接
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,
};

//这是直线弯曲
的地方var inflection = {
x:target.x,
y:source.y,
};
var radius = 5;

var结果= M + source.x +’,’+ source.y;

if(!d.source.data.type){
return;
}
if(source.x< target.x){
//子项位于父项的右边
if(d.source.data.avoidRight!= = undefined&&inflection.x> d.source.data.avoidRight.x){
//我们应该尝试避免出现第一个
结果的节点+ ='H'+ (d.source.data.avoidRight.x-2 *半径);
结果+ =‘V’+(d.source.data.avoidRight.y + rectH +半径);
inflection.y = d.source.data.avoidLeft.y + rectH + radius;
}

结果+ =’H’+(inflection.x-半径);
} else {
//子项位于父项
的左侧if(d.source.data.avoidLeft!== undefined&& inflection.x< d.source。 data.avoidLeft.x + rectW){
结果+ ='H'+(d.source.data.avoidLeft.x + rectW + 2 *半径);
结果+ =‘V’+(d.source.data.avoidLeft.y + rectH +半径);
inflection.y = d.source.data.avoidLeft.y + rectH + radius;
}

结果+ =’H’+(inflection.x +半径);
}

//在弯曲处稍微弯曲直线
结果+ ='Q'+ inflection.x +','+ inflection.y +''+弯曲。 x +','+(inflection.y +半径);

结果+ ='V'+ target.y;
的返回结果;
};

//链接功能结束//

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)
}) )


//将箭头添加到下一个决策点。

svg.append( svg:defs)
.selectAll( marker)
.data([ end])//不同的链接/路径类型可以在这里定义
.enter()
.append( svg:marker)//此部分添加了箭头
.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);

//必要,以便缩放知道从
/ * zm.translate([350,20])进行缩放的位置。 * /

root = d3.hierarchy(treeData [0],function(d){
return d.children;
});
root.x0 = 0;
root.y0 = 0;

更新(根);

d3.select(。tree-diagram)
.style( height, 1000px);

//绘制树的结尾//

函数update(source){
const treeData = treeMap(root);

//计算新的树布局。
var个节点= treeData.descendants(),
个链接= treeData.links();

//归一化为固定深度。
个node.forEach(function(d){
d.y = d.depth * 90;
});

//更新节点…
var node = svg.selectAll( g.node)
.data(nodes,function(d){
return d.data.id ||(d.id = ++ i);
});

//在父级的先前位置输入任何新节点。
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 ==='决定') {
返回'M 60 0 120 30 60 60 0 30 Z';
}否则if(d.data.type ==='action'){
返回'M 0 0 120 0 120 60 0 60 Z';
}否则{
返回'M -100 -10 -10 -10 -10 -10 -10 -10 -10Z';
}
} )
.attr( stroke-width,1)
.attr('class','myPaths')
.style( fill,function(d){
返回 lightsteelblue;
});

nodeEnter.append( text)
.attr( x,rectW / 2)
.attr( y,rectH / 2)
.attr( dy, .35em)
.attr( text-anchor, middle)
.text(function(d){
返回d.data。名称;
});

//更新
var nodeUpdate = nodeEnter.merge(node);

//过渡到节点的正确位置
nodeUpdate.transition()
.duration(duration)
.attr( transform,function(d ){
return translate( +(dx)+, +(dy)+);
});


nodeUpdate.select('path.myPaths')
.attr( d,function(d){
if(d.data.type = =='决策'){
返回'M 60 0 120 30 60 60 0 30 Z';
}否则if(d.data.type ==='action'){
返回'M 0 0 120 0 120 60 0 60 Z';
} else {
返回'M -100 -10 -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();

//更新链接…
var link = svg.selectAll(。link)
.data(links,function(d){
return d .source.data.id + + d.target.data.id;
})
.classed('link1',true);


//在父母的上一个位置输入任何新链接。
var linkEnter = link.enter()
.insert( g, g)
.attr( class, link);

linkEnter.append('path')
.on('click',function(d,i){
selectedLink = d;
//使用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)\");

//添加链接文本。
linkEnter.append(‘text’);

//合并新链接和现有链接,然后在所有链接上设置`d`和`text`
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);

//过渡到其新职位的链接。
link.transition()
.duration(duration)
.attr(\"d\", linkFunc);

//过渡到其新职位的链接。

//将退出节点过渡到父级的新位置。
link.exit()
.transition()
.duration(duration)
.attr(\"d\", linkFunc)
.remove();

//存放旧的过渡职位。
个node.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
}


//点选
函数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);

//向右移动
结果+ =‘L’+(centroid.x + rectW / 2)+’,’+ centroid.y;

//底部
结果+ =‘L’+ centroid.x +’,’+(centroid.y + rectH / 2);

//左
结果+ =‘L’+(centroid.x-rectW / 2)+’,’+ centroid.y;

//关闭形状
结果+ ='Z';

返回结果;
}

function drawRect(centroid) {
// Start at the top left
var result = ’M’ + (centroid.x - rectW / 2) + ’,’ + (centroid.y - rectH / 2);

//向右移
结果+ =‘h’+ rectW;

//向下
结果+ =‘v’+ rectH;

//左
结果+ =‘h-’+ rectW;

//关闭形状
结果+ ='Z';
的返回结果;
}

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)')//将按钮居中于` 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();
});

矩形形状
.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();
});

矩形ShapeFalse
.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’);


函数removeAllButtonElements(){
plusButton.classed('hide',true);
diamondImage.classed('hide',true);
angleShape.classed('hide',true);
diamondImageFalse.classed('hide',true);
angleShapeFalse.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 {
光标:指针;
outline: none !important;
}

.node文本{
字体:10px sans-serif;
}

.button>path {
stroke: blue;
笔划宽度:1.5;
/* outline: none; */
}

.button>rect {
fill: #ddd;
中风:灰色;
笔划宽度:1px;
}

.conditionalSvg {
/* outline: none; */
display: none;
}

.hide {
/* display: none; */
opacity: 0 !important;
/* pointer-events: none; */
}

.link:hover {
outline: none !important;
cursor:指针;
stroke-width: 3px;
}

.link path {
/* outline: none !important; * /
fill:无;
中风:深灰色;
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.


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/

解决方案

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.

这篇关于添加新节点时,D3.js节点链接溢出到同级节点中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆