create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
605 lines (533 loc) • 24.3 kB
HTML
<html lang="en">
<head>
<!-- You do not need this import map in your own code if you are installing gojs via a package manager -->
<script type="importmap">
{
"imports": {
"gojs": "https://cdn.jsdelivr.net/npm/gojs@3.1.0/release/go-module.js"
}
}
</script>
<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." />
<meta itemprop="description" content="Quickly layout and show part of a large graph of nested groups." />
<meta property="og:description" content="Quickly layout and show part of a large graph of nested groups." />
<meta name="twitter:description" content="Quickly layout and show part of a large graph of nested groups." />
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta itemprop="name" content="Virtualized Diagram with Packed Layout" />
<meta property="og:title" content="Virtualized Diagram with Packed Layout" />
<meta name="twitter:title" content="Virtualized Diagram with Packed Layout" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedpacked.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedpacked.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedpacked.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/VirtualizedPacked.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/VirtualizedPacked.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Virtualized Diagram with Packed Layout | GoJS Diagramming Library
</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
<div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
<a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
href="../">
GoJS
</a>
<div class="relative">
<button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
<svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="topnavList" class="hidden md:block">
<div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
<a href="../learn/">Learn</a>
<a href="../samples/">Samples</a>
<a href="../intro/">Intro</a>
<a href="../api/">API</a>
<a href="../download.html">Download</a>
<a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
<a id="tc" href="https://nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
<a id="tb" href="https://nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
</div>
</div>
</div>
</div>
</nav>
<script>
window.addEventListener("DOMContentLoaded", function () {
// topnav
var topButton = document.getElementById("topnavButton");
var topnavList = document.getElementById("topnavList");
if (topButton && topnavList) {
topButton.addEventListener("click", function (e) {
topnavList
.classList
.toggle("hidden");
e.stopPropagation();
});
document.addEventListener("click", function (e) {
// if the clicked element isn't the list, close the list
if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
topButton.click();
}
});
// set active <a> element
var url = window
.location
.href
.toLowerCase();
var aTags = topnavList.getElementsByTagName('a');
for (var i = 0; i < aTags.length; i++) {
var lowerhref = aTags[i]
.href
.toLowerCase();
if (lowerhref.endsWith('.html'))
lowerhref = lowerhref.slice(0, -5);
if (url.startsWith(lowerhref)) {
aTags[i]
.classList
.add('active');
break;
}
}
}
});
</script>
<div class="flex flex-col prose">
<div class="w-full max-w-screen-xl mx-auto">
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<div id="allSampleContent" class="p-4 w-full">
<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="../extensionsJSM/VirtualizedPackedLayout.js" target="_blank">VirtualizedPackedLayout.js</a>, to
quickly layout a large graph consisting of nested groups.
</p>
<p>
See also <a href="./virtualized.html">Virtualized</a>.
</p>
</div>
<script type="module" id="code">
import * as go from 'gojs';
import { VirtualizedPackedLayout, SortMode } from '../extensionsJSM/VirtualizedPackedLayout.js';
// This custom layout that applies to myWholeModel.
// It customizes the VirtualizedPackedLayout to account for Groups.
class VirtualizedPackedGroupsLayout extends VirtualizedPackedLayout {
constructor(init /*?: Partial<VirtualizedPackedGroupsLayout*/) {
super();
this.isOngoing = false;
this.model = null; // must be set when initializing Diagram and myWholeModel
this.sortMode = SortMode.Area;
this.hasCircularNodes = true;
this.topLevelNodes = [];
if (init) Object.assign(this, init);
}
doLayout() {
// ignore arg
if (!this.model) return;
var nodes = this.model.nodeDataArray;
var topGroups = this.model.topGroups;
var maxdiam = 0;
if (Array.isArray(topGroups)) {
for (var i = 0; i < topGroups.length; i++) {
var g = topGroups[i];
this.walkGroups(g);
maxdiam = Math.max(maxdiam, Math.max(g.bounds.width, g.bounds.height));
}
}
this.topLevelNodes.length = 0;
for (var i = 0; i < nodes.length; i++) {
var 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);
var mems = g._members;
if (Array.isArray(mems) && mems.length > 0) {
var maxdiam = 0;
for (var i = 0; i < mems.length; i++) {
var 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) {
var mems = node._members;
if (Array.isArray(mems)) {
for (var i = 0; i < mems.length; i++) {
var n = mems[i];
this.shiftNode(n, dx, dy);
}
}
}
}
} // end VirtualizedPackedGroupsLayout
// 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.
const myDiagram = new go.Diagram('myDiagramDiv', {
'animationManager.isEnabled': false, // don't have any unnecessary initial scrolling
initialScale: 0.25,
layout: new VirtualizedPackedGroupsLayout(),
InitialLayoutCompleted: e => {
// initial scroll so that we see some nodes
var first = null;
var arr = myWholeModel.nodeDataArray;
for (var i = 0; i < arr.length; i++) {
var 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] + ')';
}
var myColors = ['0,0,0', '0,255,0', '255,0,0', '0,0,255'];
var myLayoutFactors = [16, 8, 4, 2];
myDiagram.nodeTemplate =
new go.Node('Auto', {
isLayoutPositioned: false,
width: 50, height: 50
})
.bind('position', 'bounds', b => b.position)
.add(
new go.Shape('Circle', {
spot1: go.Spot.TopLeft,
spot2: go.Spot.BottomRight,
portId: '',
fill: 'white',
stroke: 'gray'
})
.bind('fill', 'depth', fillBinding)
.bind('stroke', 'depth', strokeBinding),
new go.TextBlock()
.bind('text', 'key')
);
myDiagram.groupTemplate =
new go.Group('Auto', { isLayoutPositioned: false }) // optimization
// note no Placeholder and no .layout, since VirtualizedPackedGroupsLayout will compute everything
.bind('position', 'bounds', b => new go.Point(b.x - b.width * 0.05, b.y - b.height * 0.05))
.bind('desiredSize', 'bounds', b => new go.Size(b.size.width * 1.1, b.size.height * 1.1))
.add(
new go.Shape('Ellipse', {
spot1: new go.Spot(0.05, 0.05),
spot2: new go.Spot(0.95, 0.95),
portId: '',
fill: 'white',
stroke: 'gray'
})
.bind('fill', 'depth', fillBinding)
.bind('stroke', 'depth', strokeBinding),
new go.TextBlock()
.bind('text', 'key')
);
// This model includes all of the data
const 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
const myLoading =
new go.Part({
location: new go.Point(0, 0), // this has to set the location or position explicitly
scale: 4
})
.add(
new 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
var nkey = model.nodeDataArray.length;
if (nkey >= totnodes) return;
var numnodes = Math.floor(Math.random() * (maxmembers - 1)) + 2;
if (nkey + numnodes >= totnodes) numnodes = totnodes - nkey;
var nodes = [];
var links = [];
for (var i = 0; i < numnodes; i++) {
var 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 (var i = 1; i <= numnodes / 3; i++) {
// additional links between nodes other than the first one
var from = Math.floor(Math.random() * (numnodes - 1)) + 1;
var 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 (var i = 0; i < numnodes; i++) {
var 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) {
var b = new go.Rect();
var ndata = model.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; // assume a GraphLinksModel
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 (!n.bounds) continue;
if (n.bounds.intersectsRect(viewb)) {
model.addNodeData(n);
}
}
var ldata = myWholeModel.linkDataArray;
for (var i = 0; i < ldata.length; i++) {
var l = ldata[i];
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
model.addNodeData(from);
model.addNodeData(to);
model.addLinkData(l);
var 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(() => 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;
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
// 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.myDiagram = myDiagram; // Attach to the window for debugging
</script>
<script>
if (window.location.protocol === 'file:') alert('For security reasons this page must be served from a web server.');
</script>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
<div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
<hr/>
<h3 class="text-xl">GoJS Features in this sample</h3>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Custom Layouts</h4>
<p>
GoJS allows for the creation of custom layouts to meet specific needs.
</p>
<p>
There are also many layouts that are extensions -- not predefined in the <code>go.js</code> or <code>go-debug.js</code> library,
but available as source code in one of the three extension directories, with some documentation and corresponding samples.
More information can be found in the <a href="../intro/layouts.html#CustomLayouts">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#customlayout">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>GoJS Extensions</h4>
<p>
<b>GoJS</b> can be extended in a variety of ways.
The most common way to change the standard behavior is to set properties on the <a href="../api/symbols/GraphObject.html" target="api">GraphObject</a>, <a href="../api/symbols/Diagram.html" target="api">Diagram</a>, <a href="../api/symbols/CommandHandler.html" target="api">CommandHandler</a>, <a href="../api/symbols/Tool.html" target="api">Tool</a>, or <a href="../api/symbols/Layout.html" target="api">Layout</a>.
But when the desired property does not exist, you might need to override methods of CommandHandler, Tool, Layout, Link, or Node.
Methods that you can override are documented in the API reference.
Various features of GoJS can be overriden, either by replacing a method on an instance (a feature of JavaScript) or by defining a subclass.
You should not modify the prototypes of any of the <b>GoJS</b> classes.
</p>
<p>
In addition to our samples, <b>GoJS</b> provides an <strong><a href="../samples/#extensions">extensions gallery</a></strong>,
showcasing the creation of custom tools and layouts.
Those classes and samples are written in TypeScript, available at <code>../extensionsJSM/</code>,
as ECMAScript/JavaScript modules -- these use the <code>../release/go-module.js</code> library.
We recommend that you copy the files that you need into your project, so that you can adjust how they refer to the GoJS library
that you choose and so that you can include them into your own building and packaging procedures.
</p>
<p>
More information can be found in the <a href="../intro/extensions.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#extensions">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
</div>
</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>