javascript - 根据D3图中的节点位置调整链路起点和终点

标签 javascript html d3.js svg

这是我的示例代码,它显示了一个简单的 d3 图,该图支持无需强制布局的节点拖动:

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
   .link {
   stroke: #aaa;
   }
   .node text {
   stroke:#333;
   cursos:pointer;
   }
   .node circle{
   stroke:#fff;
   stroke-width:3px;
   fill:#555;
   }
</style>
<body>
<p id="first"><p>
<p id="second"><p>

   <script>
      var data = {
        "nodes": [{
          "id": "source1",
          "x": 33,
          "y": 133,
          "width": 50,
          "height": 50
        },
          {
            "id": "target1",
            "x": 166,
            "y": 66,
            "width": 50,
            "height": 50
          },
          {
            "id": "source2",
            "x": 250,
            "y": 40,
            "width": 50,
            "height": 50
          },
          {
            "id": "target2",
            "x": 350,
            "y": 133,
            "width": 50,
            "height": 50
          }
        ],
        "links": [{
          "source": "source1",
          "target": "target1",
          "weight": 1,
          "id": "abc"
        },
          {
            "source": "source2",
            "target": "target2",
            "weight": 3,
            "id": "xyz"
          }
        ]
      };

      var width = 1200,
          height = 500
      var svg = d3.select("body").append("svg")
          .attr("width", width)
          .attr("height", height);

      var counterxOrtho = 0;
      // Bootstrap the Drag Capability
      var drag = d3.behavior.drag()
          .on("dragstart", dragstarted)
          .on("drag", dragged)
          .on("dragend", dragended);
      var dragInitiated = false

      function dragstarted(d) {

          d3.selectAll(".node").each(function(d) {
              d3.select(this).classed("selectedNode", function(d) {
                  return d.selected = false;
              })
          })

          d3.select(this).classed("selectedNode", function(d) {
              d.previouslySelected = d.selected;
              return d.selected = true;
          });

          dragInitiated = true

      }

      function dragged(d, i) {

          if (dragInitiated) {
              d3.event.sourceEvent.stopPropagation();

              d3.selectAll(".linkInGraph").attr("d", function(l) {

                  var sourceNode = data.nodes.filter(function(d, i) {
                      return d.id == l.source
                  })[0];

                  var targetNode = data.nodes.filter(function(d, i) {
                      return d.id == l.target
                  })[0];
          
                  if (!(sourceNode.selected || targetNode.selected)) {

                      lineData.length = 0;

                      controlPointsArr = [];

                      l.controlPoints.forEach(function(d) {
                          controlPointsArr.push(d);
                      })

                      for (i = 0; i < controlPointsArr.length; i += 2) {
                          lineData.push({
                              "a": controlPointsArr[i],
                              "b": controlPointsArr[i + 1]
                          });
                      }

                      return lineFunction(lineData)
                  }

                  lineData.length = 0;
                  controlPointsArr = [];
                  var randomVal = 0;

                  randomVal = 25;

                  lineData.push({
                      "a": sourceNode.x + randomVal,
                      "b": sourceNode.y + 50
                  });
                  controlPointsArr.push(sourceNode.x + randomVal);
                  controlPointsArr.push(sourceNode.y + 50);

                  lineData.push({
                      "a": targetNode.x + randomVal,
                      "b": targetNode.y - 8
                  });

                  controlPointsArr.push(targetNode.x + randomVal);

                  controlPointsArr.push(targetNode.y - 8);

                  l.controlPoints = [];
                  for (i = 0; i < controlPointsArr.length; i++) {
                      l.controlPoints.push(controlPointsArr[i]);
                  }

                  return lineFunction(lineData)

              })

              nodes.filter(function(d) {
                      return d.selected;
                  })
                  .each(function(d) {
                      
                      d.x += d3.event.dx;
                      d.y += d3.event.dy;

                      var a = d.id;
                      var b = "\"";
                      var position = 0;
                      var output = [a.slice(0, position), b, a.slice(position)].join('');
                      output += "\"";
                      d3.select("[id=" + output + "]").attr("transform", "translate(" + (d.x) + "," + (d.y) + ")");

                  });

          }

      }

      function dragended(d) {
          if (d3.event.sourceEvent.which == 1) {
              dragInitiated = false;
          }

      }

      var nodes = svg.selectAll(".node")
          .data(data.nodes)
          .enter().append("g").attr("id", function(d) {
              return d.id
          })
          .attr("class", "node").call(drag).attr("transform", function(d, i) {

              return "translate(" + d.x + "," + d.y + ")";
          });

      nodes.append("rect")
          .attr("width", "50").attr("height", "50").attr("fill", "lime").attr("rx", "5")
          .attr("ry", "5").style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "").style("opacity", ".9");

      nodes.append("text")
          .attr("dx", 12)
          .attr("dy", ".35em").attr("x", -12).attr("y", 25)
          .text(function(d) {
              return d.id
          });

      var LinkCurve = "linear";

      var lineFunction = d3.svg.line()
          .x(function(d) {
              return d.a;
          })
          .y(function(d) {
              return d.b;
          })
          .interpolate(LinkCurve);

      // Marker elements for edges
      var pathMarker = svg.append("marker").attr("id", "pathMarkerHead").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "0.1").attr("refY", "2");
      pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy")

      //

      //The data for our line
      var lineData = [];

      function setupPolyLinks() {

          d3.selectAll(".linkInGraph").remove();

          edges = svg.selectAll("linkInGraph")
              .data(data.links)
              .enter()
              .insert("path", ".node")
              .attr("class", "linkInGraph").attr("id", function(l) {
                  return l.id;
              }).attr("source", function(l) {
                  return l.source;
              }).attr("target", function(l) {
                  return l.target;
              }).attr("marker-end", "url(#pathMarkerHead)").attr("d", function(l) {
                  lineData.length = 0;
                  controlPointsArr = [];
                  var sourceNode = data.nodes.filter(function(d, i) {
                      return d.id == l.source
                  })[0];
                  var targetNode = data.nodes.filter(function(d, i) {
                      return d.id == l.target
                  })[0];

                  lineData.push({
                      "a": sourceNode.x + 25,
                      "b": sourceNode.y + 50
                  });
                  controlPointsArr.push(sourceNode.x + 25);
                  controlPointsArr.push(sourceNode.y + 50);

                  lineData.push({
                      "a": targetNode.x + 25,
                      "b": targetNode.y
                  });
                  controlPointsArr.push(targetNode.x + 25);
                  controlPointsArr.push(targetNode.y);
                  l.controlPoints = [];
                  for (i = 0; i < controlPointsArr.length; i++) {
                      l.controlPoints.push(controlPointsArr[i]);
                  }
                  return lineFunction(lineData)

              }).style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none");
      }
      setupPolyLinks();
   </script>
   

