D3 Force Graph With Arrows and Curved Edges - shor

2019-05-30 14:50发布

问题:

I just read through this post - Links and Arrowheads to terminate at borders of nodes in D3 - however i am struggling to map his answer (on his curved links) with my example which (i think actually) uses a simpler/different curved edge for my links.

I have been working on a reproducible example of my problem, with force graph showing, for the last 20-30 minutes however for some reason the graph is not appearing (even though the code snippet is not throwing an error). It is inevitably a bunch of code (recreating a d3 force graph), although only a small section needs to be fixed. First, here is the code snippet:

const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")

var graphs = {
  "nodes": [
    { "name": "Peter", "label": "Person", "id": 1 },
    { "name": "Michael", "label": "Person", "id": 2 },
    { "name": "Neo4j", "label": "Database", "id": 3 },
    { "name": "Graph Database", "label": "Database", "id": 4 }
  ],
  "links": [
    { "source": 1, "target": 2, "type": "KNOWS", "since": 2010 },
    { "source": 1, "target": 3, "type": "FOUNDED" },
    { "source": 2, "target": 3, "type": "WORKS_ON" },
    { "source": 3, "target": 4, "type": "IS_A" }
  ]
}

svg.append('defs').append('marker')
    .attr('id','arrowhead')
    .attr('viewBox','-0 -5 10 10')
    .attr('refX',13)
    .attr('refY',0)
    .attr('orient','auto')
    .attr('markerWidth',13)
    .attr('markerHeight',13)
    .attr('xoverflow','visible')
    .append('svg:path')
    .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
    .attr('fill', '#999')
    .style('stroke','none');

const simulation = d3.forceSimulation()
  .force("link", d3.forceLink().id(d => d.id))
  .force("charge", d3.forceManyBody())
  .force("center", d3.forceCenter(100, 100));

let linksData = graphs.links.map(link => {
  var obj = link;
  obj.source = link.source;
  obj.target = link.target;
  return obj;
})

const links = svg.select("g.links")
  .selectAll("path")
  .data(linksData)
  .enter()
  .append("path")
  .attr('stroke', '#666666')
  .attr('fill', 'transparent')
  .attr("stroke-width", 2)
  .attr('marker-end', 'url(#arrowhead)')

const nodes = nodesG
  .selectAll("g")
  .data(graphs.nodes)
  .enter().append("g")
  .attr("cursor", "pointer")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

const circles = nodes.append("circle")
  .attr("r", 12)
  .attr("fill", "000000")

nodes.append("title") 
  .text(function(d) { return d.id; });

simulation
  .nodes(graphs.nodes)
  .on("tick", ticked);

simulation.force("link", d3.forceLink().links(linksData)
  .id((d,i) => d.id)
  .distance(150));
  
function ticked() {
  links.attr("d", function(d) {
    var dx = (d.target.x - d.source.x),
        dy = (d.target.y - d.source.y),
        dr = Math.sqrt(dx * dx + dy * dy);
    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
  });

    nodes
        .attr("transform", d => `translate(${d.x}, ${d.y})`);
}

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>

</head>
<body>
<svg id="mySVG" width="500" height="500">
  <g class="links" />
	<g class="nodes" />
</svg>

For starters, any help first with fixing the code snippet to show the graph would be greatly appreciated.

The main issue, however, is that the arrows go into the nodes, whereas I would prefer to not have the overlap. This jsfiddle has a gap between the arrowheads and the nodes that I think looks much better - http://jsfiddle.net/yeQS2/89/ - although i think for my example I'd prefer an even larger gap between arrow and node.

I believe it is the ticked() function that I need to update:

function ticked() {
  links.attr("d", function(d) {
    var dx = (d.target.x - d.source.x),
        dy = (d.target.y - d.source.y),
        dr = Math.sqrt(dx * dx + dy * dy);
    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
  });

    nodes
        .attr("transform", d => `translate(${d.x}, ${d.y})`);
}

I am actively working on fixing the reproducible example. I worked really hard on this, and think this is a problem that a lot of people would run into when trying to make an aesthetically good looking d3 force layout. Any help with this is appreciated!

