d3 中的并行路径

Side-by-side paths in d3

本文关键字:路径 并行 d3      更新时间:2023-09-26

我正在尝试一种方法来让路径彼此相邻显示,这样它们就会相互推动(考虑宽度和相邻点)而不是重叠。

这是我的小提琴,大部分是从例子中拼凑出来的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");

这就是我希望在这张图片中实现的目标,基本上:

  • 对于落在同一点上的所有线,将它们向左或向右推,使它们一起围绕它。
  • 考虑线宽,以便它们不重叠,或在行间留空格。
  • 能够处理具有不同点数的路径(示例中的最大值为 3,但我想处理多达 10 个)
    • 请注意,尽管重叠的点将始终具有相同的索引(它们不会循环,而只是像树一样向外移动)
  • 能够处理落在同一点上的不同数量的线。

我遇到的一些问题:

  • 我是 d3 的新手,我发现功能有点令人困惑。甚至不知道如何开始应用将移动线条的逻辑。
  • 我的数据结构中有一些冗余信息,例如 r 表示排名(用于决定是向左推还是向右推),w 表示宽度,对于特定行,两者始终相同。
  • 有很多数据,所以这里使用的数据结构不适用于我拥有的 csv 数据。也许现在可以跳过这个,我稍后会为那个问题打开一个新问题。

我已经搜索了一下,但找不到任何如何做到这一点的例子。在某种程度上,它几乎就像一个和弦图,但有点不同,我找不到太多相关的代码来重用。关于如何实现这一目标的任何帮助(无论是使用我已经开始的方法,还是如果我错过了完全不同的方法),将不胜感激。

我会按照以下步骤操作:

  • 计算节点对象数组,即一条线访问的每个点都有一个对象
  • 计算此节点上的(即,为每个节点添加指向其父节点和子节点的链接)
  • 确保任何节点的子节点都根据它们与此节点形成的角度进行排序
  • 此时,每行现在仅取决于其最终节点
  • 对于每个节点,计算通过的行的有序列表
    • 自下而上访问所有节点(即从叶子开始)
    • "直通"列表是子节点列表 + 以当前节点结尾的所有行的串联
  • 对于每个节点,计算偏移量数组(通过对连续宽度求和通过的线路)
  • 最后,对于每行和行中的每个节点,检查偏移数组以了解该行必须移动多少

编辑:运行示例https://jsfiddle.net/toh7d9tq/1/

我在最后两个步骤(计算偏移量)中使用了略有不同的方法:我实际上为每个系列创建了一个新的p数组,其中包含{node, offset}对的列表。这样,在绘图功能中访问所有相关数据就容易得多。

我需要添加一个人工根来有一个很好的起跑线(并使其更容易递归和角度以及所有内容),如果需要,您可以在绘图阶段跳过它。

  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);

然后在绘图部分:

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");