【问题标题】:Exclude edges from participating in the layout从参与布局中排除边
【发布时间】:2022-01-07 17:02:32
【问题描述】:

考虑如下图所示:

我希望能够在用户单击按钮或类似按钮时显示/隐藏下面显示的红色边缘(忘记它们是手绘的):

我不希望红色边缘参与布局,而是让它们显示为一种覆盖。如果边缘可以尝试避免与路径中的任何节点重叠,那就太好了,但这绝对不是必需的。

我想如果我可以在边缘设置一个布尔标志,告诉布局引擎在布局设置中包含或排除它们,它就可以工作。我可以覆盖边缘上有一个physics 参数,但它似乎没有帮助 - 边缘仍然参与布局。

我可能还可以编写一些脚本来跟踪节点并在上面的另一个图中绘制红色边缘,但这正是我想要避免的。

【问题讨论】:

  • “参与布局”是什么意思?您是否使用布局引擎(graphviz 或类似引擎)将节点和蓝色链接发送到布局引擎。提取结果节点位置。绘制节点。绘制蓝色链接。绘制(或不绘制)红色链接。

标签: javascript networking graph overlay vis.js


【解决方案1】:

在可见网络 (options.layout.hierarchical.enabled = true) 中使用分层布局时,似乎没有实现此目的的选项。然而,这可以通过覆盖来实现。问题提到这是不需要的,但将其添加为选项。下面的帖子以及https://jsfiddle.net/7abovhtu/ 中包含了一个示例。

总之,该解决方案将覆盖画布放置在可见网络画布之上。由于 CSS pointer-events: none;,覆盖画布上的点击会传递到 vis 网络画布。使用节点的定位将额外的边缘绘制到覆盖画布上。对覆盖画布的更新由 vis 网络事件 afterDrawing 触发,该事件在网络发生变化(拖动、缩放等)时触发。

此答案利用答案https://stackoverflow.com/a/18363333/1620449 中提供的最接近椭圆计算的点来结束节点边缘的线。此答案还利用答案https://stackoverflow.com/a/6333775/1620449 中的函数在画布上绘制箭头。

// create an array with nodes
var nodes = new vis.DataSet([
  { id: 1, label: "Node 1" },
  { id: 2, label: "Node 2" },
  { id: 3, label: "Node 3" },
  { id: 4, label: "Node 4" },
  { id: 5, label: "Node 5" },
  { id: 6, label: "Node 6" },
  { id: 7, label: "Node 7" },
]);

// create an array with edges
var edges = new vis.DataSet([
  { from: 1, to: 2 },
  { from: 2, to: 3 },
  { from: 3, to: 4 },
  { from: 3, to: 5 },
  { from: 3, to: 6 },
  { from: 6, to: 7 }
]);

// create an array with extra edges displayed on button press
var extraEdges = [
  { from: 7, to: 5 },
  { from: 6, to: 1 }
];

// create a network
var container = document.getElementById("network");
var data = {
  nodes: nodes,
  edges: edges,
};
var options = {
  layout: {
    hierarchical: {
      enabled: true,
      direction: 'LR',
      sortMethod: 'directed',
      shakeTowards: 'roots'
    }
  }
};
var network = new vis.Network(container, data, options);

// Create an overlay for displaying extra edges
var overlayCanvas = document.getElementById("overlay");
var overlayContext = overlayCanvas.getContext("2d");

