d3中的并排路径

5
我正在尝试一种方法,使路径可以并排显示,这样它们会相互推动(考虑宽度和相邻点),而不会重叠。
这是我的fiddle,大部分是从示例中拼凑而成的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数据。现在可以跳过这个问题,稍后我会为此提出新问题。

我已经搜索了一下,但找不到任何示例来完成这个任务。从某种程度上说,它几乎像一个弦图,但有一些不同之处,我找不到可重用的相关代码。如果您能帮助我实现这个目标(无论是使用我已经开始的方法还是完全不同的方法),我将不胜感激。


这是一个非常复杂、不平凡的问题。在最坏情况下,您需要使用线性交点方程来确定它们相交的位置,然后将它们移动到彼此不相交的位置。但由于这些直线重叠,您的情况可能会更简单。我想知道是否可以确定每条直线的斜率和截距,如果它们匹配,则向左或向右移动。 - Mark
我同意这个问题的复杂性,但是我认为它可能比你描述的要简单。例如,我认为我不需要计算相交,因为我只想移动顶点相交的线。例如:取每条路径中的第一个点,并将具有相同(x,y)的点分组,然后在每个组内进行移动。对于每条路径中的第二个点、第三个点等,也做同样的操作。这可能会变得更加复杂(例如:计算推动它们的角度,而不仅仅是左/右),但作为一个开始,只知道它们需要移动应该是(希望)简单的。 - crimsonbinome22
1个回答

3
我建议按照以下步骤进行:
  • 计算一个节点对象数组,即每条线路访问的每个点都有一个对象
  • 在此节点上计算树形结构(即对于每个节点,添加到其父节点和子节点的链接)
  • 确保任何节点的子节点按角度排序
  • 此时,每条线路仅取决于其最终节点
  • 为每个节点计算一个经过的线路列表
    • 自下而上访问所有节点(即从叶子开始)
    • "经过"列表是子列表和所有以当前节点结尾的线路的连接
  • 对于每个节点,计算一个偏移量数组(通过总和线路宽度的连续线路)
  • 最后,对于每条线路和每个节点,检查偏移量数组以了解线路必须移动多少

编辑:运行示例 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");

这听起来是一个很好的解决方案,我很想看到它的实际效果。我曾经开始尝试过类似的东西,但只能计算推动行左/右侧的距离,以避免重叠。没有涉及到角度,但考虑到我的代码,我认为短时间内不会涉及到角度! - crimsonbinome22
1
@crimsonbinome22,这里我已经用代码完成了答案。 - tarulen
这非常令人印象深刻,而且做得非常优雅,谢谢你的帮助!我很快就会尝试实现它。 - crimsonbinome22

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接