criver.to
criver.to

Reputation: 540

D3 tree link arrows are weird, not in the right place. How to fix them?

I have a D3 tree and I added arrow link markers with the code below, but they don't always match with the links, like if they are to the right or left of the parent. How can I fix it so that they look better?

 svg
    .append("svg:defs")
    .selectAll("marker")
    .data(["end"])
    .enter()
    .append("svg:marker")  
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 20)
    .attr("refY", function(d) {
        return 0
    })
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M0,-5L10,0L0,5");

My code for the tree is below and you can see how they look

var margin = {
  top: 30,
  right: 20,
  bottom: 20,
  left: 20
},
    width = 400 - margin.right - margin.left,
    height = 400 - margin.top - margin.bottom;

var i = 0,
    duration = 400,
    root;

var tree = d3.layout.tree()
    .nodeSize([5, 5])
    .separation(function separation(a, b) {
        return (a.parent == b.parent ? 1 : 2) / a.depth;
    });

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

var svg = d3.select("#render").append("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
    .call(zm = d3.behavior.zoom().scaleExtent([1,8]).on("zoom", redraw))
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

zm.translate([40, 40]);

svg.append("svg:defs").selectAll("marker")
    .data(["end"])
    .enter().append("svg:marker")  
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 20)
    .attr("refY", function(d) {
        return 0
    })
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M0,-5L10,0L0,5");


//Redraw for zoom
function redraw() {
  svg.attr("transform",
      "translate(" + d3.event.translate + ")"
      + " scale(" + d3.event.scale + ")");
}

var node = {
  name: 'Root',
  type: 'root',
  children: [{
    name: 'A',
    type: 'child'
  },
  {
    name: 'B',
    type: 'child'
  },
  {
    name: 'C',
    type: 'child'
  }]
};

root = node;
root.y0 = height / 2;
root.x0 = 0;

root.children.forEach(collapse);
update(root);

function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

function update(source) {
  var newHeight = Math.max(tree.nodes(root).reverse().length * 30, height);

  d3.select("#render svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", newHeight + margin.top + margin.bottom);

  tree = d3.layout.tree().size([newHeight, width]);

  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  nodes.forEach(function (d) {
    d.y = d.depth * 80;
  });

  var node = svg.selectAll("g.node")
      .data(nodes, function (d) {
          return d.id || (d.id = ++i);
      });

  var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function (d) {
          return "translate(" + source.x0 + "," + source.y0 + ")";
      })
      .on("click", click);

  nodeEnter
    .append("circle")
    .attr("r", 1e-6)
    .style("fill", function (d) {
        return d.endNode ? "orange" : "lightsteelblue";
     });

  nodeEnter.append("text")
    .attr("y", function (d) {
    return -20;
  })
    .attr("dy", ".35em")
    .attr("text-anchor", function (d) {
    return "middle";
  })
    .text(function (d) {
    return d.name;
  })
    .style("fill-opacity", 1e-6);

  var nodeUpdate = node.transition()
  .duration(duration)
  .attr("transform", function (d) {
    return "translate(" + d.x + "," + d.y + ")";
  });

  nodeUpdate.select("circle")
    .attr("r", 10)
    .style("fill", function (d) {
    return d.endNode ? "orange" : "lightsteelblue";
  });

  nodeUpdate.select("text")
    .style("fill-opacity", 1);

  var nodeExit = node.exit().transition()
  .duration(duration)
  .attr("transform", function (d) {
    return "translate(" + source.x + "," + source.y + ")";
  })
  .remove();

  nodeExit.select("circle")
    .attr("r", 1e-6);

  nodeExit.select("text")
    .style("fill-opacity", 1e-6);

  var link = svg.selectAll("path.link")
  .data(links, function (d) {
    return d.target.id;
  });

  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
    });
  })
  .attr("marker-end", "url(#end)");

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

  link.exit().transition()
    .duration(duration)
    .attr("d", function (d) {
    var o = {
      x: source.x,
      y: source.y
    };
    return diagonal({
      source: o,
      target: o
    });
  })
    .remove();

  nodes.forEach(function (d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

function click(d) {

      var children = d.children || [];
      for (var i = 0; i < 1; i++) {
        children.push({
          name: d.id + 1,
          type: 'child',
          endNode: true
        });
      }

      d.children = children;
      update(d);
}
#render {
  overflow: auto;
  text-align: center;
}

#render .node {
  cursor: pointer;
}

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