// Function called to draw the extra edges, called on initial display and
// when the network completes each draw (due to drag, zoom etc.)
function drawExtraEdges(){
  // Resize overlay canvas in case the continer has changed
  overlayCanvas.height = container.clientHeight;
  overlayCanvas.width = container.clientWidth;
  
  // Begin drawing path on overlay canvas
  overlayContext.beginPath();
  
  // Clear any existing lines from overlay canvas
  overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
  
  // Loop through extra edges to draw them
    extraEdges.forEach(edge => {
    // Gather the necessary coordinates for the start and end shapres
    const startPos = network.canvasToDOM(network.getPosition(edge.from));
    const endPos = network.canvasToDOM(network.getPosition(edge.to));
    const endBox = network.getBoundingBox(edge.to);
    
    // Determine the radius of the ellipse based on the scale of network
    // Start and end ellipse are presumed to be the same size
    const scale = network.getScale();
    const radiusX = ((endBox.right * scale) - (endBox.left * scale)) / 2;
    const radiusY = ((endBox.bottom * scale) - (endBox.top * scale)) / 2;
    
    // Get the closest point on the end ellipse to the start point
    const endClosest = getEllipsePt(endPos.x, endPos.y, radiusX, radiusY, startPos.x, startPos.y);
    
    // Now we have an end point get the point on the ellipse for the start
    const startClosest = getEllipsePt(startPos.x, startPos.y, radiusX, radiusY, endClosest.x, endClosest.y);
    
    // Draw arrow on diagram
    drawArrow(overlayContext, startClosest.x, startClosest.y, endClosest.x, endClosest.y);
  });
  
  // Apply red color to overlay canvas context
  overlayContext.strokeStyle = '#ff0000';
  
  // Make the line dashed
  overlayContext.setLineDash([10, 3]);
  
  // Apply lines to overlay canvas
  overlayContext.stroke();
}

// Adjust the positioning of the lines each time the network is redrawn
network.on("afterDrawing", function (event) {
  // Only draw the lines if they have been toggled on with the button
  if(extraEdgesShown){
    drawExtraEdges();
  }
});

// Add button event to show / hide extra edges
var extraEdgesShown = false; 
document.getElementById('extraEdges').onclick = function() {
  if(!extraEdgesShown){
    if(extraEdges.length > 0){
      // Call function to draw extra lines
      drawExtraEdges();
      extraEdgesShown = true;
    }
  } else {
    // Remove extra edges
    // Clear the overlay canvas
    overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
    extraEdgesShown = false;
  }
}

//////////////////////////////////////////////////////////////////////
// Elllipse closest point calculation
// https://stackoverflow.com/a/18363333/1620449
//////////////////////////////////////////////////////////////////////
var halfPI = Math.PI / 2;
var steps = 8; // larger == greater accuracy

// calc a point on the ellipse that is "near-ish" the target point
// uses "brute force"
function getEllipsePt(cx, cy, radiusX, radiusY, targetPtX, targetPtY) {
    // calculate which ellipse quadrant the targetPt is in
    var q;
    if (targetPtX > cx) {
        q = (targetPtY > cy) ? 0 : 3;
    } else {
        q = (targetPtY > cy) ? 1 : 2;
    }

    // calc beginning and ending radian angles to check
    var r1 = q * halfPI;
    var r2 = (q + 1) * halfPI;
    var dr = halfPI / steps;
    var minLengthSquared = 200000000;
    var minX, minY;

    // walk the ellipse quadrant and find a near-point
    for (var r = r1; r < r2; r += dr) {

        // get a point on the ellipse at radian angle == r
        var ellipseX = cx + radiusX * Math.cos(r);
        var ellipseY = cy + radiusY * Math.sin(r);

        // calc distance from ellipsePt to targetPt
        var dx = targetPtX - ellipseX;
        var dy = targetPtY - ellipseY;
        var lengthSquared = dx * dx + dy * dy;

        // if new length is shortest, save this ellipse point
        if (lengthSquared < minLengthSquared) {
            minX = ellipseX;
            minY = ellipseY;
            minLengthSquared = lengthSquared;
        }
    }

    return ({
        x: minX,
        y: minY
    });
}

//////////////////////////////////////////////////////////////////////
// Draw Arrow on Canvas Function
// https://stackoverflow.com/a/6333775/1620449
//////////////////////////////////////////////////////////////////////
function drawArrow(ctx, fromX, fromY, toX, toY) {
  var headLength = 10; // length of head in pixels
  var dX = toX - fromX;
  var dY = toY - fromY;
  var angle = Math.atan2(dY, dX);
  ctx.fillStyle = "red";
  ctx.moveTo(fromX, fromY);
  ctx.lineTo(toX, toY);
  ctx.lineTo(toX - headLength * Math.cos(angle - Math.PI / 6), toY - headLength * Math.sin(angle - Math.PI / 6));
  ctx.moveTo(toX, toY);
  ctx.lineTo(toX - headLength * Math.cos(angle + Math.PI / 6), toY - headLength * Math.sin(angle + Math.PI / 6));
}
#container {
  width: 100%;
  height: 80vh;
  border: 1px solid lightgray;
  position: relative;
}

