使用d3.forcesimulation()的D3 selfnode链接

时间:2017-05-07 11:35:27

标签: javascript d3.js

我有两个问题。首先,我使用此示例(http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4)来创建d3图并尝试创建自身节点链接到同一节点,但我不知道如何。

var width = 960,
          height = 500,
          nodeSize = 20,
          arrowWidth = 8,
          svg = d3.select("body")
            .append("svg")
            .attr("width", width)
            .attr("height", height)
          linkG = svg.append("g")
          nodeG = svg.append("g")
          // Arrows are separate from link lines so that their size
          // can be controlled independently from the link lines.
          arrowG = svg.append("g");
      
      // Arrowhead setup.
      // Draws from Mobile Patent Suits example:
      // http://bl.ocks.org/mbostock/1153292
      svg.append("defs")
        .append("marker")
          .attr("id", "arrow")
          .attr("orient", "auto")
          .attr("preserveAspectRatio", "none")
          // See also http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
          //.attr("viewBox", "0 -" + arrowWidth + " 10 " + (2 * arrowWidth))
          .attr("viewBox", "0 -5 10 10")
          // See also http://www.w3.org/TR/SVG/painting.html#MarkerElementRefXAttribute
          .attr("refX", 10)
          .attr("refY", 0)
          .attr("markerWidth", 10)
          .attr("markerHeight", arrowWidth)
        .append("path")
          .attr("d", "M0,-5L10,0L0,5");
      
      var simulation = d3.forceSimulation()
        .force("link", d3.forceLink())
        .force("charge", d3.forceManyBody())
        .force("center", d3.forceCenter(width / 2, height / 2));
      
      simulation.force("link")
        .distance(140);
      
      var drag = d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended);
      
      function dragstarted(d) {
        if (!d3.event.active) simulation.alphaTarget(0.3).restart()
        simulation.fix(d);
      }
      
      function dragged(d) {
        simulation.fix(d, d3.event.x, d3.event.y);
      }
      
      function dragended(d) {
        if (!d3.event.active) simulation.alphaTarget(0);
      }
      
      function render(graph){
        
        var link = linkG.selectAll("line").data(graph.links);
        var linkEnter = link.enter().append("line")
          .attr("class", "link-line");
        link.exit().remove();
        link = link.merge(linkEnter);
        
        var arrow = arrowG.selectAll("line").data(graph.links);
        var arrowEnter = arrow.enter().append("line")
          .attr("class", "arrow")
          .attr("marker-end", "url(#arrow)" );
        arrow.exit().remove();
        arrow = arrow.merge(arrowEnter);
        
        var node = nodeG.selectAll("g").data(graph.nodes);
        var nodeEnter = node.enter().append("g").call(drag);
        node.exit().remove();
        
        nodeEnter.append("rect")
            .attr("class", "node-rect")
          .attr("y", -nodeSize)
          .attr("height", nodeSize * 2)
          .attr("rx", nodeSize)
          .attr("ry", nodeSize)
          .on("click", function (d){
            simulation.unfix(d);
          });
        
        nodeEnter.append("text")
          .attr("class", "node-text");
        
        node = node.merge(nodeEnter);
        
        node.select(".node-text")
          .text(function (d){ return d.name; })
          .each(function (d) {
          
            var circleWidth = nodeSize * 2,
                textLength = this.getComputedTextLength(),
                textWidth = textLength + nodeSize;
          
            if(circleWidth > textWidth) {
              d.isCircle = true;
              d.rectX = -nodeSize;
              d.rectWidth = circleWidth;
            } else {
              d.isCircle = false;
              d.rectX = -(textLength + nodeSize) / 2;
              d.rectWidth = textWidth;
              d.textLength = textLength;
            }
          });
        
        node.select(".node-rect")
          .attr("x", function(d) { return d.rectX; })
          .attr("width", function(d) { return d.rectWidth; });
        
        simulation.force("link").links(graph.links);
        
        simulation.nodes(graph.nodes).on("tick", function (){
          
          graph.nodes.forEach(function (d) {
            if(d.isCircle){
              d.leftX = d.rightX = d.x;
            } else {
              d.leftX =  d.x - d.textLength / 2 + nodeSize / 2;
              d.rightX = d.x + d.textLength / 2 - nodeSize / 2;
            }
          });
          
          link.call(edge);
          arrow.call(edge);
          
          node.attr("transform", function(d) {      
            return "translate(" + d.x + "," + d.y + ")";
          });
        });
      }
      
      // Sets the (x1, y1, x2, y2) line properties for graph edges.
      function edge(selection){
        selection
          .each(function (d) {
            var sourceX, targetX, midX, dy, dy, angle;
          
            // This mess makes the arrows exactly perfect.
            if( d.source.rightX < d.target.leftX ){
              sourceX = d.source.rightX;
              targetX = d.target.leftX;
            } else if( d.target.rightX < d.source.leftX ){
              targetX = d.target.rightX;
              sourceX = d.source.leftX;
            } else if (d.target.isCircle) {
              targetX = sourceX = d.target.x;
            } else if (d.source.isCircle) {
              targetX = sourceX = d.source.x;
            } else {
              midX = (d.source.x + d.target.x) / 2;
              if(midX > d.target.rightX){
                midX = d.target.rightX;
              } else if(midX > d.source.rightX){
                midX = d.source.rightX;
              } else if(midX < d.target.leftX){
                midX = d.target.leftX;
              } else if(midX < d.source.leftX){
                midX = d.source.leftX;
              }
              targetX = sourceX = midX;
            }
          
            dx = targetX - sourceX;
            dy = d.target.y - d.source.y;
            angle = Math.atan2(dx, dy);
          
            // Compute the line endpoint such that the arrow
            // is touching the edge of the node rectangle perfectly.
            d.sourceX = sourceX + Math.sin(angle) * nodeSize;
            d.targetX = targetX - Math.sin(angle) * nodeSize;
            d.sourceY = d.source.y + Math.cos(angle) * nodeSize;
            d.targetY = d.target.y - Math.cos(angle) * nodeSize;
          })
          .attr("x1", function(d) { return d.sourceX; })
          .attr("y1", function(d) { return d.sourceY; })
          .attr("x2", function(d) { return d.targetX; })
          .attr("y2", function(d) { return d.targetY; });
      }
      
      var graph = {"nodes":["socks","shoes","shirt","belt","tie","jacket","pants","underpants"],"links":[{"source":0,"target":1},{"source":2,"target":3},{"source":2,"target":4},{"source":3,"target":5},{"source":4,"target":5},{"source":6,"target":1},{"source":6,"target":3},{"source":7,"target":6}]};
      
      graph.nodes = graph.nodes.map(function (d){
        return { name: d };
      });
      graph.links = graph.links.map(function (d){
        d.source = graph.nodes[d.source];
        d.target = graph.nodes[d.target];
        return d;
      });
      render(graph);
.node-rect {
        fill: white;
        stroke: black;
        stroke-width: 1.5;
        cursor: move;
      }
      
      .node-text {
        font-family: "Roboto", sans-serif;
        font-size: 2em;
        text-anchor: middle;
        alignment-baseline: middle;
        pointer-events: none;
        /* Disable text selection
           from http://stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting */
        -webkit-touch-callout: none; /* iOS Safari */
        -webkit-user-select: none;   /* Chrome/Safari/Opera */
        -khtml-user-select: none;    /* Konqueror */
        -moz-user-select: none;      /* Firefox */
        -ms-user-select: none;       /* Internet Explorer/Edge */
        user-select: none;
      }
      
      .link-line {
        stroke: black;
        stroke-width: 1.5;
      }
      
      /* Set the arrowhead size. */
      .arrow {
        stroke-width: 1.5px;
      }
<script src="https://d3js.org/d3.v4.0.0-alpha.40.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" type="text/css">

我想要这样的东西https://jsfiddle.net/dhxc6fr3/(selfnode有红色) 或者(这是第二个问题)是在所有画布区域上改变那个奇怪的移动元素(节点和链接)的方法吗?我的意思是,图表将在一个地方生成而没有这种奇怪的移动(因此,我认为该坐标是实时计算的)

抱歉我的英文不好

1 个答案:

答案 0 :(得分:2)

您的第一个问题是您的工作示例使用line元素绘制链接,而您的第二个示例(绘制自节点链接)正在使用路径。您需要先修改工作示例才能使用path(因为您无法使用line绘制曲线)。然后,检查它是否是if (d.source === d.target)的自我节点并相应地绘制链接是非常微不足道的:

if (d.source === d.target){
 return "M" + sourceX + "," + sourceY + "A" + 40 + "," + 40 + " " + -45 + "," + 1 + "," + 0 + " " + (sourceX - 1) + "," + (sourceY + 1); 
} else {
  return "M" + sourceX + "," + sourceY + "L" + targetX + "," + targetY;
} 

&#34;魔法&#34;我选择绘制曲线的数字大多是从试验和误差中推导出来的,以使自节点曲线看起来很好。

完整示例:

&#13;
&#13;
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

    <!--
    
      This program is a tool for visualizing small directed graphs.
      Inspired by:
        Force Dragging I
        http://bl.ocks.org/mbostock/2675ff61ea5e063ede2b5d63c08020c7
        Reactive Flow Diagram
        http://bl.ocks.org/curran/5905182da50a4667dc00
         
      Curran Kelleher May 2016
    -->

    <meta name="viewport" content="width=device-width">
    <title>Graph Editor</title>
    <script src="https://d3js.org/d3.v4.0.0-alpha.40.min.js"></script>
    <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" type="text/css">
    <style>
      
      .node-rect {
        fill: white;
        stroke: black;
        stroke-width: 1.5;
        cursor: move;
      }
      
      .node-text {
        font-family: "Roboto", sans-serif;
        font-size: 2em;
        text-anchor: middle;
        alignment-baseline: middle;
        pointer-events: none;
        /* Disable text selection
           from http://stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting */
        -webkit-touch-callout: none; /* iOS Safari */
        -webkit-user-select: none;   /* Chrome/Safari/Opera */
        -khtml-user-select: none;    /* Konqueror */
        -moz-user-select: none;      /* Firefox */
        -ms-user-select: none;       /* Internet Explorer/Edge */
        user-select: none;
      }
      
      .link-line {
        stroke: black;
        stroke-width: 1.5;
        fill: none;
      }
      
      /* Set the arrowhead size. */
      .arrow {
        stroke-width: 1.5px;
        fill: none;
      }
    </style>
  </head>
  <body>
    <script>
      
      var width = 960,
          height = 500,
          nodeSize = 20,
          arrowWidth = 8,
          svg = d3.select("body")
            .append("svg")
            .attr("width", width)
            .attr("height", height)
          linkG = svg.append("g")
          nodeG = svg.append("g")
          // Arrows are separate from link lines so that their size
          // can be controlled independently from the link lines.
          arrowG = svg.append("g");
      
      // Arrowhead setup.
      // Draws from Mobile Patent Suits example:
      // http://bl.ocks.org/mbostock/1153292
      svg.append("defs")
        .append("marker")
          .attr("id", "arrow")
          .attr("orient", "auto")
          .attr("preserveAspectRatio", "none")
          // See also http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
          //.attr("viewBox", "0 -" + arrowWidth + " 10 " + (2 * arrowWidth))
          .attr("viewBox", "0 -5 10 10")
          // See also http://www.w3.org/TR/SVG/painting.html#MarkerElementRefXAttribute
          .attr("refX", 10)
          .attr("refY", 0)
          .attr("markerWidth", 10)
          .attr("markerHeight", arrowWidth)
        .append("path")
          .attr("d", "M0,-5L10,0L0,5");
      
      var simulation = d3.forceSimulation()
        .force("link", d3.forceLink())
        .force("charge", d3.forceManyBody())
        .force("center", d3.forceCenter(width / 2, height / 2));
      
      simulation.force("link")
        .distance(140);
      
      var drag = d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended);
      
      function dragstarted(d) {
        if (!d3.event.active) simulation.alphaTarget(0.3).restart()
        simulation.fix(d);
      }
      
      function dragged(d) {
        simulation.fix(d, d3.event.x, d3.event.y);
      }
      
      function dragended(d) {
        if (!d3.event.active) simulation.alphaTarget(0);
      }
      
      function render(graph){
        
        var link = linkG.selectAll("path").data(graph.links);
        var linkEnter = link.enter().append("path")
          .attr("class", "link-line");
        link.exit().remove();
        link = link.merge(linkEnter);
        
        var arrow = arrowG.selectAll("path").data(graph.links);
        var arrowEnter = arrow.enter().append("path")
          .attr("class", "arrow")
          .attr("marker-end", "url(#arrow)" );
        arrow.exit().remove();
        arrow = arrow.merge(arrowEnter);
        
        var node = nodeG.selectAll("g").data(graph.nodes);
        var nodeEnter = node.enter().append("g").call(drag);
        node.exit().remove();
        
        nodeEnter.append("rect")
            .attr("class", "node-rect")
          .attr("y", -nodeSize)
          .attr("height", nodeSize * 2)
          .attr("rx", nodeSize)
          .attr("ry", nodeSize)
          .on("click", function (d){
            simulation.unfix(d);
          });
        
        nodeEnter.append("text")
          .attr("class", "node-text");
        
        node = node.merge(nodeEnter);
        
        node.select(".node-text")
          .text(function (d){ return d.name; })
          .each(function (d) {
          
            var circleWidth = nodeSize * 2,
                textLength = this.getComputedTextLength(),
                textWidth = textLength + nodeSize;
          
            if(circleWidth > textWidth) {
              d.isCircle = true;
              d.rectX = -nodeSize;
              d.rectWidth = circleWidth;
            } else {
              d.isCircle = false;
              d.rectX = -(textLength + nodeSize) / 2;
              d.rectWidth = textWidth;
              d.textLength = textLength;
            }
          });
        
        node.select(".node-rect")
          .attr("x", function(d) { return d.rectX; })
          .attr("width", function(d) { return d.rectWidth; });
        
        simulation.force("link").links(graph.links);
        
        simulation.nodes(graph.nodes).on("tick", function (){
          
          graph.nodes.forEach(function (d) {
            if(d.isCircle){
              d.leftX = d.rightX = d.x;
            } else {
              d.leftX =  d.x - d.textLength / 2 + nodeSize / 2;
              d.rightX = d.x + d.textLength / 2 - nodeSize / 2;
            }
          });
          
          link.call(edge);
          arrow.call(edge);
          
          node.attr("transform", function(d) {      
            return "translate(" + d.x + "," + d.y + ")";
          });
        });
      }
      
      // Sets the (x1, y1, x2, y2) line properties for graph edges.
      function edge(selection){
        selection
          .attr("d", function (d) {          
         
              var sourceX, targetX, midX, dy, dy, angle;

              // This mess makes the arrows exactly perfect.
              if( d.source.rightX < d.target.leftX ){
                sourceX = d.source.rightX;
                targetX = d.target.leftX;
              } else if( d.target.rightX < d.source.leftX ){
                targetX = d.target.rightX;
                sourceX = d.source.leftX;
              } else if (d.target.isCircle) {
                targetX = sourceX = d.target.x;
              } else if (d.source.isCircle) {
                targetX = sourceX = d.source.x;
              } else {
                midX = (d.source.x + d.target.x) / 2;
                if(midX > d.target.rightX){
                  midX = d.target.rightX;
                } else if(midX > d.source.rightX){
                  midX = d.source.rightX;
                } else if(midX < d.target.leftX){
                  midX = d.target.leftX;
                } else if(midX < d.source.leftX){
                  midX = d.source.leftX;
                }
                targetX = sourceX = midX;
              }

              dx = targetX - sourceX;
              dy = d.target.y - d.source.y;
              angle = Math.atan2(dx, dy);

              // Compute the line endpoint such that the arrow
              // is touching the edge of the node rectangle perfectly.
              sourceX = sourceX + Math.sin(angle) * nodeSize;
              targetX = targetX - Math.sin(angle) * nodeSize;
              sourceY = d.source.y + Math.cos(angle) * nodeSize;
              targetY = d.target.y - Math.cos(angle) * nodeSize;
              
          	if (d.source === d.target){
              return "M" + sourceX + "," + sourceY + "A" + 40 + "," + 40 + " " + -45 + "," + 1 + "," + 0 + " " + (sourceX - 1) + "," + (sourceY + 1); 
            } else {
              return "M" + sourceX + "," + sourceY + "L" + targetX + "," + targetY;
            }       
          	
          });
      }
      
      var graph = {"nodes":["socks","shoes","shirt","belt","tie","jacket","pants","underpants"],"links":[{"source":0,"target":1},{"source":2,"target":3},{"source":2,"target":4},{"source":3,"target":5},{"source":4,"target":5},{"source":6,"target":1},{"source":6,"target":3},{"source":7,"target":6},{"source":7,"target":7},{"source":1,"target":1},{"source":2,"target":2},{"source":3,"target":3},{"source":4,"target":4},{"source":5,"target":5},{"source":6,"target":6}]};
      
      graph.nodes = graph.nodes.map(function (d){
        return { name: d };
      });
      graph.links = graph.links.map(function (d){
        d.source = graph.nodes[d.source];
        d.target = graph.nodes[d.target];
        return d;
      });
      render(graph);
    </script>
  </body>
</html>
&#13;
&#13;
&#13;

关于第二个问题,请参阅此示例here。关键是:

// See https://github.com/d3/d3-force/blob/master/README.md#simulation_tick
for (var i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) {
  simulation.tick();
}

这基本上只是在渲染图形之前运行模拟完成。