gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
489 lines (438 loc) • 21 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="An example of virtualization where a virtualized ForceDirectedLayout sets the bounds for each node data."/>
<link rel="stylesheet" href="../assets/css/style.css"/>
<!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
<title>Virtualized Graph with ForceDirectedLayout</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; // for conciseness in defining templates
// The Diagram just shows what should be visible in the viewport.
// Its model does NOT include node data for the whole graph, but only that
// which might be visible in the viewport.
myDiagram =
new go.Diagram("myDiagramDiv",
{
// use a virtualized ForceDirectedLayout which does not require
// that the Nodes and Links exist first for an accurate layout
layout: $(VirtualizedForceDirectedLayout,
{ defaultSpringLength: 10, maxIterations: 15 }),
// Define the template for Nodes, used by virtualization.
nodeTemplate:
$(go.Node, "Auto",
{ isLayoutPositioned: false }, // optimization
new go.Binding("position", "bounds", b => b.position)
.makeTwoWay((p, d) => new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height)),
{ width: 70, height: 20 }, // in cooperation with the load function, below
$(go.Shape, "Rectangle",
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 2 },
new go.Binding("text", "color")),
{
toolTip:
$("ToolTip",
$(go.TextBlock, { margin: 3 },
new go.Binding("text", "", d => "key: " + d.key + "\nbounds: " + d.bounds.toString()))
)
}
),
// Define the template for Links
linkTemplate:
$(go.Link,
{ isLayoutPositioned: false }, // optimization
$(go.Shape)
),
"animationManager.isEnabled": false
});
// This model includes all of the data
myWholeModel = new go.GraphLinksModel(); // must match the model used by the Diagram, below
// The virtualized layout works on the full model, not on the Diagram Nodes and Links
myDiagram.layout.model = myWholeModel;
// Do not set myDiagram.model = myWholeModel -- that would create a zillion Nodes and Links!
// In the future Diagram may have built-in support for virtualization.
// For now, we have to implement virtualization ourselves by having the Diagram's model
// be different than the "real" model.
myDiagram.model = // this only holds nodes that should be in the viewport
new go.GraphLinksModel(); // must match the model, above
// for now, we have to implement virtualization ourselves
myDiagram.isVirtualized = true;
myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);
// once the layout has finished we can decide where to position the viewport
myDiagram.addDiagramListener("InitialLayoutCompleted", e => {
var firstdata = myWholeModel.findNodeDataForKey(0);
if (firstdata !== null) {
myDiagram.centerRect(firstdata.bounds);
}
});
myDiagram.delayInitialization(diagram => spinDuring(diagram, "mySpinner", load));
}
// implement a wait spinner in HTML with CSS animation
function spinDuring(diagram, spinner, compute) { // where compute is a function of zero args
// show the animated spinner
if (typeof spinner === "string") spinner = document.getElementById(spinner);
if (spinner) {
// position it in the middle of the viewport DIV
var x = Math.floor(diagram.div.offsetWidth/2 - spinner.naturalWidth/2);
var y = Math.floor(diagram.div.offsetHeight/2 - spinner.naturalHeight/2);
spinner.style.left = x + "px";
spinner.style.top = y + "px";
spinner.style.display = "inline";
}
setTimeout(() => {
try {
compute(); // do the computation
} finally {
if (spinner) spinner.style.display = "none";
}
}, 20);
}
function load() {
// create a lot of data for the myWholeModel
generateNodes(myWholeModel, 2345, 2345);
generateLinks(myWholeModel, 1, 5);
myDiagram.layoutDiagram(true);
}
// Creates a random number of randomly colored nodes.
function generateNodes(model, min, max) {
if (isNaN(min) || min < 0) min = 2;
if (isNaN(max) || max < min) max = min;
var nodeArray = [];
var numNodes = Math.floor(Math.random() * (max - min + 1)) + min;
for (var i = 0; i < numNodes; i++) {
var d = {
key: i,
color: go.Brush.randomColor() // the node's color
};
//!!!???@@@ this needs to be customized to account for your chosen Node template
d.bounds = new go.Rect(0, 0, 70, 20);
nodeArray.push(d);
}
model.nodeDataArray = nodeArray;
}
// Takes the random collection of nodes and creates a random tree with them.
// Respects the minimum and maximum number of links from each node.
// (The minimum can be disregarded if we run out of nodes to link to)
function generateLinks(model, min, max) {
if (model.nodeDataArray.length < 2) return;
if (isNaN(min) || min < 1) min = 1;
if (isNaN(max) || max < min) max = min;
var linkArray = [];
// make two Lists of nodes to keep track of where links already exist
var nodes = new go.List();
nodes.addAll(model.nodeDataArray);
var available = new go.List();
available.addAll(nodes);
for (var i = 0; i < nodes.length; i++) {
var next = nodes.get(i);
available.delete(next)
var children = Math.floor(Math.random() * (max - min + 1)) + min;
for (var j = 1; j <= children; j++) {
if (available.length === 0) break;
var to = available.get(0);
available.delete(to);
linkArray.push({
from: next.key,
to: to.key
});
}
}
model.linkDataArray = linkArray;
}
// The following functions implement virtualization of the Diagram
// Assume data.bounds is a Rect of the area occupied by the Node in document coordinates.
// The normal mechanism for determining the size of the document depends on all of the
// Nodes and Links existing, so we need to use a function that depends only on the model data.
function computeDocumentBounds() {
var b = new go.Rect();
var ndata = myWholeModel.nodeDataArray;
for (var i = 0; i < ndata.length; i++) {
var d = ndata[i];
if (!d.bounds) continue;
if (i === 0) {
b.set(d.bounds);
} else {
b.unionRect(d.bounds);
}
}
return b;
}
// As the user scrolls or zooms, make sure the Parts (Nodes and Links) exist in the viewport.
function onViewportChanged(e) {
var diagram = e.diagram;
// make sure there are Nodes for each node data that is in the viewport
// or that is connected to such a Node
var viewb = diagram.viewportBounds; // the new viewportBounds
var model = diagram.model;
var oldskips = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
var b = new go.Rect();
var ndata = myWholeModel.nodeDataArray;
for (var i = 0; i < ndata.length; i++) {
var n = ndata[i];
if (model.containsNodeData(n)) continue;
if (!n.bounds) continue;
if (n.bounds.intersectsRect(viewb)) {
addNode(diagram, n);
}
if (model instanceof go.TreeModel) {
// make sure links to all parent nodes appear
var parentkey = myWholeModel.getParentKeyForNodeData(n);
var parent = myWholeModel.findNodeDataForKey(parentkey);
if (parent !== null) {
if (n.bounds.intersectsRect(viewb)) { // N is inside viewport
addNode(diagram, parent); // so that link to parent appears
var node = diagram.findNodeForData(n);
if (node !== null) {
var link = node.findTreeParentLink();
if (link !== null) {
// do this now to avoid delayed routing outside of transaction
link.updateRoute();
}
}
} else { // N is outside of viewport
// see if there's a parent that is in the viewport,
// or if the link might cross over the viewport
b.set(n.bounds);
b.unionRect(parent.bounds);
if (b.intersectsRect(viewb)) {
addNode(diagram, n); // add N so that link to parent appears
var child = diagram.findNodeForData(n);
if (child !== null) {
var link = child.findTreeParentLink();
if (link !== null) {
// do this now to avoid delayed routing outside of transaction
link.updateRoute();
}
}
}
}
}
}
}
if (model instanceof go.GraphLinksModel) {
var ldata = myWholeModel.linkDataArray;
for (var i = 0; i < ldata.length; i++) {
var l = ldata[i];
if (model.containsLinkData(l)) continue;
var fromkey = myWholeModel.getFromKeyForLinkData(l);
if (fromkey === undefined) continue;
var from = myWholeModel.findNodeDataForKey(fromkey);
if (from === null || !from.bounds) continue;
var tokey = myWholeModel.getToKeyForLinkData(l);
if (tokey === undefined) continue;
var to = myWholeModel.findNodeDataForKey(tokey);
if (to === null || !to.bounds) continue;
b.set(from.bounds);
b.unionRect(to.bounds);
if (b.intersectsRect(viewb)) {
// also make sure both connected nodes are present,
// so that link routing is authentic
addNode(diagram, from);
addNode(diagram, to);
model.addLinkData(l);
var link = diagram.findLinkForData(l);
if (link !== null) {
// do this now to avoid delayed routing outside of transaction
link.updateRoute();
}
}
}
}
diagram.skipsUndoManager = oldskips;
if (myRemoveTimer === null) {
// only remove offscreen nodes after a delay
myRemoveTimer = setTimeout(() => removeOffscreen(diagram), 3000);
}
updateCounts(); // only for this sample
}
function addNode(diagram, data) {
const model = diagram.model;
if (model.containsNodeData(data)) return;
model.addNodeData(data);
const n = diagram.findNodeForData(data);
if (n !== null) n.ensureBounds();
}
// occasionally remove Parts that are offscreen from the Diagram
var myRemoveTimer = null;
function removeOffscreen(diagram) {
myRemoveTimer = null;
var viewb = diagram.viewportBounds;
var model = diagram.model;
var remove = []; // collect for later removal
var removeLinks = new go.Set(); // links connected to a node data to remove
var it = diagram.nodes;
while (it.next()) {
var n = it.value;
var d = n.data;
if (d === null) continue;
if (!n.actualBounds.intersectsRect(viewb) && !n.isSelected) {
// even if the node is out of the viewport, keep it if it is selected or
// if any link connecting with the node is still in the viewport
if (!n.linksConnected.any(l => l.actualBounds.intersectsRect(viewb))) {
remove.push(d);
if (model instanceof go.GraphLinksModel) {
removeLinks.addAll(n.linksConnected);
}
}
}
}
if (remove.length > 0) {
var oldskips = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
model.removeNodeDataCollection(remove);
if (model instanceof go.GraphLinksModel) {
removeLinks.each(l => {
if (!l.isSelected) model.removeLinkData(l.data);
});
}
diagram.skipsUndoManager = oldskips;
}
updateCounts(); // only for this sample
}
// end of virtualized Diagram
// start of VirtualizedForceDirected[Layout/Network] classes
// Here we try to replace the dependence of ForceDirectedLayout on Nodes
// with depending only on the data in the GraphLinksModel.
class VirtualizedForceDirectedLayout extends go.ForceDirectedLayout {
constructor() {
super();
this.isOngoing = false;
this.model = null; // add this property for holding the whole GraphLinksModel
}
createNetwork() {
return new VirtualizedForceDirectedNetwork(this); // defined below
}
// ignore the argument, an (implicit) collection of Parts
makeNetwork(coll) {
var net = this.createNetwork();
net.addData(this.model); // use the model data, not any actual Nodes and Links
return net;
}
commitLayout() {
super.commitLayout();
// can't depend on regular bounds computation that depends on all Nodes existing
this.diagram.fixedBounds = computeDocumentBounds();
// update the positions of any existing Nodes
this.diagram.nodes.each(node => node.updateTargetBindings());
}
}
// end VirtualizedForceDirectedLayout class
class VirtualizedForceDirectedNetwork extends go.ForceDirectedNetwork {
constructor(layout) {
super(layout);
}
addData(model) {
if (model instanceof go.GraphLinksModel) {
var dataVertexMap = new go.Map();
// create a vertex for each node data
var ndata = model.nodeDataArray;
for (var i = 0; i < ndata.length; i++) {
var d = ndata[i];
var v = this.createVertex();
v.data = d; // associate this Vertex with data, not a Node
dataVertexMap.set(model.getKeyForNodeData(d), v);
this.addVertex(v);
}
// create an edge for each link data
var ldata = model.linkDataArray;
for (var i = 0; i < ldata.length; i++) {
var d = ldata[i];
// now find corresponding vertexes
var from = dataVertexMap.get(model.getFromKeyForLinkData(d));
var to = dataVertexMap.get(model.getToKeyForLinkData(d));
if (from === null || to === null) continue; // skip
// create and add VirtualizedForceDirectedEdge
var e = this.createEdge();
e.data = d; // associate this Edge with data, not a Link
e.fromVertex = from;
e.toVertex = to;
this.addEdge(e);
}
} else {
throw new Error("can only handle GraphLinksModel data");
}
}
deleteArtificialVertexes() { }
}
// end VirtualizedForceDirectedNetwork class
// end of VirtualizedForceDirected[Layout/Network] classes
// This function is only used in this sample to demonstrate the effects of the virtualization.
// In a real application you would delete this function and all calls to it.
function updateCounts() {
document.getElementById("myMessage1").textContent = myWholeModel.nodeDataArray.length;
document.getElementById("myMessage2").textContent = myDiagram.nodes.count;
document.getElementById("myMessage3").textContent = myWholeModel.linkDataArray.length;
document.getElementById("myMessage4").textContent = myDiagram.links.count;
}
window.addEventListener('DOMContentLoaded', init);
</script>
<style>
#mySpinner { display: none; position: absolute; z-index: 100; animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
<div id="sample">
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 600px"></div>
<img id="mySpinner" src="images/spinner.png">
<div id="description">
<p>This uses a <a>GraphLinksModel</a> and a virtualized <a>ForceDirectedLayout</a>.</p>
Node data in Model: <span id="myMessage1"></span>.
Actual Nodes in Diagram: <span id="myMessage2"></span>.<br />
Link data in model: <span id="myMessage3"></span>.
Actual Links in Diagram: <span id="myMessage4"></span>.
</div>
</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>