D3带盒树

时间:2018-09-09 17:20:16

标签: d3.js

利用我有限的D3知识,我结合了一些树布局示例来制作带有分支标签的可折叠树。这是功能提取:

var root = {
    children:[
        {
            title:"Node title",
            children:[
                {
                    type:"end",
                    items:[],
                    optionTitle:"Branch 1"
                },
                {
                    type:"end",
                    items:[],
                    optionTitle:"Branch 2"
                }
            ]
        }
    ]
}

var maxLabelLength = 23;
var i = 0;
var duration = 750;

// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;

var viewerWidth = 800;
var viewerHeight = 300;

var tree = d3.layout.tree();

var diagonal = d3.svg.diagonal()
    .projection(function(d) {
        return [d.y, d.x];
    });

function visit(parent, visitFn, childrenFn) {
    if (!parent) return;

    visitFn(parent);

    var children = childrenFn(parent);
    if (children) {
        var count = children.length;
        for (var i = 0; i < count; i++) {
            visit(children[i], visitFn, childrenFn);
        }
    }
}

var baseSvg = d3.select('.tree').append("svg")
    .attr("width", viewerWidth)
    .attr("height", viewerHeight)
    .attr("class", "tree-container");


// Helper functions for collapsing and expanding nodes.
function collapse(d) {
    if (d.children) {
        d._children = d.children;
        d._children.forEach(collapse);
        d.children = null;
    }
}

function centerNode(source) {
    var scale = 1;
    var x = 20;
    var y = -source.x0;
    y = y * scale + viewerHeight / 2;
    d3.select('g').transition()
        .duration(duration)
        .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}

function toggleChildren(d) {
    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else if (d._children) {
        expand(d);
    }
    return d;
}

function expand(d) {
    if (d._children) {
        d.children = d._children;
        d._children = null;
        if (d.children.length == 1) {
            expand(d.children[0])
        }
    }
}

function click(d) {
    if (d._children) {
        if (d.type!='end') {
            expandCollapse(d);
        }
    } else {
        expandCollapse(d);
    }
}

function expandCollapse(d) {
    d = toggleChildren(d);
    update(d);
    centerNode(d);
}

