gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
456 lines (408 loc) • 19.8 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="Quickly layout and show part of a large graph of nested groups."/>
<link rel="stylesheet" href="../assets/css/style.css"/>
<!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
<title>Virtualized Packed Groups Layout</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="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<div id="allSampleContent" class="p-4 w-full">
<script id="code">
function init() {
require(["../release/go.js", "./VirtualizedPackedLayout"], function(go, mod) {
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
const $ = go.GraphObject.make;
// This custom layout that applies to myWholeModel.
// It customizes the VirtualizedPackedLayout to account for Groups.
class VirtualizedPackedGroupsLayout extends mod.VirtualizedPackedLayout {
constructor() {
super();
this.isOngoing = false;
this.model = null; // must be set when initializing Diagram and myWholeModel
this.sortMode = mod.VirtualizedPackedLayout.Area;
this.hasCircularNodes = true;
this.topLevelNodes = [];
}
doLayout() { // ignore arg
if (!this.model) return;
const nodes = this.model.nodeDataArray;
const topGroups = this.model.topGroups;
let maxdiam = 0;
if (Array.isArray(topGroups)) {
for (let i = 0; i < topGroups.length; i++) {
const g = topGroups[i];
this.walkGroups(g);
maxdiam = Math.max(maxdiam, Math.max(g.bounds.width, g.bounds.height));
}
}
this.topLevelNodes.length = 0;
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
if (n.group === undefined) this.topLevelNodes.push(n);
maxdiam = Math.max(maxdiam, Math.max(n.bounds.width, n.bounds.height));
}
this.spacing = Math.max(50, maxdiam * 0.2);
this.performLayout(this.topLevelNodes); // only top-level nodes
this.diagram.fixedBounds = this.actualBounds;
}
// depth-first walk
walkGroups(g) {
if (!g || !g.isGroup || !g._members) throw new Error("not a group data: " + g);
const mems = g._members;
if (Array.isArray(mems) && mems.length > 0) {
let maxdiam = 0;
for (let i = 0; i < mems.length; i++) {
const n = mems[i];
if (n.isGroup) {
this.walkGroups(n);
}
maxdiam = Math.max(maxdiam, Math.max(n.bounds.width, n.bounds.height));
}
this.spacing = Math.max(50, maxdiam * 0.2);
this.performLayout(mems);
g.bounds = this.actualBounds.copy();
} else {
//!!!???@@@ this needs to be customized to account for your chosen Group template
g.bounds = new go.Rect(0, 0, 50, 50);
}
}
// override moveNode to handle groups
moveNode(node, nx, ny) {
const dx = nx - node.bounds.x;
const dy = ny - node.bounds.y;
this.shiftNode(node, dx, dy);
}
shiftNode(node, dx, dy) {
node.bounds.x += dx;
node.bounds.y += dy;
if (node.isGroup) {
const mems = node._members;
if (Array.isArray(mems)) {
for (let i = 0; i < mems.length; i++) {
const n = mems[i];
this.shiftNode(n, dx, dy);
}
}
}
}
}
// 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",
{
"animationManager.isEnabled": false, // don't have any unnecessary initial scrolling
initialScale: 0.25,
layout: new VirtualizedPackedGroupsLayout(),
"InitialLayoutCompleted": function(e) { // initial scroll so that we see some nodes
let first = null;
const arr = myWholeModel.nodeDataArray;
for (let i = 0; i < arr.length; i++) {
const d = arr[i];
if (!d.isGroup) { first = d; break; }
}
if (first) {
e.diagram.centerRect(first.bounds);
}
}
});
function fillBinding(depth) { if (depth >= myColors.length) depth = 0; return "rgba(" + myColors[depth] + ",0.1)"; }
function strokeBinding(depth) { if (depth >= myColors.length) depth = 0; return "rgb(" + myColors[depth] + ")"; }
const myColors = ["0,0,0", "0,255,0", "255,0,0", "0,0,255"];
const myLayoutFactors = [16, 8, 4, 2];
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{ isLayoutPositioned: false }, // optimization
new go.Binding("position", "bounds", function(b) { return b.position; }),
{ width: 50, height: 50 }, // in cooperation with the load function, below
$(go.Shape, "Circle",
{
spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight,
portId: "", fill: "white", stroke: "gray"
},
new go.Binding("fill", "depth", fillBinding),
new go.Binding("stroke", "depth", strokeBinding)),
$(go.TextBlock,
new go.Binding("text", "key"))
);
myDiagram.groupTemplate =
$(go.Group, "Auto",
{ isLayoutPositioned: false }, // optimization
// note no Placeholder and no .layout, since VirtualizedPackedGroupsLayout will compute everything
new go.Binding("position", "bounds", function(b) { return new go.Point(b.x - b.width * 0.05, b.y - b.height * 0.05); }),
new go.Binding("desiredSize", "bounds", function(b) { return new go.Size(b.size.width * 1.1, b.size.height * 1.1); }),
$(go.Shape, "Ellipse",
{
spot1: new go.Spot(0.05, 0.05), spot2: new go.Spot(0.95, 0.95),
portId: "", fill: "white", stroke: "gray"
},
new go.Binding("fill", "depth", fillBinding),
new go.Binding("stroke", "depth", strokeBinding)),
$(go.TextBlock,
new go.Binding("text", "key"))
);
// 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);
// This is a status message
myLoading =
$(go.Part, // this has to set the location or position explicitly
{ location: new go.Point(0, 0), scale: 4 },
$(go.TextBlock, "loading...",
{ stroke: "red", font: "20pt sans-serif" }));
// temporarily add the status indicator
myDiagram.add(myLoading);
// Allow the myLoading indicator to be shown now,
// but allow objects added in load to also be considered part of the initial Diagram.
// If you are not going to add temporary initial Parts, don't call delayInitialization.
myDiagram.delayInitialization(load);
// The following code creates a large randomized graph with nested groups in myWholeModel.
function load() {
// create a lot of data for the myWholeModel
addGraph(myWholeModel, 123456, 50, 4, 1.0);
// remove the status indicator
myDiagram.remove(myLoading);
}
function addGraph(model, totnodes, maxmembers, maxdepth, percentgroup) {
model.topGroups = []; // add this property to GraphLinksModel
addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, 0, null);
}
function addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, depth, groupdata) {
// groupdata may be null for top-level nodes
const nkey = model.nodeDataArray.length;
if (nkey >= totnodes) return;
let numnodes = Math.floor(Math.random() * (maxmembers-1)) + 2;
if (nkey + numnodes >= totnodes) numnodes = totnodes-nkey;
const nodes = [];
const links = [];
for (let i = 0; i < numnodes; i++) {
const data = { key: nkey + i, bounds: undefined, depth: depth };
if (groupdata) {
if (!groupdata.isGroup || !groupdata._members) {
throw new Error("not a group data: " + groupdata);
}
// initially no .bounds property for group data
data.group = groupdata.key;
groupdata._members.push(data);
}
if (depth < maxdepth && Math.random() < percentgroup) {
data.isGroup = true;
data._members = [];
if (!groupdata) model.topGroups.push(data); // only remember top-level groups
} else {
//!!!???@@@ this needs to be customized to account for your chosen Node template
data.bounds = new go.Rect(0, 0, 50, 50);
}
nodes.push(data);
if (i > 0) links.push({ from: nkey, to: nkey + i });
}
for (let i = 1; i <= numnodes/3; i++) {
// additional links between nodes other than the first one
const from = Math.floor(Math.random() * (numnodes-1)) + 1;
const to = Math.floor(Math.random() * (numnodes-1)) + 1;
links.push({ from: nodes[from].key, to: nodes[to].key });
}
model.addNodeDataCollection(nodes);
model.addLinkDataCollection(links);
for (let i = 0; i < numnodes; i++) {
const data = nodes[i];
if (data.isGroup) {
addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, depth+1, data);
}
}
}
// 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(model) {
const b = new go.Rect();
const ndata = model.nodeDataArray;
for (let i = 0; i < ndata.length; i++) {
const 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) {
const 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
const viewb = diagram.viewportBounds; // the new viewportBounds
const model = diagram.model; // assume a GraphLinksModel
const oldskips = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
const b = new go.Rect();
const ndata = myWholeModel.nodeDataArray;
for (let i = 0; i < ndata.length; i++) {
const n = ndata[i];
if (!n.bounds) continue;
if (n.bounds.intersectsRect(viewb)) {
model.addNodeData(n);
}
}
const ldata = myWholeModel.linkDataArray;
for (let i = 0; i < ldata.length; i++) {
const l = ldata[i];
const fromkey = myWholeModel.getFromKeyForLinkData(l);
if (fromkey === undefined) continue;
const from = myWholeModel.findNodeDataForKey(fromkey);
if (from === null || !from.bounds) continue;
const tokey = myWholeModel.getToKeyForLinkData(l);
if (tokey === undefined) continue;
const 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
model.addNodeData(from);
model.addNodeData(to);
model.addLinkData(l);
const link = diagram.findLinkForData(l);
if (link !== null) {
// do this now to avoid delayed routing outside of transaction
link.fromNode.ensureBounds();
link.toNode.ensureBounds();
link.updateRoute();
}
}
}
diagram.skipsUndoManager = oldskips;
if (myRemoveTimer === null) {
// only remove offscreen nodes after a delay
myRemoveTimer = setTimeout(function() { removeOffscreen(diagram); }, 3000);
}
updateCounts(); // only for this sample
}
// occasionally remove Parts that are offscreen from the Diagram
var myRemoveTimer = null;
function removeOffscreen(diagram) {
myRemoveTimer = null;
const viewb = diagram.viewportBounds;
const model = diagram.model;
const remove = []; // collect for later removal
const removeLinks = new go.Set(); // links connected to a node data to remove
const it = diagram.nodes;
while (it.next()) {
const n = it.value;
const 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(function(l) { return l.actualBounds.intersectsRect(viewb); })) {
remove.push(d);
if (model instanceof go.GraphLinksModel) {
removeLinks.addAll(n.linksConnected);
}
}
}
}
if (remove.length > 0) {
const oldskips = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
model.removeNodeDataCollection(remove);
if (model instanceof go.GraphLinksModel) {
removeLinks.each(function(l) { if (!l.isSelected) model.removeLinkData(l.data); });
}
diagram.skipsUndoManager = oldskips;
}
updateCounts(); // only for this sample
}
// end of virtualized Diagram
// 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>
<div id="sample">
<div id="myDiagramDiv" style="width:100%; height:800px; border: solid 1px black"></div>
<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>.
</p>
<p>
This uses the <a>VirtualizedPackedLayout</a> extension,
defined <a href="VirtualizedPackedLayout.ts" target="_blank">VirtualizedPackedLayout.ts</a>,
to quickly layout a large graph consisting of
nested groups.
</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>