从参与布局中排除边缘

Exclude edges from participating in the layout

考虑如下所示的图表:

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

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

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

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

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

中有更详细的描述

请注意,当按照 Vis Network 选项中的设置使用分层布局时,以下选项不起作用 options.layout.hierarchical.enabled = true

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

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

// 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>

在 vis 网络 (options.layout.hierarchical.enabled = true) 中使用分层布局时,似乎没有实现此目的的选项。然而,这可以通过覆盖来实现。问题提到这不是我们想要的,而是将其添加为一个选项。下面的 post 和 https://jsfiddle.net/7abovhtu/.

中包含一个示例

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

此答案使用答案 to end the lines at the edge of the nodes. This answer also makes use of the function in the answer 中提供的最接近椭圆计算的点在 canvas 上绘制箭头。

// 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
// 
//////////////////////////////////////////////////////////////////////
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
// 
//////////////////////////////////////////////////////////////////////
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>