Alternating or avoiding intersecting paths in D3

I am creating an arc diagram where I would like to hopefully find a way to prevent the arcs from overlapping. Here's an example of a working bl.ock .

Arc diagram

The darker lines in this case are overlapping lines where multiple nodes share the same edge. I would like to prevent this, perhaps by doing two passes: the first will alternate the arc to go above the nodes, not below, which gives the appearance of a spiral; the second will draw a slightly larger arc if the arc already exists above / below to help differentiate links.

var width   = 1000,
    height  = 500,
    margin  = 20,
    pad     = margin / 2,
    radius  = 6,
    yfixed  = pad + radius;

var color = d3.scale.category10();

// Main
//-----------------------------------------------------

function arcDiagram(graph) {
  var radius = d3.scale.sqrt()
    .domain([0, 20])
    .range([0, 15]);

  var svg = d3.select("#chart").append("svg")
      .attr("id", "arc")
      .attr("width", width)
      .attr("height", height);

  // create plot within svg
  var plot = svg.append("g")
    .attr("id", "plot")
    .attr("transform", "translate(" + pad + ", " + pad + ")");

  // fix graph links to map to objects
  graph.links.forEach(function(d,i) {
    d.source = isNaN(d.source) ? d.source : graph.nodes[d.source];
    d.target = isNaN(d.target) ? d.target : graph.nodes[d.target];
  });

  linearLayout(graph.nodes);
  drawLinks(graph.links);
  drawNodes(graph.nodes);
}

// layout nodes linearly
function linearLayout(nodes) {
  nodes.sort(function(a,b) {
    return a.uniq - b.uniq;
  })

  var xscale = d3.scale.linear()
    .domain([0, nodes.length - 1])
    .range([radius, width - margin - radius]);

  nodes.forEach(function(d, i) {
    d.x = xscale(i);
    d.y = yfixed;
  });
}

function drawNodes(nodes) {

  var gnodes = d3.select("#plot").selectAll("g.node")
    .data(nodes)
  .enter().append('g');

  var nodes = gnodes.append("circle")
    .attr("class", "node")
    .attr("id", function(d, i) { return d.name; })
    .attr("cx", function(d, i) { return d.x; })
    .attr("cy", function(d, i) { return d.y; })
    .attr("r", 5)
    .style("stroke", function(d, i) { return color(d.gender); });

  nodes.append("text")
    .attr("dx", function(d) { return 20; })
    .attr("cy", ".35em")
    .text(function(d) { return d.name; })

}

function drawLinks(links) {
  var radians = d3.scale.linear()
  .range([Math.PI / 2, 3 * Math.PI / 2]);

  var arc = d3.svg.line.radial()
    .interpolate("basis")
    .tension(0)
    .angle(function(d) { return radians(d); });

  d3.select("#plot").selectAll(".link")
    .data(links)
  .enter().append("path")
    .attr("class", "link")
    .attr("transform", function(d,i) {
      var xshift = d.source.x + (d.target.x - d.source.x) / 2;
      var yshift = yfixed;
      return "translate(" + xshift + ", " + yshift + ")";
    })
    .attr("d", function(d,i) {
      var xdist = Math.abs(d.source.x - d.target.x);
      arc.radius(xdist / 2);
      var points = d3.range(0, Math.ceil(xdist / 3));
      radians.domain([0, points.length - 1]);
      return arc(points);
    });
}

      

Any pointers on how I can start getting closer to the problem?

+3


source to share


1 answer


Here is a bl.ock link for reference. It shows your original paths in gray and suggested paths in red.

First, remember the number of times the given path takes place:

graph.links.forEach(function(d,i) {
  var pathCount = 0;
  for (var j = 0; j < i; j++) {
    var otherPath = graph.links[j];
    if (otherPath.source === d.source && otherPath.target === d.target) {
      pathCount++;
    }
  }

  d.pathCount = pathCount;
});

      

Then, once you get this data, I would use an ellipse instead of a radial line, as it seems like a radial line can only draw a curve for the circle:



d3.select("#plot").selectAll(".ellipse-link")
  .data(links)
.enter().append("ellipse")
  .attr("fill", "transparent")
  .attr("stroke", "gray")
  .attr("stroke-width", 1)
  .attr("cx", function(d) {
    return (d.target.x - d.source.x) / 2 + radius;
  })
  .attr("cy", pad)
  .attr("rx", function(d) {
    return Math.abs(d.target.x - d.source.x) / 2;
  })
  .attr("ry", function(d) {
    return 150 + d.pathCount * 20;
  })
  .attr("transform", function(d,i) {
    var xshift = d.source.x - radius;
    var yshift = yfixed;
    return "translate(" + xshift + ", " + yshift + ")";
  });

      

Note that changing the value for ry

above will change the heights of the various curves.

Finally, you will have to use a clip path to constrain the area of ​​each ellipse that is actually shown so that they only appear below the nodes. (This fails in bl.ock)

+1


source







All Articles