function update(source) {
    var levelWidth = [1];
    var childCount = function(level, n) {

        if (n.children && n.children.length > 0) {
            if (levelWidth.length <= level + 1) levelWidth.push(0);

            levelWidth[level + 1] += n.children.length;
            n.children.forEach(function(d) {
                childCount(level + 1, d);
            });
        }
    };
    childCount(0, root);
    var newHeight = d3.max(levelWidth) * 25;
    tree = tree.size([newHeight, viewerWidth]);

    // Compute the new tree layout.
    var nodes = tree.nodes(root).reverse(),
        links = tree.links(nodes);

    // Set widths between levels based on maxLabelLength.
    nodes.forEach(function(d) {
        d.y = (d.depth * (maxLabelLength * 8)); 

        if (d.x<root.x) {
            d.x -= (root.x-d.x)*3;
        } else if (d.x>root.x) {
            d.x += (d.x-root.x)*3;
        }
    });

    // Update the nodes…
    var node = svgGroup.selectAll("g.node")
        .data(nodes, function(d) {
            return d.id || (d.id = ++i);
        });

    // Enter any new nodes at the parent's previous position.
    var nodeEnter = node.enter().append("g")
        .attr("class", "node")
        .attr("transform", function(d) {
            return "translate(" + source.y0 + "," + source.x0 + ")";
        })
        .on('click', click);

    nodeEnter.append("circle")
        .attr('class', 'nodeCircle');
        
    // Change the circle fill depending on whether it has children and is collapsed
    node.select("circle.nodeCircle")
        .attr("r", 6)
        .style("fill", function(d) {
            return getNodeFill(d);
        });

    nodeEnter.append("text")
        .attr("x", function(d) {
            return d.children || d._children ? -10 : 10;
        })
        .attr("dy", ".35em")
        .attr('class', 'nodeText')
        .attr("text-anchor", function(d) {
            return d.children || d._children ? "end" : "start";
        })
        .text(function(d) {
            return d.title;
        })
        .style("fill-opacity", 0);


    // Update the text to reflect whether node has children or not.
    node.select('text')
        .attr("x", function(d) {
            return d.children || d._children ? -10 : 10;
        })
        .text(function(d) {
            if (d.type!='end') {
                return d.title
            } else {
                return 'End node'
            }
        });

    // Transition nodes to their new position.
    var nodeUpdate = node.transition()
        .duration(duration)
        .attr("transform", function(d) {
            return "translate(" + d.y + "," + d.x + ")";
        });

    // Fade the text in
    nodeUpdate.select("text")
        .style("fill-opacity", 1);

    // Transition exiting nodes to the parent's new position.
    var nodeExit = node.exit().transition()
        .duration(duration)
        .attr("transform", function(d) {
            return "translate(" + source.y + "," + source.x + ")";
        })
        .remove();

    nodeExit.select("circle")
        .attr("r", 0);

    nodeExit.select("text")
        .style("fill-opacity", 0);

    // Update the links…
    var link = svgGroup.selectAll("path.link")
        .data(links, function(d) {
            return d.target.id;
        });

    // Enter any new links at the parent's previous position.
    link.enter().insert("path", "g")
        .attr("class", "link")
        .attr("d", function(d) {
            var o = {
                x: source.x0,
                y: source.y0
            };
            return diagonal({
                source: o,
                target: o
            });
        });

    // Transition links to their new position.
    link.transition()
        .duration(duration)
        .attr("d", diagonal);

    // Transition exiting nodes to the parent's new position.
    link.exit().transition()
        .duration(duration)
        .attr("d", function(d) {
            var o = {
                x: source.x,
                y: source.y
            };
            return diagonal({
                source: o,
                target: o
            });
        })
        .remove();

    // Update the link text
    var linktext = svgGroup.selectAll("g.link")
        .data(links, function (d) {
            return d.target.id;
        });

    linktext.enter()
        .insert("g")
        .attr("class", "link")
        .append("text")
        .attr("dy", ".35em")
        .attr("text-anchor", "middle")
        .text(function (d) {
            return d.target.optionTitle;
        });

    // Transition link text to their new positions

    linktext.transition()
        .duration(duration)
        .attr("transform", function (d) {
            return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
        })

    //Transition exiting link text to the parent's new position.
    linktext.exit().transition().remove();


    // Stash the old positions for transition.
    nodes.forEach(function(d) {
        d.x0 = d.x;
        d.y0 = d.y;
    });


}

var svgGroup = baseSvg.append("g");

// Layout the tree initially and center on the root node.
update(root);
centerNode(root);

svgGroup
    .append('defs')
    .append('pattern')
    .attr('id', function(d,i){
        return 'pic_plus';
    })
    .attr('height',60)
    .attr('width',60)
    .attr('x',0)
    .attr('y',0)
    .append('image')
    .attr('xlink:href',function(d,i){
        return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
    })
    .attr('height',12)
    .attr('width',12)
    .attr('x',0)
    .attr('y',0);

function getNodeFill(d) {
    if (isFinal(d)) {
        return '#0f0';
    } else if (d._children || (!d._children&&!d.children)) {
        return 'url(#pic_plus)'
    } else {
        return '#fff'
    }
}

function isFinal(node) {
    return node.type=='end';
}
body {
    background-color: #ddd;
}

.tree-custom,
.tree {
    width:100%;
    height: 100%;
    background-color: #fff;
}

.holder {
    margin: 0 auto;
    width: 1000px;
    height: 800px;
    background-color: #fff;
}

.node {
    cursor: pointer;
}

.node circle {
    fill: #fff;
    stroke: steelblue;
    stroke-width: 1.5px;
}

.node text {
    font: 10px sans-serif;
}

