【问题标题】:How to render links as elbow connectors in d3 force directed graph如何在 d3 力有向图中将链接渲染为弯头连接器
【发布时间】:2015-09-18 16:18:17
【问题描述】:

我对 D3 很陌生,这就是我迄今为止所做的事情here

实际代码在这里:

var width = 1840,
    height = 1480,
    constant = 100, 
    color = "#BCD8CD"

var nodes = [
    {label: '1st stage', x:   constant, y: 215 , width:70,height:50 , color :color , stage: true },
    {label: '2nd stage', x: constant + 150 , y: 215 ,width:70,height:50 ,color :color, stage: true },
    {label: '3rd stage', x: constant + 279, y: 215 ,width:70,height:50, color :color, stage: false },
    {label: '4th stage', x: constant + 460, y: 215 ,width:70,height:50, color :color, stage: false },
    {label: '5th stage', x: constant + 660, y: 215 ,width:70,height:50 ,color :color, stage: false },
    {label: '6th stage', x: constant + 350, y: 350 ,width:70,height:50, color :color, stage: true }
];

var links = [
    { source: 0, target: 1 },
    { source: 1, target: 2},
    { source: 2, target: 3},
    { source: 3, target: 4},
    { source: 1, target: 5}
];

var svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height);

var marker = svg.append('marker')
    .attr('id',"triangle")
    .attr('viewBox',"0 0 10 10")
    .attr('refX',"0")
    .attr('refY',"5")
    .attr('markerUnits','strokeWidth')
    .attr('markerWidth','4')
    .attr('markerHeight','3')
    .attr('orient','auto')

var path = marker.append('path')
      .attr('d',"M 0 0 L 10 5 L 0 10 z")

var force = d3.layout.force()
    .size([width, height])
    .nodes(nodes)
    .links(links);
force.linkDistance(width/4);

var link = svg.selectAll('.link')
    .data(links)
    .enter().append('line')
    .attr("stroke-width", "2")
    .attr('marker-end','url(#triangle)')
    .attr('stroke','black')

var defs = svg.append("defs");

// create filter with id #drop-shadow
// height=130% so that the shadow is not clipped
var filter = defs.append("filter")
    .attr("id", "drop-shadow")
    .attr("height", "130%");

// SourceAlpha refers to opacity of graphic that this filter will be applied to
// convolve that with a Gaussian with standard deviation 3 and store result
// in blur
filter.append("feGaussianBlur")
    .attr("in", "SourceAlpha")
    .attr("stdDeviation", 3)
    .attr("result", "blur");

// translate output of Gaussian blur to the right and downwards with 2px
// store result in offsetBlur
var feOffset = filter.append("feOffset")
    .attr("in", "blur")
    .attr("dx", 2)
    .attr("dy", 2)
    .attr("result", "offsetBlur");

// overlay original SourceGraphic over translated blurred opacity by using
// feMerge filter. Order of specifying inputs is important!
var feMerge = filter.append("feMerge");

feMerge.append("feMergeNode")
    .attr("in", "offsetBlur")
feMerge.append("feMergeNode")
    .attr("in", "SourceGraphic");

var node = svg.selectAll('.node')
    .data(nodes)
    .enter().append('g')
    .attr('class', 'node')
.attr("transform", function(d){
    return "translate("+d.x+","+d.y+")";
})

node.append("rect").attr("class", "nodeRect")
        .attr("rx", 6)
        .attr("ry", 6)
        .attr('width', function(d) { return d.width; })
        .attr('height', function(d) { return d.height; })
        .style("fill", function(d) { return d.color; })
        .transition()
        .duration(1000) // this is 1s
        .delay(1000) 
        .style("fill",function(d){if(d.stage) return "#FF9966"})
        .style("filter",function(d){if(d.stage) return "url(#drop-shadow)"})


node.append("text").style("text-anchor", "middle")
        .style("pointer-events", "none")
        .style("font-weight", 900)
        .attr("fill", "white")
        .style("stroke-width", "0.3px")
        .style("font-size", "16px")
        .attr("y", function (d){return d.height/2+6;})
        .attr("x", function (d){return d.width/2;})
        .text(function (d) {return d.label;})

