强制布局添加链接时根据内容BBox创建节点并获取源节点宽度

Create nodes based on content BBox and get width of source node when adding links in force layout

我有一个 d3 force 布局,其中有一条线连接一个角色。我的代码目前看起来像这样...

edges.selectAll("line")
          .attr("x1", function (d) { return d.source.x; })
          .attr("y1", function (d) { return d.source.y; })
          .attr("x2", function (d) { return d.target.x; })
          .attr("y2", function (d) { return d.target.y; });

<g class="edge"><line x1="183.7429436753079" y1="-3182.732396966405" x2="-224.94046319279028" y2="-2920.273406745797" style="stroke: rgb(255, 255, 255); stroke-width: 1px;"></line><text fill="white" x="-20.59875975874118" y="-3051.502901856101"></text></g>

<g class="node" transform="translate(4109.590685978889,2004.5469511133144)"><text fill="white"></text><text fill="white" y="15">Interior</text></g>

当我使用它时,我看到类似下面的内容...

如您所见,所有关系都在底部相遇,但是,我希望它们在左侧相遇。为此,我认为我需要更改为...

edges.selectAll("line")
          .attr("x1", function (d) { return d.source.x+r; })
          .attr("y1", function (d) { return d.source.y-r; })
          .attr("x2", function (d) { return d.target.x+r; })
          .attr("y2", function (d) { return d.target.y-r; });

其中 r 是半径。除了保留一个包含所有节点宽度的数组外,有没有办法获取源和目标的宽度?

在文档中添加文本元素时,可以将BBox添加到节点数据中作为参考。如果先添加节点,然后添加链接,则可以通过将对节点元素的引用添加到数据元素上来将信息从前者传输到后者。您还可以在数据数组元素上添加您可能需要的任何其他有用的定位状态。之后,您可以通过 d.sourced.target 在 force tick 回调中访问您需要的任何内容。

使用 font awesome fonts 我注意到边界框至少需要一个动画周期才能演变成正确的形状,因此我不得不在初始渲染后将动画计时器添加到 运行 几次 (10)更新边界框几次并重新定位字形以正确居中。


编辑
使边界框调整永久(不仅仅是 运行 10 次)以解决 webkit 中的一个错误,即字形对齐在缩放事件时中断。然而,这在 moz 中引起了问题,因此需要找到另一种方法来修复 webkit 中的缩放错误。


Note
Referencing the svg element that the data is bound to, from that data element, creates a circular reference. So special care needs to be taken to break the reference chain. In the example below, the BBox reference is deleted after the required state has been copied onto the data elements.


工作示例

//debug panel/////////////////////////////////////////////////////////////////////////////
d3.select("#update").on("click", (function() {
 var dataSet = false;
 return function() {
  refresh(dataSets[(dataSet = !dataSet, +dataSet)])
 }
})());
var alpha = d3.select("#alpha").text("waiting..."),
  cog = d3.select("#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true),
  fdgInst = d3.select("#fdg");
elapsedTime = ElapsedTime("#panel", {margin: 0, padding: 0})
 .message(function (id) {
  return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap())
 });