path.link {
    fill: none;
    stroke: #ccc;
    stroke-width: 1.5px;
}

.link text {
    font: 10px sans-serif;
    fill: #666;
}
<html>
    <head>
        <script src="https://d3js.org/d3.v3.min.js"></script>
    </head>
    <body>
		<div class="tree"></div>
		<script src="code.js"></script>
    </body>
</html>

在我的代码中,节点是圆圈,节点旁边是节点标签:

Expanded tree example

当我折叠一个节点时,会显示一个plus标志。

Collapsed tree example

现在,我正尝试将节点标签放在此example code中所示的框中。

Boxed labels

我知道我必须像示例代码中那样将circles更改为foreignObjects,但是当我这样做时,路径并没有调整到框内。

如何将circles更改为foreignObjects,并保持相同的扩展/折叠/加号功能?

1 个答案:

答案 0 :(得分:1)

您需要对当前布局进行的更改是

  • 移动蓝色框,使其与圆垂直对齐,并且其右边缘与每个圆的左侧相邻;

  • 更改路径以在每个蓝色框的左侧终止

第一个可以使用变换来更改蓝色框的位置,第二个可以通过更改每条线的目标点的坐标来实现。

查看链接到的样本,在rect元素后面有foreignObject个元素提供了颜色,并且foreignObject从{{1}的位置偏移了}元素。我自由地将rect元素添加到您的代码中,并将rectrect元素分组在一起,以便可以在单个转换中移动它们:

foreignObject