force.start();

link.attr('x1', function(d) { return d.source.x + d.source.width/2; })
    .attr('y1', function(d) { return d.source.y + d.source.height/2; })
    .attr('x2', function(d) { return d.target.x + d.target.width/2; })
    .attr('y2', function(d) { return d.target.y + d.target.height/2; })
    .transition()
    .duration(1000) // this is 1s
    .delay(1000) 
    .style("filter",function(d){if(d.source.stage) return "url(#drop-shadow)"})

这按预期工作,除了正在呈现的链接。例如这个链接:

但是我希望该链接是:

如何在 d3 中实现这一点?

【问题讨论】:

  • 您可以使用d3.svg.line
  • 来吧,蝙蝠侠,我正在努力工作……我回答了你的问题,不是吗? ;)

标签: javascript d3.js


【解决方案1】:

惯用的方法是使用path 元素而不是line 并使用d3.svg.line() 创建链接。这样,箭头也可以工作,并且可以完全轻松地设置动画。


注意事项

在处理这个(非常有趣的!)示例时,我发现了一些系统性问题...

  1. Bug in IE
    显然 MS 无法解决这个问题,但是在渲染带有标记的元素时存在问题。 work-arround 用于在其父级上插入路径,这就是强制 ontick 事件处理程序中此行的目的...
    link.each(function() {this.parentNode.insertBefore(this, this); });
  2. 滤镜裁剪问题
    在示例中,我们有类似d="M28,46L28,23L77,23" 的路径元素指令来渲染两条正交线。这适用于过滤器并且投影按预期呈现,但是,当节点被拖动使得其中一条线的长度短于标记的相应尺寸时,出现了一个问题:路径元素,包括标记,开始被过滤器剪裁。
    我不明白到底发生了什么,但似乎过滤器的边界框(路径边界框的百分比)折叠到零高度,这以某种方式剪辑了引用路径元素。一旦路径边界框变为零,问题就会消失(至少在 Chrome 和 Opera 中是这样......)。

作为解决上述问题的尝试的一部分,我尝试将路径元素中的所有数字限制为整数,这是通过使用此代码向节点数据添加量化器 getter 来实现的...

 force.nodes().forEach(function(d) {
    d.q = {};
    Object.keys(d).forEach(function (p) {
      if (!isNaN(d[p])) Object.defineProperty(d.q, p, {
        get: function () {
          return Math.round(d[p])
        }
      });
    })
  });

这会在每个节点数据上创建一个 q 对象,并为返回数值的任何成员创建一个 getter - 我不需要考虑哪些成员,所以我只需点击它们 - 这让我可以做比如这个……

  node.attr("transform", function (d) {
    return "translate(" + d.q.x + "," + d.q.y + ")";
  })  

所以,d.q.xd.q.yd.xd.y 的舍入版本。 我也打算在linkPath 函数中使用它来使路径d 属性中的所有数字都为整数,但我意识到使用@ 中的自定义xy 访问器可以更好地实现这一点987654340@ 对象在这里...

  var connector = d3.svg.line().interpolate("linear")
    .x(function(d){return Math.round(d[0])})
    .y(function(d){return Math.round(d[1])});
  function linkPath(d){
    var  h1 = d.source.height, w1 = d.source.width, x1 = d.source.x + w1/2, y1 = d.source.y + h1/2,
      h2 = d.target.height, w2 = d.target.width, x2 = d.target.x  - markerW - 4, y2 = d.target.y + h2/2;
      return connector([[x1, y1], [x1, y2], [x2, y2]]);
  }  

d3.svg.line().interpolate("linear") 返回的函数接受[[p1x, p1y], [p2x, p2y], ... ] 形式的点数组,并使用提供的标准插值器为路径d 属性构造字符串值,(尝试其他也很有趣,标准 d3 插值器功能,例如基础)。通过添加自定义访问器,确保提供的所有坐标都四舍五入到最接近的整数值。
在 force tick 回调中调用的函数linkPath 只是根据链接数据构造一个由三个点组成的数组,并将该数组传递给connector 函数并返回一个可以用作d 的字符串path 元素的属性。调用签名确保它被传递给每个元素的绑定数据的副本......

link.attr("d", linkPath);  

因此,绑定到每个链接的数据用于创建三个点,这些点被插值并渲染为路径。


工作代码

需要管理一些问题以确保连接器和箭头正常工作,但这些问题在这里并不真正相关,因此我没有通过修复来混淆代码...

  var width = 600,
    height = 148,
    constant = 10,
    color = "#BCD8CD"

  var scale = .75, w = 70*scale, h = 50*scale,
    nodes = [
    {label: '1st stage', x:   constant, y: 20*scale , width:w,height:h , color :color , stage: true },
    {label: '2nd stage', x: constant + 150*scale , y: 20*scale ,width:w,height:h ,color :color, stage: true },
    {label: '3rd stage', x: constant + 279*scale, y: 20*scale ,width:w,height:h, color :color, stage: false },
    {label: '4th stage', x: constant + 460*scale, y: 20*scale ,width:w,height:h, color :color, stage: false },
    {label: '5th stage', x: constant + 660*scale, y: 20*scale ,width:w,height:h ,color :color, stage: false },
    {label: '6th stage', x: constant + 350*scale, y: 100*scale ,width:w,height:h, color :color, stage: true }
    ].map(function(d, i){return (d.fixed = (i != 5), d)});

  var links = [
    { source: 0, target: 1 },
    { source: 1, target: 2},
    { source: 2, target: 3},
    { source: 3, target: 4},
    { source: 1, target: 5}
  ];

  var svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height);

  var markerW = 4, markerH = 3,
    marker = svg.append('marker')
    .attr('id',"triangle")
    .attr('viewBox',"0 0 10 10")
    .attr('refX',"0")
    .attr('refY',5)
    .attr('markerUnits','strokeWidth')
    .attr('markerWidth',markerW)
    .attr('markerHeight',markerH)
    .attr('orient','auto')

  var path = marker.append('path')
    .attr('d',"M 0 0 L 10 5 L 0 10 z")

  var force = d3.layout.force()
    .size([width, height])
    .nodes(nodes)
    .links(links)
    .linkDistance(width/4)
    .on("tick", function(e){
      //hack to force IE to do it's job!
      link.each(function() {this.parentNode.insertBefore(this, this); });

      link.attr("d", linkPath);
      node.attr("transform", function (d) {
        return "translate(" + d.q.x + "," + d.q.y + ")";
      })
    });
    force.nodes().forEach(function(d) {
      d.q = {};
      Object.keys(d).forEach(function (p) {
        if (!isNaN(d[p])) Object.defineProperty(d.q, p, {
          get: function () {
            return Math.round(d[p])
          }
        });
      })
    });

  var connector = d3.svg.line().interpolate("linear")
    .x(function(d){return Math.round(d[0])})
    .y(function(d){return Math.round(d[1])});
  function linkPath(d){
    return connector([[d.source.x + d.source.width/2, d.source.y + d.source.height/2],
      [d.source.x + d.source.width/2, d.target.y + d.target.height/2],
      [d.target.x  - markerW - 4, d.target.y + d.target.height/2]]);
  }

  var link = svg.selectAll('.link')
    .data(links)
    .enter().append('path')
    .attr("stroke-width", "2")
    .attr('marker-end','url(#triangle)')
    .attr('stroke','black')
    .attr("fill", "none");

  var defs = svg.append("defs");

  // create filter with id #drop-shadow
  // height=130% so that the shadow is not clipped
  var filter = defs.append("filter")
    .attr("id", "drop-shadow")
    .attr({"height": "200%", "width": "200%", x: "-50%", y: "-50%"});

  // SourceAlpha refers to opacity of graphic that this filter will be applied to
  // convolve that with a Gaussian with standard deviation 3 and store result
  // in blur
  filter.append("feGaussianBlur")
    .attr("in", "SourceAlpha")
    .attr("stdDeviation", 3)
    .attr("result", "blur");

  // translate output of Gaussian blur to the right and downwards with 2px
  // store result in offsetBlur
  var feOffset = filter.append("feOffset")
    .attr("in", "blur")
    .attr("dx", 2)
    .attr("dy", 2)
    .attr("result", "offsetBlur");

  // overlay original SourceGraphic over translated blurred opacity by using
  // feMerge filter. Order of specifying inputs is important!
  var feMerge = filter.append("feMerge");

  feMerge.append("feMergeNode")
    .attr("in", "offsetBlur")
  feMerge.append("feMergeNode")
    .attr("in", "SourceGraphic");

  var node = svg.selectAll('.node')
    .data(nodes)
    .enter().append('g')
    .attr('class', 'node')
    .attr("transform", function(d){
      return "translate("+ d.q.x+","+ d.q.y+")";
    })
  .call(force.drag)

  node.append("rect").attr("class", "nodeRect")
    .attr("rx", 6)
    .attr("ry", 6)
    .attr('width', function(d) { return d.width; })
    .attr('height', function(d) { return d.height; })
    .style("fill", function(d) { return d.color; })
    .transition()
    .duration(1000) // this is 1s
    .delay(1000)
    .style("fill",function(d){if(d.stage) return "#FF9966"})
    .style("filter",function(d){if(d.stage) return "url(#drop-shadow)"})


  node.append("text").style("text-anchor", "middle")
    .style("pointer-events", "none")
    .style("font-weight", 900)
    .attr("fill", "white")
    .style("stroke-width", "0.3px")
    .style("font-size", 16*scale + "px")
    .attr("y", function (d){return d.height/2+6*scale;})
    .attr("x", function (d){return d.width/2;})
    .text(function (d) {return d.label;})

  force.start();

    link.attr("d", linkPath)
   .transition()
    .duration(1000) // this is 1s
    .delay(1000)
    .style("filter",function(d){if(d.source.stage) return "url(#drop-shadow)"});

  d3.select("svg").append("text").attr({"y": height - 20, fill: "black"}).text("drag me!")
svg { overflow: visible;}
.node {
  fill: #ccc;
  stroke: #fff;
  stroke-width: 2px;
}
.link {
  stroke: #777;
  stroke-width: 2px;
}
g.hover {
  background-color: rgba(0, 0, 0, .5);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

【讨论】:

  • 这太棒了!我想知道这些行做了什么` force.nodes().forEach(function(d) { dq = {}; Object.keys(d).forEach(function (p) { if (!isNaN(d[p] )) Object.defineProperty(dq, p, { get: function () { return Math.round(d[p]) } }); }) });`
  • 还有var connector = d3.svg.line().interpolate("linear") .x(function(d){return Math.round(d[0])}) .y(function(d){return Math.round(d[1])}); function linkPath(d){ return connector([[d.source.x + d.source.width/2, d.source.y + d.source.height/2], [d.source.x + d.source.width/2, d.target.y + d.target.height/2], [d.target.x - markerW - 4, d.target.y + d.target.height/2]]); }
  • @batman:干杯。我在帖子中添加了一些文字,试图回答您的 cmets...如果仍然不清楚,请告诉我。
【解决方案2】:

我通过创建一个空标签找到了解决方法:

Working demo

var nodes = [
    {label: '1st stage', x:   constant, y: 215 , width:70,height:50 , color :color , stage: true },
    {label: '2nd stage', x: constant + 150 , y: 215 ,width:70,height:50 ,color :color, stage: true },
    {label: '3rd stage', x: constant + 279, y: 215 ,width:70,height:50, color :color, stage: false },
    {label: '4th stage', x: constant + 460, y: 215 ,width:70,height:50, color :color, stage: false },
    {label: '5th stage', x: constant + 660, y: 215 ,width:70,height:50 ,color :color, stage: false },
    {label: '', x: constant + 185, y: 370 ,width:0,height:0 ,color :color, stage: true },
    {label: '6th stage', x: constant + 350, y: 350 ,width:70,height:50, color :color, stage: true }
];

var links = [
    { source: 0, target: 1 },
    { source: 1, target: 2},
    { source: 2, target: 3},
    { source: 3, target: 4},
    { source: 1, target: 5},
    { source: 5, target: 6}
];

【讨论】:

    猜你喜欢
    • 2012-05-02
    • 1970-01-01
    • 1970-01-01
    • 2019-02-20
    • 2019-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多