#network, #overlay {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

#overlay {
  z-index: 100;
  pointer-events: none;
}
<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<button id="extraEdges">Toggle Extra Edges</button>
<div id="container">
  <div id="network"></div>
  <canvas width="600" height="400" id="overlay"></canvas>
</div>

【讨论】:

  • 正如您所建议的,这绝对不是我正在寻找的解决方案。另一方面,它看起来是迄今为止 best 的解决方案,我会接受这个答案,让问题得以解决,直到出现更好的解决方案。感谢您提供详尽的答案 - 我很感激!
【解决方案2】:

这可以使用额外边缘(红色边缘)上的physicshidden 选项来实现。作为参考,这些选项在https://visjs.github.io/vis-network/docs/network/edges.html 中有更详细的描述。

请注意,当使用可见网络选项options.layout.hierarchical.enabled = true中设置的分层布局时,以下选项不起作用。

物理 - 使用物理选项的示例是https://jsfiddle.net/6oac73p0。但是,正如您提到的,这可能会导致与启用了物理的节点重叠。在此示例中,额外的边缘设置为虚线,以确保所有内容仍然可见。

隐藏 - 使用隐藏选项的一个示例是 https://jsfiddle.net/xfcuvtgk/,也包含在下面的这篇文章中。在生成布局时,设置为隐藏的边缘仍然是物理计算的一部分,您提到这不是我们所希望的,但这确实意味着它们在以后显示时非常适合。

// create an array with nodes
var nodes = new vis.DataSet([
  { id: 1, label: "Node 1" },
  { id: 2, label: "Node 2" },
  { id: 3, label: "Node 3" },
  { id: 4, label: "Node 4" },
  { id: 5, label: "Node 5" },
]);

// create an array with edges
var edges = new vis.DataSet([
  { from: 1, to: 3 },
  { from: 1, to: 2 },
  { from: 2, to: 4 },
  { from: 2, to: 5 },
  { from: 3, to: 3 },
  { from: 4, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true },
  { from: 3, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true },
  { from: 1, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true }
]);

// create a network
var container = document.getElementById("mynetwork");
var data = {
  nodes: nodes,
  edges: edges,
};
var options = {};
var network = new vis.Network(container, data, options);

document.getElementById('extraEdges').onclick = function() {
    // Extract the list of extra edges
  edges.forEach(function(edge){
    if(edge.extra){
        // Toggle the hidden value
      edge.hidden = !edge.hidden;
      
      // Update edge back onto data set
      edges.update(edge);
    }
  });
}
#mynetwork {
  width: 600px;
  /* Height adjusted for Stack Overflow inline demo */
  height: 160px;
  border: 1px solid lightgray;
}
<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<button id="extraEdges">Show/Hide Extra Edges</button>
<div id="mynetwork"></div>

【讨论】:

  • 嗨,克里斯,我真的很喜欢你的回答,但它只有在我不使用布局时才有效。一旦我在options 中配置了布局,添加额外边时布局就会更新。
  • 如果您指的是在options.layout.hierarchical.enabled 设置的分层布局,我明白您的意思,无论这些设置如何,当显示新边时,这似乎总是重新计算。这是您正在使用的选项,还是其他布局选项?
  • 我确实在使用分层布局。据我所知,布局代码没有考虑到额外边缘上的physics: false。我有changed 你的例子,如果没有物理应用到额外的边缘,我希望node 5 在稳定时恢复到原来的位置——但事实并非如此。也许layout: true/false 属性是这里最合适的解决方案?
  • 再看一点,如果不创建叠加层,我看不到任何方法。无论如何,网络都会维护所有边的分层布局。
  • 我整理了一个使用叠加层的快速示例,因此适用于分层布局,但如果这不是您感兴趣的方法,那是公平的。我已将其作为单独的答案发布,因为我相信此答案可能仍然有用,而且它是一种截然不同的方法。
猜你喜欢
  • 2014-05-24
  • 2018-07-16
  • 2016-04-16
  • 2019-01-04
  • 2020-04-11
  • 1970-01-01
  • 2021-09-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多