elapsedTime.consoleOn = false;
//////////////////////////////////////////////////////////////////////////////////
var dataSets = [
 {
  "nodes": [
   {"name": "node1", "content": "the first Node"},
   {"name": "node2", "content": "node2"},
   {"name": "node3", "content":{"fa": "fa/*-spin*/", text: "\uf013"}},
   {"name": "node4", "content":{"fa": "fa/*-spin*/", text: "\uf1ce"}}
  ],
  "edges": [
   {"source": 2, "target": 0},
   {"source": 2, "target": 1},
   {"source": 2, "target": 3}
  ]
 },
 {
  "nodes": [
   {"name": "node1", "content": "node1"},
   {"name": "node2", "content":{"fa": "fa/*-spin*/", text: "\uf1ce"}},
   {"name": "node3", "content":{"fa": "fa/*-spin*/", text: "\uf013"}},
   {"name": "node4", "content": "4"},
   {"name": "node5", "content": "5"},
   {"name": "node6", "content": "6"}
  ],
  "edges": [
   {"source": 2, "target": 0},
   {"source": 2, "target": 1},
   {"source": 2, "target": 3},
   {"source": 2, "target": 4},
   {"source": 2, "target": 5}
  ]
 }
];
var refresh = (function(){
 var instID = Date.now(),
   height = 160,
   width = 500,
   force = d3.layout.force()
    .size([width, height])
    .charge(-1000)
    .linkDistance(50)
    .on("end", function(){cog.classed("fa-spin", false); elapsedTime.stop()})
    .on("start", function(){cog.classed("fa-spin", true); elapsedTime.start()});

 return function refresh(data) {
  force
   .nodes(data.nodes)
   .links(data.edges)
   .on("tick", (function(instID) {
    return function(e) {
     elapsedTime.mark();
     alpha.text(d3.format(" >8.4f")(e.alpha));
     fdgInst.text("fdg instance: " + instID);
     lines.attr("x1", function(d) {
      return d.source.x + d.source.cx + d.source.r;
     }).attr("y1", function(d) {
      return d.source.y + d.source.cy;
     }).attr("x2", function(d) {
      return d.target.x + d.target.cx;
     }).attr("y2", function(d) {
      return d.target.y + d.target.cy;
     });
     node.attr("transform", function(d) {
      return "translate(" + [d.x, d.y] + ")"
     });
    }
   })(instID))
   .start();

  var svg = d3.select("body").selectAll("svg").data([data]);

  svg.enter().append("svg")
   .attr({height: height, width: width});

  var lines = svg.selectAll(".links")
     .data(linksData),
    linesEnter = lines.enter()
     .insert("line", d3.select("#nodes") ? "#nodes" : null)
     .attr("class", "links")
     .attr({stroke: "steelblue", "stroke-width": 3});

  var nodes = svg.selectAll("#nodes").data(nodesData),
    nodesEnter = nodes.enter().append("g")
     .attr("id", "nodes"),
    node = nodes.selectAll(".node")
     .data(id),
    newNode = node.enter().append("g")
     .attr("class","node")
     .call(force.drag);
  newNode.append("text")
   .attr({class: "content", fill: "steelblue"})
  newNode.insert("circle", ".node .content");

  var glyphs = node.select("text")
     .each(function(d) {
      var node = d3.select(this);
      if(d.content.fa)
       node.style({'font-family': 'FontAwesome', 'font-size': '32px', 'dominant-baseline': 'central'})
        .classed(d.content.fa, true)
        .text(d.content.text);
      else
       node.text(d.content)
        .attr({"class": "content", style: null});
     })
     .call(getBB),
    backGround  = node.select("circle").each(function(d) {
    d3.select(this).attr(makeCircleBB(d))
   }).style({"fill": "red", opacity: 0.8});

  (function(id){
   //adjust the bounding box after the font loads
   var count = 0;
   d3.timer(function() {
    console.log(id);
    glyphs.call(getBB);
    backGround.each(function(d) {
     d3.select(this).attr(makeCircleBB(d))
    });
    return /*false || id != instID*/++count > 10; //needs to keep running due to webkit zoom bug
   })
  })(instID);

  lines.exit().remove();
  node.exit().remove();

  function nodesData(d) {
   return [d.nodes];
  }

  function linksData(d) {
   return d.edges;
  }
 };
 function getBB(selection) {
  this.each(function(d) {
   d.bb = this.getBBox();
  })
 }

 function makeCircleBB(d, i, j) {
  var bb = d.bb;
  d.r = Math.max(bb.width, bb.height) / 2;
  delete d.bb; //plug potential memory leak!
  d.cy = bb.height / 2 + bb.y;
  d.cx = bb.width / 2;
  return {
   r: d.r, cx: d.cx, cy: d.cy, height: bb.height, width: bb.width
  }
 }
 function id(d) {
  return d;
 }

})();
refresh(dataSets[0]);
svg {
      outline: 1px solid #282f51;
      pointer-events: all;
      overflow: visible;
    }

    g.outline {
      outline: 1px solid red;
    }

    #panel div {
      display: inline-block;
      margin: 0 .25em 3px 0;
    }

    #panel div div {
      white-space: pre;
      margin: 0 .25em 3px 0;
    }

    div#inputDiv {
      white-space: normal;
      display: inline-block;
    }

    .node {
      cursor: default;
    }

    .content {
      transform-origin: 50% 50%;
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-1.0.js"></script>
<div id="panel">
  <div id="inputDiv">
    <input id="update" type="button" value="update">
  </div>
  <div id="wrapAlpha">alpha:<div id="alpha"></div></div>
  <div id="fdg"></div>
</div>
<div id="viz"></div>