简体   繁体   中英

Side-by-side paths in d3

I'm trying out a way to get paths to display next to each other, such that they'll push each other around (factoring in widths and neighbouring points) and not overlap.

This is my fiddle, mostly pieced together from examples https://jsfiddle.net/crimsonbinome22/k2xqn24x/

var LineGroup = svg.append("g")
.attr("class","line");

var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.x); })
.y(function(d) { return (d.y); })
;

LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.p); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");

And this is what I'm hoping to achieve in this image here , basically:

  • For all lines landing on the same point, push them left or right of that point so together they center around it.
  • Factor in line width so they don't overlap, or leave whitespace between.
  • Be able to handle paths with different numbers of points (max in example is 3 but I want to deal with up to 10)
    • Note though points that overlap will always have the same index (they won't loop around, but just go outwards like a tree)
  • Be able to handle different numbers of lines landing on the same point.

Some issues I'm having:

  • I'm new to d3 and I find functions a bit baffling. Not sure how to even start to apply logic that will move the lines around.
  • My data structure has some redundant info in it, such as r for the rank (to decide whether to push left or right) and w for the width both of which will always be the same for a particular line.
  • I have a lot of data so the data structure used here won't work with the csv data I have. Can maybe skip this one for now and I'll open up a new question for that one later.

I've had a search around but can't find any examples of how to do this. In a way it's almost like a chord diagram but a little different, and I can't find much relevant code to reuse. Any help on how to achieve this (either with the approach I've started, or something totally different if I've missed it) would be appreciated.

I would go with the following steps:

  • compute an array of node objects, ie one object for each point visited by a line
  • compute the tree on this node (that is, for every node, add links to its parent and children)
  • make sure that children of any node are ordered according to the angle they make with this node
  • at this point, each line now only depends on its final node
  • for each node compute an ordered list of lines going through
    • visit all nodes bottom-up (ie starting from the leaves)
    • the "go-through" list is the concatenation of the lists of the children + all lines that end at the current node
  • for each node, compute an array of offsets (by summing the successive width of the lines going through)
  • finally, for every line and every node in the line, check the array of offsets to know how much the line must be shifted

Edit: running example https://jsfiddle.net/toh7d9tq/1/

I have used a slightly different approach for the last two steps (computing the offset): I actually create a new p array for each series with a list of pairs {node, offset} . This way it is much easier to access all relevant data in the drawing function.

I needed to add an artificial root to have a nice starting line (and to make it easier for recursion and angles and everything), you can skip it in the drawing phase if you want.

  function key(p) {
   return p.time+"_"+p.value
  }

  // a node has fields:
  // - time/value (coordinates)
  // - series (set of series going through)
  // - parent/children (tree structure) 
  // - direction: angle of the arc coming from the parent 

  //artificial root
  var root={time:200, value:height, series:[], direction:-Math.PI/2};

  //set of nodes
  var nodes = d3.map([root], key);
  //create nodes, link each series to the corresponding leaf
  series.forEach(function(s){
    s.pWithOffset=[]; //this will be filled later on
    var parent=root;  
    s.p.forEach(function(d) {  
     var n=nodes.get(key(d));
     if (!n) {
       //create node at given coordinates if does not exist
       n={time:d.time, 
          value:d.value, 
          parent:parent, 
          series:[],
          direction:Math.atan2(d.value-parent.value, d.time-parent.time)};
       nodes.set(key(n),n);   
       //add node to the parent's children
       if (!parent.children) parent.children=[];
       parent.children.push(n);
     }    
     //this node is the parent of the next one
     parent=n;
    })
    //last node is the leaf of this series
    s.leafNode=parent;
    parent.series.push(s);  
  })

  //sort children by direction
  nodes.values().forEach(function(n){
      if (n.children) 
       n.children.sort(function (a,b){
         if (a.direction>n.direction)
         return a.direction-b.direction;
       });
      });

  //recursively list all series through each node (bottom-up)
  function listSeries(n) {
     if (!n.children) return;
     n.children.forEach(listSeries);
     n.series=d3.merge(n.children.map(function(c){return c.series}));   
  }
  listSeries(root); 
  //compute offsets for each series in each node, and add them as a list to the corresponding series
  //in a first time, this is not centered
  function listOffsets(n) {
     var offset=0;   
     n.series.forEach(function(s){
       s.pWithOffset.push( {node:n, offset:offset+s.w/2})
       offset+=s.w;     
     })
     n.totalOffset=offset;
     if (n.children)
       n.children.forEach(listOffsets);
  }
  listOffsets(root);

And then in the drawing section:

var line = d3.svg.line()
    .interpolate("linear")
    .x(function(d) { return (d.node.time-Math.sin(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
    .y(function(d) { return (d.node.value+Math.cos(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
    ;

LineGroup.selectAll(".line")
    .data(series)
  .enter().append("path")
    .attr("class", "line")
    .attr("d", function(d){ return line(d.pWithOffset); })
    .attr("stroke", function(d){ return d.c; })
    .attr("stroke-width", function(d){ return d.w; })
    .attr("fill", "none");

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM