添加新节点时,D3.js节点链接溢出到同级节点中

时间:2020-09-16 07:22:39

标签: javascript svg d3.js

我正在研究构建垂直流程图的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个决策节点添加到一个决策节点及其下面的更多决策节点为止。连接节点的链接穿过另一个兄弟节点。

要复制小提琴中的问题:

要添加新节点,请单击出现在节点上的橙色菱形。

enter image description here

分别在左侧和右侧添加2个同级节点(1个动作/矩形和1个决策节点)。为决策节点添加2个决策节点,为这2个决策节点添加2个决策节点。

下面的图片可以使您更加清晰。如您所见,添加所有节点后,左侧突出显示的路径经过“ New Action”节点,而不是停留在较早的位置。另外,当添加更多孩子时,兄弟姐妹之间的距离会增加很多。

enter image description here

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中)。

编辑:-这是我不断添加更多节点时的样子,左侧的节点与右侧的节点混合在一起,并且节点/链接混乱了。

enter image description here

关于UX的外观应该如何:-(向链接添加弯曲,并相应地向左或向右调整整棵树?)

enter image description here

编辑:-

Modified JsFiddle to show the issue during the initial launch :- http://jsfiddle.net/c3yz4bj0/3/

enter image description here

1 个答案:

答案 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当前包含的拐点进行逻辑处理。