Edit: thanks to whoever helped get the graph working - i went in and made the fill transparent on the links, so only the strokes show!

Edit2: not sure this is allowed, but this is for a big project of mine and I'll either bounty this post in 2 days, or bounty the winner if sooner, for sure.

回答1:

Applying the same idea in my answer here.

Produces:

const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")

var graphs = {
  "nodes": [{
      "name": "Peter",
      "label": "Person",
      "id": 1
    },
    {
      "name": "Michael",
      "label": "Person",
      "id": 2
    },
    {
      "name": "Neo4j",
      "label": "Database",
      "id": 3
    },
    {
      "name": "Graph Database",
      "label": "Database",
      "id": 4
    }
  ],
  "links": [{
      "source": 1,
      "target": 2,
      "type": "KNOWS",
      "since": 2010
    },
    {
      "source": 1,
      "target": 3,
      "type": "FOUNDED"
    },
    {
      "source": 2,
      "target": 3,
      "type": "WORKS_ON"
    },
    {
      "source": 3,
      "target": 4,
      "type": "IS_A"
    }
  ]
}

svg.append('defs').append('marker')
  .attr('id', 'arrowhead')
  .attr('viewBox', '-0 -5 10 10')
  .attr('refX', 0)
  .attr('refY', 0)
  .attr('orient', 'auto')
  .attr('markerWidth', 13)
  .attr('markerHeight', 13)
  .attr('xoverflow', 'visible')
  .append('svg:path')
  .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
  .attr('fill', '#999')
  .style('stroke', 'none');

const simulation = d3.forceSimulation()
  .force("link", d3.forceLink().id(d => d.id))
  .force("charge", d3.forceManyBody())
  .force("center", d3.forceCenter(100, 100));

let linksData = graphs.links.map(link => {
  var obj = link;
  obj.source = link.source;
  obj.target = link.target;
  return obj;
})

const links = svg.select("g.links")
  .selectAll("path")
  .data(linksData)
  .enter()
  .append("path")
  .attr('stroke', '#666666')
  .attr('fill', 'transparent')
  .attr("stroke-width", 2)
  .attr('marker-end', 'url(#arrowhead)')

const nodes = nodesG
  .selectAll("g")
  .data(graphs.nodes)
  .enter().append("g")
  .attr("cursor", "pointer")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

const circles = nodes.append("circle")
  .attr("r", 12)
  .attr("fill", "000000")

nodes.append("title")
  .text(function(d) {
    return d.id;
  });

simulation
  .nodes(graphs.nodes)
  .on("tick", ticked);

simulation.force("link", d3.forceLink().links(linksData)
  .id((d, i) => d.id)
  .distance(150));

function ticked() {
  links.attr("d", function(d) {
    var dx = (d.target.x - d.source.x),
      dy = (d.target.y - d.source.y),
      dr = Math.sqrt(dx * dx + dy * dy);
    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
  });

  // recalculate and back off the distance
  links.attr("d", function(d) {

    // length of current path
    var pl = this.getTotalLength(),
      // radius of circle plus backoff
      r = (12) + 30,
      // position close to where path intercepts circle
      m = this.getPointAtLength(pl - r);

    var dx = m.x - d.source.x,
      dy = m.y - d.source.y,
      dr = Math.sqrt(dx * dx + dy * dy);

    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
  });

  nodes
    .attr("transform", d => `translate(${d.x}, ${d.y})`);
}

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>

</head>

<body>
  <svg id="mySVG" width="500" height="500">
  <g class="links" />
	<g class="nodes" />
</svg>

The key is this:

  // recalculate and back off the distance
  links.attr("d", function(d) {

    // length of current path
    var pl = this.getTotalLength(),
      // radius of circle plus backoff
      r = (12) + 30, //<-- 12 is your radius 30 is the "back-off" distance
      // position close to where path intercepts circle
      m = this.getPointAtLength(pl - r);

    var dx = m.x - d.source.x,
      dy = m.y - d.source.y,
      dr = Math.sqrt(dx * dx + dy * dy);

    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
  });