如果您查看生成的树,则var rectGrpEnter = nodeEnter.append('g') .attr('class', 'node-rect-text-grp'); rectGrpEnter.append('rect') .attr('rx', 6) .attr('ry', 6) .attr('width', rectNode.width) .attr('height', rectNode.height) .attr('class', 'node-rect'); rectGrpEnter.append('foreignObject') .attr('x', rectNode.textMargin) .attr('y', rectNode.textMargin) .attr('width', function() { return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 : (rectNode.width - rectNode.textMargin * 2) }) .attr('height', function() { return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 : (rectNode.height - rectNode.textMargin * 2) }) .append('xhtml').html(function(d) { return '<div style="width: ' + (rectNode.width - rectNode.textMargin * 2) + 'px; height: ' + (rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' + '<b>' + d.title + '</b>' + '</div>'; }); / rect组需要沿x方向平移foreignObject元素的长度+ rect半径轴},并将circle元素沿y轴的高度减半。因此,首先让我们添加一个代表圆半径的变量,然后用该变量替换硬编码数字:

rect

现在编写转换:

var circleRadius = 6;

// a bit further on
node.select("circle.nodeCircle")
    .attr("r", circleRadius)
    .style("fill", function(d) {
        return getNodeFill(d);
    });

检查生成的树:

var rectGrpEnter = nodeEnter.append('g')
    .attr('class', 'node-rect-text-grp')
    .attr('transform', 'translate('
    + -(rectNode.width + circleRadius) + ','  // note the transform is negative
    + -(rectNode.height/2) + ')' );
var rectNode = {
  width: 120,
  height: 45,
  textMargin: 5
};

var root = {
  slideId: 100,
  children: [{
    title: "Node title",
    children: [{
        type: "end",
        items: [],
        optionTitle: "Branch 1"
      },
      {
        type: "end",
        items: [],
        optionTitle: "Branch 2"
      }
    ]
  }]
}

var maxLabelLength = 23;
var i = 0;
var duration = 750;

// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;

var viewerWidth = 800;
var viewerHeight = 300;

var tree = d3.layout.tree();

var diagonal = d3.svg.diagonal()
  .projection(function(d) {
    return [d.y, d.x];
  });

function visit(parent, visitFn, childrenFn) {
  if (!parent) return;

  visitFn(parent);

  var children = childrenFn(parent);
  if (children) {
    var count = children.length;
    for (var i = 0; i < count; i++) {
      visit(children[i], visitFn, childrenFn);
    }
  }
}

var baseSvg = d3.select('.tree').append("svg")
  .attr("width", viewerWidth)
  .attr("height", viewerHeight)
  .attr("class", "tree-container");

// Helper functions for collapsing and expanding nodes.
function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

function centerNode(source) {
  var scale = 1;
  var x = 20;
  var y = -source.x0;
  y = y * scale + viewerHeight / 2;
  d3.select('g').transition()
    .duration(duration)
    .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}

function toggleChildren(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else if (d._children) {
    expand(d);
  }
  return d;
}

function expand(d) {
  if (d._children) {
    d.children = d._children;
    d._children = null;
    if (d.children.length == 1) {
      expand(d.children[0])
    }
  }
}

function click(d) {
  if (d._children) {
    if (d.type != 'end') {
      expandCollapse(d);
    }
  } else {
    expandCollapse(d);
  }
}

function expandCollapse(d) {
  d = toggleChildren(d);
  update(d);
  centerNode(d);
}

function update(source) {
  var levelWidth = [1];
  var childCount = function(level, n) {

    if (n.children && n.children.length > 0) {
      if (levelWidth.length <= level + 1) levelWidth.push(0);

      levelWidth[level + 1] += n.children.length;
      n.children.forEach(function(d) {
        childCount(level + 1, d);
      });
    }
  };
  childCount(0, root);
  var newHeight = d3.max(levelWidth) * 25;
  var circleRadius = 6;

  tree = tree.size([newHeight, viewerWidth]);

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
    links = tree.links(nodes);

  // Set widths between levels based on maxLabelLength.
  nodes.forEach(function(d) {
    d.y = (d.depth * (maxLabelLength * 8));

    if (d.x < root.x) {
      d.x -= (root.x - d.x) * 3;
    } else if (d.x > root.x) {
      d.x += (d.x - root.x) * 3;
    }
  });

  // Update the nodes…
  var node = svgGroup.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return "translate(" + source.y0 + "," + source.x0 + ")";
    })
    .on('click', click);

  var rectGrpEnter = nodeEnter.append('g')
    .attr('class', 'node-rect-text-grp')
    .attr('transform', 'translate('
    + -(rectNode.width + circleRadius) + ','  // note the transform is negative
    + -(rectNode.height/2) + ')' );

  rectGrpEnter.append('rect')
    .attr('rx', 6)
    .attr('ry', 6)
    .attr('width', rectNode.width)
    .attr('height', rectNode.height)
    .attr('class', 'node-rect');

  rectGrpEnter.append('foreignObject')
    .attr('x', rectNode.textMargin)
    .attr('y', rectNode.textMargin)
    .attr('width', function() {
      return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
        (rectNode.width - rectNode.textMargin * 2)
    })
    .attr('height', function() {
      return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
        (rectNode.height - rectNode.textMargin * 2)
    })
    .append('xhtml').html(function(d) {
      return '<div style="width: ' +
        (rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
        (rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
        '<b>' + d.title + '</b>' +
        '</div>';
    });

  nodeEnter.append("circle")
    .attr('class', 'nodeCircle');

  // Change the circle fill depending on whether it has children and is collapsed
  node.select("circle.nodeCircle")
    .attr("r", circleRadius)
    .style("fill", function(d) {
      return getNodeFill(d);
    });

  nodeEnter.append("text")
    .attr("x", function(d) {
      return d.children || d._children ? -10 : 10;
    })
    .attr("dy", ".35em")
    .attr('class', 'nodeText')
    .attr("text-anchor", function(d) {
      return d.children || d._children ? "end" : "start";
    })
    .text(function(d) {
      return d.title;
    })
    .style("fill-opacity", 0);


  // Update the text to reflect whether node has children or not.
  node.select('text')
    .attr("x", function(d) {
      return d.children || d._children ? -10 : 10;
    })
    .text(function(d) {
      if (d.type != 'end') {
        return d.title
      } else {
        return 'End node'
      }
    });

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + d.y + "," + d.x + ")";
    });

  // Fade the text in
  nodeUpdate.select("text")
    .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + source.y + "," + source.x + ")";
    })
    .remove();

  nodeExit.select("circle")
    .attr("r", 0);

  nodeExit.select("text")
    .style("fill-opacity", 0);

  // Update the links…
  var link = svgGroup.selectAll("path.link")
    .data(links, function(d) {
      return d.target.id;
    });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("d", function(d) {
      var o = {
        x: source.x0,
        y: source.y0
      };
      return diagonal({
        source: o,
        target: o
      });
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", diagonal);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
      var o = {
        x: source.x,
        y: source.y
      };
      return diagonal({
        source: o,
        target: o
      });
    })
    .remove();

  // Update the link text
  var linktext = svgGroup.selectAll("g.link")
    .data(links, function(d) {
      return d.target.id;
    });

  linktext.enter()
    .insert("g")
    .attr("class", "link")
    .append("text")
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .text(function(d) {
      return d.target.optionTitle;
    });

  // Transition link text to their new positions

  linktext.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
    })

  //Transition exiting link text to the parent's new position.
  linktext.exit().transition().remove();


  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });


}

