gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
252 lines (233 loc) • 12.2 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="A custom Link routing that goes smoothly through a sequence of Nodes."/>
<link rel="stylesheet" href="../assets/css/style.css"/>
<!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
<title>Multi-Node Path Links</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary">
<div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2">
<div class="md:pl-4">
<a class="text-white hover:text-white no-underline hover:no-underline
font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../">
<h1 class="my-0 p-1 ">GoJS</h1>
</a>
</div>
<button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation">
<svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
<path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path>
<path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
<div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20">
<ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0">
<li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li>
</ul>
</div>
</div>
<hr class="border-b border-gray-600 opacity-50 my-0 py-0" />
</nav>
<div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
<div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div>
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="../release/go.js"></script>
<div id="allSampleContent" class="p-4 w-full">
<script id="code">
function init() {
// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make;
myDiagram =
new go.Diagram("myDiagramDiv", // the ID of the DIV HTML element
{
allowCopy: false, // would need to copy linkdata.path and update all of the refenced node keys
allowDelete: false, // would need to update linkdata.path for all links going through that node
"Changed": invalidateLinkRoutes,
"undoManager.isEnabled": true
});
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse),
$(go.Shape,
{ figure: "Circle", fill: "white" },
new go.Binding("fill", "color")),
$(go.TextBlock,
{ font: "bold 11pt sans-serif" },
new go.Binding("text"))
);
myDiagram.linkTemplate =
$(MultiNodePathLink, // subclass of Link, defined below
go.Link.Bezier,
{ layerName: "Background", toShortLength: 4 },
$(go.Shape, { strokeWidth: 4 },
new go.Binding("stroke", "color")),
$(go.Shape, { toArrow: "Standard", scale: 3, strokeWidth: 0 },
new go.Binding("fill", "color"))
);
function invalidateLinkRoutes(e) {
// when a Node is moved, invalidate the route for all MultiNodePathLinks that go through it
if (e.change === go.ChangedEvent.Property && e.propertyName === "location" && e.object instanceof go.Node) {
const diagram = e.diagram;
const node = e.object;
if (node._PathLinks) {
node._PathLinks.each(l => l.invalidateRoute());
}
} else if (e.change === go.ChangedEvent.Remove && e.object instanceof go.Layer) {
// when a Node is deleted that has MultiNodePathLinks going through it, invalidate those link routes
if (e.oldValue instanceof go.Node) {
const node = e.oldValue;
if (node._PathLinks) {
node._PathLinks.each(l => l.invalidateRoute());
}
} else if (e.oldValue instanceof MultiNodePathLink) {
// when deleting a MultiNodePathLink, remove all references to it in Node._PathLinks
const link = e.oldValue;
const diagram = e.diagram;
const midkeys = link.data.path;
if (Array.isArray(midkeys)) {
for (let i = 0; i < midkeys.length; i++) {
const node = diagram.findNodeForKey(midkeys[i]);
if (node !== null && node._PathLinks) node._PathLinks.remove(link);
}
}
}
}
}
// create a few nodes and links
myDiagram.model = new go.GraphLinksModel([
{ key: 1, text: "Alpha", color: "lightyellow", loc: "0 0" },
{ key: 2, text: "Beta", color: "brown", loc: "200 0" },
{ key: 3, text: "Gamma", color: "green", loc: "300 100" },
{ key: 4, text: "Delta", color: "slateblue", loc: "100 200" },
{ key: 5, text: "Epsilon", color: "aquamarine", loc: "300 350" },
{ key: 6, text: "Zeta", color: "tomato", loc: "0 100" },
{ key: 7, text: "Eta", color: "goldenrod", loc: "0 300" },
{ key: 8, text: "Theta", color: "orange", loc: "300 200" },
], [
{ from: 1, to: 5, path: [2, 3, 4], color: "blue" },
{ from: 6, to: 5, path: [7, 4, 8], color: "red" }
]);
}
class MultiNodePathLink extends go.Link {
// ignores this.routing, this.adjusting, this.corner, this.smoothness, this.curviness
computePoints() {
// get the list of Nodes that should be along the path
const nodes = [];
if (this.fromNode !== null && this.fromNode.location.isReal()) {
nodes.push(this.fromNode);
}
const midkeys = this.data.path;
if (Array.isArray(midkeys)) {
const diagram = this.diagram;
for (let i = 0; i < midkeys.length; i++) {
const node = diagram.findNodeForKey(midkeys[i]);
if (node instanceof go.Node && node.location.isReal()) {
nodes.push(node);
// Optimization?: remember on each path Node all of
// the MultiNodePathLinks that go through it;
// but this optimization requires maintaining this cache
// in a Diagram Changed event listener.
let set = node._PathLinks;
if (!set) set = node._PathLinks = new go.Set(/*go.Link*/);
set.add(this);
}
}
}
if (this.toNode !== null && this.toNode.location.isReal()) {
nodes.push(this.toNode);
}
// now do the routing
this.clearPoints();
let prevloc = null;
let thisloc = null;
let nextloc = null;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
thisloc = node.location;
nextloc = (i < nodes.length - 1) ? nodes[i + 1].location : null;
let prevpt = null;
let nextpt = null;
if (this.curve === go.Link.Bezier) {
if (prevloc !== null && nextloc !== null) {
const prevang = thisloc.directionPoint(prevloc);
const nextang = thisloc.directionPoint(nextloc);
let avg = (prevang + nextang) / 2;
let clockwise = prevang > nextang;
if (Math.abs(prevang - nextang) > 180) {
avg += 180;
clockwise = !clockwise;
}
if (avg >= 360) avg -= 360;
prevpt = new go.Point(Math.sqrt(thisloc.distanceSquaredPoint(prevloc)) / 4, 0);
prevpt.rotate(avg + (clockwise ? 90 : -90));
prevpt.add(thisloc);
nextpt = new go.Point(Math.sqrt(thisloc.distanceSquaredPoint(nextloc)) / 4, 0);
nextpt.rotate(avg - (clockwise ? 90 : -90));
nextpt.add(thisloc);
} else if (nextloc !== null) {
prevpt = null;
nextpt = thisloc; // fix this point after the loop
} else if (prevloc !== null) {
prevpt = thisloc; // fix this point after the loop
nextpt = null;
}
}
if (prevpt !== null) this.addPoint(prevpt);
this.addPoint(thisloc);
if (nextpt !== null) this.addPoint(nextpt);
prevloc = thisloc;
}
// fix up the end points when it's Bezier
if (this.curve === go.Link.Bezier) {
// fix up the first point and the first control point
const start = this.getLinkPointFromPoint(this.fromNode, this.fromPort, this.fromPort.getDocumentPoint(go.Spot.Center), this.getPoint(3), true);
const ctrl2 = this.getPoint(2);
this.setPoint(0, start);
this.setPoint(1, new go.Point((start.x * 3 + ctrl2.x) / 4, (start.y * 3 + ctrl2.y) / 4));
// fix up the last point and the last control point
const end = this.getLinkPointFromPoint(this.toNode, this.toPort, this.toPort.getDocumentPoint(go.Spot.Center), this.getPoint(this.pointsCount - 4), false);
const ctrl1 = this.getPoint(this.pointsCount - 3);
this.setPoint(this.pointsCount - 2, new go.Point((end.x * 3 + ctrl1.x) / 4, (end.y * 3 + ctrl1.y) / 4));
this.setPoint(this.pointsCount - 1, end);
}
return true;
}
}
// end MultiNodePathLink class
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px; min-width: 200px"></div>
<p>
This sample demonstrates customization of the <a>Link</a>'s routing to go through multiple Nodes.
The nodes are specified by key in the link data's "path" property, which must be an Array of node keys.
</p>
<p>
As the user drags around Nodes on the "path", the routing is automatically recomputed to maintain a smooth curve.
</p>
</div>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
</body>
<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>