#render .node text {
  font: 16px "Hiragino Sans GB", "华文细黑", "STHeiti", "微软雅黑", "Microsoft YaHei", SimHei, "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}

#render .link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.10/d3.min.js"></script>
<div id="render"></div>

Upvotes: 2

Views: 465

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

Your marker is 6px high and your circles have a radius of 10px. So, inside the diagonal function, let's move the paths' end 16px up (that is, 6 + 10):

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

Of course, don't forget to change the markers' refX attribute accordingly (that is, subtracting 16).

Here is your code with that change, I'm also removing the texts to better see the path and the marker:

var margin = {
  top: 30,
  right: 20,
  bottom: 20,
  left: 20
},
    width = 400 - margin.right - margin.left,
    height = 400 - margin.top - margin.bottom;

var i = 0,
    duration = 400,
    root;

var tree = d3.layout.tree().nodeSize([5, 5])
.separation(function separation(a, b) {
  return (a.parent == b.parent ? 1 : 2) / a.depth;
});

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

var svg = d3.select("#render").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.call(zm = d3.behavior.zoom().scaleExtent([1,8]).on("zoom", redraw))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

zm.translate([40, 40]);

svg.append("svg:defs").selectAll("marker")
    .data(["end"])
  .enter().append("svg:marker")  
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 4)
    .attr("refY", function(d) {
  return 0
})
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
  .append("svg:path")
    .attr("d", "M0,-5L10,0L0,5");


//Redraw for zoom
function redraw() {
  svg.attr("transform",
      "translate(" + d3.event.translate + ")"
      + " scale(" + d3.event.scale + ")");
}

var node = {
  name: 'Root',
  type: 'root',
  children: [{
    name: 'A',
    type: 'child'
  },{
    name: 'B',
    type: 'child'
  },{
    name: 'C',
    type: 'child'
  }]
};

root = node;
root.y0 = height / 2;
root.x0 = 0;

root.children.forEach(collapse);
update(root);

function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

function update(source) {
  var newHeight = Math.max(tree.nodes(root).reverse().length * 30, height);

  d3.select("#render svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", newHeight + margin.top + margin.bottom);

  tree = d3.layout.tree().size([newHeight, width]);

  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  nodes.forEach(function (d) {
    d.y = d.depth * 80;
  });

  var node = svg.selectAll("g.node")
  .data(nodes, function (d) {
    return d.id || (d.id = ++i);
  });

  var nodeEnter = node.enter().append("g")
  .attr("class", "node")
  .attr("transform", function (d) {
    return "translate(" + source.x0 + "," + source.y0 + ")";
  })
  .on("click", click);

  nodeEnter.append("circle")
    .attr("r", 1e-6)
    .style("fill", function (d) {
    return d.endNode ? "orange" : "lightsteelblue";
  });


  var nodeUpdate = node.transition()
  .duration(duration)
  .attr("transform", function (d) {
    return "translate(" + d.x + "," + d.y + ")";
  });

  nodeUpdate.select("circle")
    .attr("r", 10)
    .style("fill", function (d) {
    return d.endNode ? "orange" : "lightsteelblue";
  });


  var nodeExit = node.exit().transition()
  .duration(duration)
  .attr("transform", function (d) {
    return "translate(" + source.x + "," + source.y + ")";
  })
  .remove();

  nodeExit.select("circle")
    .attr("r", 1e-6);


  var link = svg.selectAll("path.link")
  .data(links, function (d) {
    return d.target.id;
  });

  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
    });
  })
  .attr("marker-end", "url(#end)");

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

  link.exit().transition()
    .duration(duration)
    .attr("d", function (d) {
    var o = {
      x: source.x,
      y: source.y
    };
    return diagonal({
      source: o,
      target: o
    });
  })
    .remove();

  nodes.forEach(function (d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

function click(d) {

      var children = d.children || [];
      for (var i = 0; i < 1; i++) {
        children.push({
          name: d.id + 1,
          type: 'child',
          endNode: true
        });
      }

      d.children = children;
      update(d);
}
#render {
  overflow: auto;
  text-align: center;
}

#render .node {
  cursor: pointer;
}

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

#render .node text {
  font: 16px "Hiragino Sans GB", "华文细黑", "STHeiti", "微软雅黑", "Microsoft YaHei", SimHei, "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}

#render .link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.10/d3.min.js"></script>
<div id="render"></div>

Upvotes: 1

Related Questions