在此图中,拖动节点时,关联的链接始终在静态点开始和结束,即在这种情况下,链接从源的中下点开始,到目标的中上点结束。

我想要实现的是当拖动节点时,链接的起点和终点应该自动调整如下:

-如果目标在顶部,源在其下方,则链接应从源的顶部中间开始,到目标的底部中间结束。

但在我的例子中,它看起来像这样,我不希望这样:

wrong case 1

-如果源和目标在一条水平线上,先是源再是目标,那么链接应该从源的右中开始,到目标的左中结束。

在我的例子中是这样的:

wrong case 2

还有更多这样的案例......

这个想法是在拖动时链接永远不会与其自己的节点重叠

最佳答案

这是一个将链接附加到被拖动节点右侧的解决方案:

var data = {
  "nodes": [{
    "id": "source1",
    "x": 33,
    "y": 133,
    "width": 50,
    "height": 50
  },
    {
      "id": "target1",
      "x": 166,
      "y": 66,
      "width": 50,
      "height": 50
    },
    {
      "id": "source2",
      "x": 250,
      "y": 40,
      "width": 50,
      "height": 50
    },
    {
      "id": "target2",
      "x": 350,
      "y": 133,
      "width": 50,
      "height": 50
    }
  ],
  "links": [{
    "source": "source1",
    "target": "target1",
    "weight": 1,
    "id": "abc"
  },
    {
      "source": "source2",
      "target": "target2",
      "weight": 3,
      "id": "xyz"
    }
  ]
};

let svg = d3.select("svg").attr("width", 1200).attr("height", 500);

// nodes:

let nodes = svg.selectAll(".node")
  .data(data.nodes)
  .enter().append("g")
  .attr("id", d => d.id)
  .attr("class", "node")
  .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
  .call(d3.drag().on("drag", dragged));

nodes.append("rect")
  .attr("width", 50).attr("height", 50)
  .attr("fill", "lime")
  .attr("rx", 5).attr("ry", 5)
  .style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "")
  .style("opacity", ".9")
  .style("cursor", "pointer");

nodes.append("text")
  .attr("x", -12).attr("y", 25)
  .attr("dx", 12).attr("dy", ".35em")
  .text(d => d.id)
  .style("cursor", "pointer");

// links:

var pathMarker = svg.append("marker").attr("id", "pathMarkerHead").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "7").attr("refY", "2");
pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy");

svg.selectAll("linkInGraph")
  .data(data.links)
  .enter().append("path")
  .attr("class", "linkInGraph")
  .attr("id", d => d.id)
  .attr("d", moveLink)
  .style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none")
  .attr("marker-end", "url(#pathMarkerHead)");

// drag behavior:

function dragged(n) {

  // Move the node:
  d3.select(this)
    .attr(
      "transform",
      d => "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")"
    );

  // Move the link:
  d3.selectAll(".linkInGraph")
    .filter(l => l.source == n.id || l.target == n.id)
    .attr("d", moveLink)
}

// link position:

function moveLink(l) {

  let nsid = data.nodes.filter(n => n.id == l.source)[0].id;
  let ndid = data.nodes.filter(n => n.id == l.target)[0].id;

  let ns = d3.select("#" + nsid).datum();
  let nd = d3.select("#" + ndid).datum();

  let min = Number.MAX_SAFE_INTEGER;
  let best = {};
  [[25, 0], [50, 25], [25, 50], [0, 25]].forEach(s =>
    [[25, 0], [50, 25], [25, 50], [0, 25]].forEach(d => {
      let dist = Math.hypot(
        (nd.x + d[0]) - (ns.x + s[0]),
        (nd.y + d[1]) - (ns.y + s[1])
      );
      if (dist < min) {
        min = dist;
        best = {
          s: { x: ns.x + s[0], y: ns.y + s[1] },
          d: { x: nd.x + d[0], y: nd.y + d[1] }
        };
      }
    })
  );

  var lineFunction = d3.line().x(d => d.x).y(d => d.y).curve(d3.curveLinear);

  return lineFunction([best.s, best.d]);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

由于目标在于避免拖动的节点与其链接之间的重叠,我们必须将链接附加到其节点的适当一侧。

对于给定的链接,最佳节点的边就是最小化链接长度的边。

因此,我们的想法是计算链接在连接到它的两个节点边的所有组合时可以获得的 16 种大小;在我们的例子中是 [[25, 0], [50, 25], [25, 50], [0, 25]] 与自身的笛卡尔积(其中节点的宽度/高度是50,这个列表的每个元素都是一个节点边的中间坐标)。


请注意链接末尾 svg 标记的变化。我必须在链接内稍微翻译一下,以使箭头的头部与链接的末端重合,从而避免箭头位于节点内。


另请注意,我转而使用 d3v5 以避免再制作一个 d3v3 遗留示例(如有必要,切换回 d3v3 应该不会那么困难)。

关于javascript - 根据D3图中的节点位置调整链路起点和终点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50990010/

相关文章:

javascript - 使用 jQuery 淡入创建队列

html - 需要 css 对齐问题的解决方案/支持

javascript - 在 html 表格中将特定列设为粗体

jquery - 如何防止 li 中的溢出文本将右浮动文本推错位置?

javascript - 如何在 D3 JS 中制作色标以用于填充属性?

javascript - 如何运行 Mike Bostock 的 D3 示例?

javascript - 从外部来源覆盖 td 标签样式

javascript - jQuery Mobile 弹出动态内容不刷新

javascript - 如何拆分单体 node.js javascript

html - 如何放置窗口/导航栏 HTML 底部的 div - materializecss