var svgGroup = baseSvg.append("g");

// Layout the tree initially and center on the root node.
update(root);
centerNode(root);

svgGroup
  .append('defs')
  .append('pattern')
  .attr('id', function(d, i) {
    return 'pic_plus';
  })
  .attr('height', 60)
  .attr('width', 60)
  .attr('x', 0)
  .attr('y', 0)
  .append('image')
  .attr('xlink:href', function(d, i) {
    return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
  })
  .attr('height', 12)
  .attr('width', 12)
  .attr('x', 0)
  .attr('y', 0);

function getNodeFill(d) {
  if (isFinal(d)) {
    return '#0f0';
  } else if (d._children || (!d._children && !d.children)) {
    return 'url(#pic_plus)'
  } else {
    return '#fff'
  }
}

function isFinal(node) {
  return node.type == 'end';
}

function isCollapsed(node) {
  return d._children || (!d._children && !d.children);
}
body {
  background-color: #ddd;
}

.tree-custom,
.tree {
  width: 100%;
  height: 100%;
  background-color: #fff;
}

.holder {
  margin: 0 auto;
  width: 1000px;
  height: 800px;
  background-color: #fff;
}

.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

path.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

.link text {
  font: 10px sans-serif;
  fill: #666;
}

.node-rect {
  fill: #00f;
}

.node-text {
  color: #fff;
}

下一个任务是更改链接,使其在<html> <head> <script src="https://d3js.org/d3.v3.min.js"></script> </head> <body> <div class="tree"></div> </body> </html>框的边缘终止。如果检查覆盖链接位置和过渡的代码,则rectenter选择都将使用exit节点的位置。我们感兴趣的代码是这样的:

source

对角函数在哪里

  link.transition()
    .duration(duration)
    .attr("d", diagonal);

var diagonal = d3.svg.diagonal() .projection(function(d) { return [d.y, d.x]; }); 采用以下形式的对象

d3.svg.diagonal()

,如果您查看{ source: { x: 10, y: 10 }, target: { x: 20, y: 50 } } 数组中的每个项目,就会发现它的格式为

tree.links

因此,要更改链接目标的位置,我们需要创建一个{ source: { /* source node coordinates */ }, target: { /* target node coords */ } 坐标已更改的新对象。同样,x轴更改应为target; y轴正常。请注意,尽管-(rectNode.width + circleRadius)函数切换了diagonalx的值,所以我们需要更改目标的y值,而不是y值。因此,我们有:

x

将其添加到我们的代码中:

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", function(d) {
      return diagonal({
        source: d.source,  // this is the same
        target: { x: d.target.x, y: d.target.y - (rectNode.width + circleRadius) } 
      });
    });
var rectNode = {
  width: 120,
  height: 45,
  textMargin: 5
};

var root = {
  slideId: 100,
  children: [{
    title: "Node title",
    children: [{
        type: "end",
        items: [],
        optionTitle: "Branch 1"
      },
      {
        type: "end",
        items: [],
        optionTitle: "Branch 2"
      }
    ]
  }]
}

var maxLabelLength = 23;
var i = 0;
var duration = 750;

// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;

var viewerWidth = 800;
var viewerHeight = 300;

var tree = d3.layout.tree();

var diagonal = d3.svg.diagonal()
  .projection(function(d) {
    return [d.y, d.x];
  });

function visit(parent, visitFn, childrenFn) {
  if (!parent) return;

  visitFn(parent);

  var children = childrenFn(parent);
  if (children) {
    var count = children.length;
    for (var i = 0; i < count; i++) {
      visit(children[i], visitFn, childrenFn);
    }
  }
}

var baseSvg = d3.select('.tree').append("svg")
  .attr("width", viewerWidth)
  .attr("height", viewerHeight)
  .attr("class", "tree-container");

// Helper functions for collapsing and expanding nodes.
function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

function centerNode(source) {
  var scale = 1;
  var x = 20;
  var y = -source.x0;
  y = y * scale + viewerHeight / 2;
  d3.select('g').transition()
    .duration(duration)
    .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}

function toggleChildren(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else if (d._children) {
    expand(d);
  }
  return d;
}

function expand(d) {
  if (d._children) {
    d.children = d._children;
    d._children = null;
    if (d.children.length == 1) {
      expand(d.children[0])
    }
  }
}

function click(d) {
  if (d._children) {
    if (d.type != 'end') {
      expandCollapse(d);
    }
  } else {
    expandCollapse(d);
  }
}

function expandCollapse(d) {
  d = toggleChildren(d);
  update(d);
  centerNode(d);
}

function update(source) {
  var levelWidth = [1];
  var childCount = function(level, n) {

    if (n.children && n.children.length > 0) {
      if (levelWidth.length <= level + 1) levelWidth.push(0);

      levelWidth[level + 1] += n.children.length;
      n.children.forEach(function(d) {
        childCount(level + 1, d);
      });
    }
  };
  childCount(0, root);
  var newHeight = d3.max(levelWidth) * 25;
  var circleRadius = 6;

  tree = tree.size([newHeight, viewerWidth]);

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
    links = tree.links(nodes);

  // Set widths between levels based on maxLabelLength.
  nodes.forEach(function(d) {
    d.y = (d.depth * (maxLabelLength * 8));

    if (d.x < root.x) {
      d.x -= (root.x - d.x) * 3;
    } else if (d.x > root.x) {
      d.x += (d.x - root.x) * 3;
    }
  });

  // Update the nodes…
  var node = svgGroup.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return "translate(" + source.y0 + "," + source.x0 + ")";
    })
    .on('click', click);

  var rectGrpEnter = nodeEnter.append('g')
    .attr('class', 'node-rect-text-grp')
    .attr('transform', 'translate(' +
      -(rectNode.width + circleRadius) + ',' // note the transform is negative
      +
      -(rectNode.height / 2) + ')');

  rectGrpEnter.append('rect')
    .attr('rx', 6)
    .attr('ry', 6)
    .attr('width', rectNode.width)
    .attr('height', rectNode.height)
    .attr('class', 'node-rect');

  rectGrpEnter.append('foreignObject')
    .attr('x', rectNode.textMargin)
    .attr('y', rectNode.textMargin)
    .attr('width', function() {
      return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
        (rectNode.width - rectNode.textMargin * 2)
    })
    .attr('height', function() {
      return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
        (rectNode.height - rectNode.textMargin * 2)
    })
    .append('xhtml').html(function(d) {
      return '<div style="width: ' +
        (rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
        (rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
        '<b>' + d.title + '</b>' +
        '</div>';
    });

  nodeEnter.append("circle")
    .attr('class', 'nodeCircle');

  // Change the circle fill depending on whether it has children and is collapsed
  node.select("circle.nodeCircle")
    .attr("r", circleRadius)
    .style("fill", function(d) {
      return getNodeFill(d);
    });

  nodeEnter.append("text")
    .attr("x", function(d) {
      return d.children || d._children ? -10 : 10;
    })
    .attr("dy", ".35em")
    .attr('class', 'nodeText')
    .attr("text-anchor", function(d) {
      return d.children || d._children ? "end" : "start";
    })
    .text(function(d) {
      return d.title;
    })
    .style("fill-opacity", 0);


  // Update the text to reflect whether node has children or not.
  node.select('text')
    .attr("x", function(d) {
      return d.children || d._children ? -10 : 10;
    })
    .text(function(d) {
      if (d.type != 'end') {
        return d.title
      } else {
        return 'End node'
      }
    });

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + d.y + "," + d.x + ")";
    });

  // Fade the text in
  nodeUpdate.select("text")
    .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + source.y + "," + source.x + ")";
    })
    .remove();

  nodeExit.select("circle")
    .attr("r", 0);

  nodeExit.select("text")
    .style("fill-opacity", 0);

  // Update the links…
  var link = svgGroup.selectAll("path.link")
    .data(links, function(d) {
      return d.target.id;
    });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("d", function(d) {
      var o = {
        x: source.x0,
        y: source.y0
      };
      return diagonal({
        source: o,
        target: o
      });
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", function(d) {
      return diagonal({
        source: d.source,
        target: { x: d.target.x, y: d.target.y - (rectNode.width + circleRadius) }
      });
    });


  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
      var o = {
        x: source.x,
        y: source.y
      };
      return diagonal({
        source: o,
        target: o
      });
    })
    .remove();

  // Update the link text
  var linktext = svgGroup.selectAll("g.link")
    .data(links, function(d) {
      return d.target.id;
    });

  linktext.enter()
    .insert("g")
    .attr("class", "link")
    .append("text")
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .text(function(d) {
      return d.target.optionTitle;
    });

  // Transition link text to their new positions

  linktext.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
    })

  //Transition exiting link text to the parent's new position.
  linktext.exit().transition().remove();


  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });


}

var svgGroup = baseSvg.append("g");

// Layout the tree initially and center on the root node.
update(root);
centerNode(root);

svgGroup
  .append('defs')
  .append('pattern')
  .attr('id', function(d, i) {
    return 'pic_plus';
  })
  .attr('height', 60)
  .attr('width', 60)
  .attr('x', 0)
  .attr('y', 0)
  .append('image')
  .attr('xlink:href', function(d, i) {
    return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
  })
  .attr('height', 12)
  .attr('width', 12)
  .attr('x', 0)
  .attr('y', 0);

function getNodeFill(d) {
  if (isFinal(d)) {
    return '#0f0';
  } else if (d._children || (!d._children && !d.children)) {
    return 'url(#pic_plus)'
  } else {
    return '#fff'
  }
}

function isFinal(node) {
  return node.type == 'end';
}

function isCollapsed(node) {
  return d._children || (!d._children && !d.children);
}
body {
  background-color: #ddd;
}

.tree-custom,
.tree {
  width: 100%;
  height: 100%;
  background-color: #fff;
}

.holder {
  margin: 0 auto;
  width: 1000px;
  height: 800px;
  background-color: #fff;
}

.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

path.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

.link text {
  font: 10px sans-serif;
  fill: #666;
}

.node-rect {
  fill: #00f;
}

.node-text {
  color: #fff;
}

您可以通过删除<html> <head> <script src="https://d3js.org/d3.v3.min.js"></script> </head> <body> <div class="tree"></div> </body> </html>元素上的填充来检查链接是否在正确的位置终止。

现在还有许多其他修复方法,但这应该已经证明了您的概念证明;如果您不清楚新代码的任何部分,请询问。