create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
689 lines (605 loc) • 27.5 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 TreeLayout sets the bounds for each node data." />
<meta itemprop="description" content="An example of virtualization where a virtualized TreeLayout sets the bounds for each node data." />
<meta property="og:description" content="An example of virtualization where a virtualized TreeLayout sets the bounds for each node data." />
<meta name="twitter:description" content="An example of virtualization where a virtualized TreeLayout sets the bounds for each node data." />
<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 Tree with TreeLayout" />
<meta property="og:title" content="Virtualized Tree with TreeLayout" />
<meta name="twitter:title" content="Virtualized Tree with TreeLayout" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedtree.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedtree.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedtree.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/virtualizedTreeLayout.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/virtualizedTreeLayout.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Virtualized Tree with TreeLayout | 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 -->
<script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>
<div id="allSampleContent" class="p-4 w-full">
<script id="code">
// this controls whether the tree grows deeper towards the right or downwards:
const HORIZONTAL = true;
function init() {
// 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', {
initialDocumentSpot: go.Spot.Center,
initialViewportSpot: go.Spot.Center,
// Use a virtualized TreeLayout which does not require
// that the Nodes and Links exist first for an accurate layout
layout: new VirtualizedTreeLayout({ angle: HORIZONTAL ? 0 : 90, nodeSpacing: 10 }),
// Define the template for Nodes, used by virtualization.
nodeTemplate:
new go.Node('Auto', {
isLayoutPositioned: false,
width: 70, height: 20, // in cooperation with the load function, below
toolTip:
go.GraphObject.build('ToolTip')
.add(
new go.TextBlock({ margin: 3 })
.bind('text', '', d => 'key: ' + d.key + '\nbounds: ' + d.bounds.toString())
)
}) // optimization
.bindTwoWay('position', 'bounds', b => b.position, (p, d) => new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height))
.add(
new go.Shape('Rectangle')
.bind('fill', 'color'),
new go.TextBlock({ margin: 2 })
.bind('text', 'color')
),
// Define the template for Links
linkTemplate:
new go.Link({
fromSpot: HORIZONTAL ? go.Spot.Right : go.Spot.Bottom,
toSpot: HORIZONTAL ? go.Spot.Left : go.Spot.Top
})
.add(
new go.Shape()
),
SelectionMoved: e => {
e.subject.each(n => {
if (n instanceof go.Node) n.data.points = undefined;
});
},
'animationManager.isEnabled': false
});
// This model includes all of the data
myWholeModel = new go.TreeModel(); // 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.TreeModel(); // must match the model, above
// for now, we have to implement virtualization ourselves
myDiagram.isVirtualized = true;
myDiagram.addDiagramListener('ViewportBoundsChanged', onViewportChanged);
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
const x = Math.floor(diagram.div.offsetWidth / 2 - spinner.naturalWidth / 2);
const 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
const total = 123456;
const treedata = [];
for (let i = 0; i < total; i++) {
const d = {
key: i, // this node data's key
color: go.Brush.randomColor(), // the node's color
parent: i > 0 ? Math.floor((Math.random() * i) / 2) : undefined // the random parent's key
};
//!!!???@@@ this needs to be customized to account for your chosen Node template
d.bounds = new go.Rect(0, 0, 70, 20);
treedata.push(d);
}
myWholeModel.nodeDataArray = treedata;
myDiagram.layoutDiagram(true);
}
// 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() {
const b = new go.Rect();
const ndata = myWholeModel.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;
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 (model.containsNodeData(n)) continue;
if (!n.bounds) continue;
if (n.bounds.intersectsRect(viewb)) {
model.addNodeData(n);
}
if (model instanceof go.TreeModel) {
// make sure links to all parent nodes appear
const parentkey = myWholeModel.getParentKeyForNodeData(n);
const parent = myWholeModel.findNodeDataForKey(parentkey);
if (parent !== null) {
if (n.bounds.intersectsRect(viewb)) {
// N is inside viewport
model.addNodeData(parent); // so that link to parent appears
const child = diagram.findNodeForData(n);
if (child !== null) {
const link = child.findTreeParentLink();
if (link !== null) {
// do this now to avoid delayed routing outside of transaction
link.fromNode.ensureBounds();
link.toNode.ensureBounds();
if (child.data.fromSpot) link.fromSpot = child.data.fromSpot;
if (child.data.toSpot) link.toSpot = child.data.toSpot;
if (child.data.points) {
link.points = child.data.points;
} else {
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)) {
model.addNodeData(n); // add N so that link to parent appears
const child = diagram.findNodeForData(n);
if (child !== null) {
const link = child.findTreeParentLink();
if (link !== null) {
// do this now to avoid delayed routing outside of transaction
link.fromNode.ensureBounds();
link.toNode.ensureBounds();
if (child.data.fromSpot) link.fromSpot = child.data.fromSpot;
if (child.data.toSpot) link.toSpot = child.data.toSpot;
if (child.data.points) {
link.points = child.data.points;
} else {
link.updateRoute();
}
}
}
}
}
}
}
}
if (model instanceof go.GraphLinksModel) {
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();
if (link.data.fromSpot) link.fromSpot = link.data.fromSpot;
if (link.data.toSpot) link.toSpot = link.data.toSpot;
//if (link.data.points) {
// link.points = link.data.points;
//} else {
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;
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(l => 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(l => {
if (!l.isSelected) model.removeLinkData(l.data);
});
}
diagram.skipsUndoManager = oldskips;
}
updateCounts(); // only for this sample
}
// end of virtualized Diagram
// start of VirtualizedTree[Layout/Network] classes
// Here we try to replace the dependence of TreeLayout on Nodes
// with depending only on the data in the TreeModel.
class VirtualizedTreeLayout extends go.TreeLayout {
constructor(init) {
super();
this.isOngoing = false;
this.model = null; // add this property for holding the whole TreeModel
if (init) Object.assign(this, init);
}
static _cachedLinks = [];
createNetwork() {
return new VirtualizedTreeNetwork(this); // defined below
}
// ignore the argument, an (implicit) collection of Parts
makeNetwork(coll) {
const net = this.createNetwork();
net.addData(this.model); // use the model data, not any actual Nodes and Links
return net;
}
commitLayout() {
VirtualizedTreeEdge._dummyLink = this.diagram.linkTemplate.copy();
VirtualizedTreeEdge._dummyFromNode = this.diagram.nodeTemplate.copy();
VirtualizedTreeEdge._dummyToNode = this.diagram.nodeTemplate.copy();
VirtualizedTreeEdge._dummyLink.fromNode = VirtualizedTreeEdge._dummyFromNode;
VirtualizedTreeEdge._dummyLink.toNode = VirtualizedTreeEdge._dummyToNode;
this.diagram.add(VirtualizedTreeEdge._dummyFromNode);
this.diagram.add(VirtualizedTreeEdge._dummyToNode);
this.diagram.add(VirtualizedTreeEdge._dummyLink);
super.commitLayout();
// can't depend on regular bounds computation that depends on all Nodes existing
this.diagram.fixedBounds = computeDocumentBounds();
this.diagram.remove(VirtualizedTreeEdge._dummyFromNode);
this.diagram.remove(VirtualizedTreeEdge._dummyToNode);
this.diagram.remove(VirtualizedTreeEdge._dummyLink);
// update the positions of any existing Nodes
this.diagram.nodes.each(node => node.updateTargetBindings());
}
setPortSpots(v) {
v.destinationEdges.each(e => {
e.link = VirtualizedTreeLayout._cachedLinks.pop() || new go.Link();
});
super.setPortSpots(v);
v.destinationEdges.each(e => {
if (e.data) {
e.data.fromSpot = e.link.fromSpot.copy();
e.data.toSpot = e.link.toSpot.copy();
}
VirtualizedTreeLayout._cachedLinks.push(e.link);
e.link = null;
});
}
}
// end VirtualizedTreeLayout class
class VirtualizedTreeNetwork extends go.TreeNetwork {
constructor(layout) {
super(layout);
}
createEdge() {
return new VirtualizedTreeEdge(this);
}
addData(model) {
if (model instanceof go.TreeModel) {
const dataVertexMap = new go.Map();
const ndata = model.nodeDataArray;
for (let i = 0; i < ndata.length; i++) {
const d = ndata[i];
const v = this.createVertex();
v.data = d; // associate this Vertex with data, not a Node
dataVertexMap.set(d, v);
this.addVertex(v);
}
for (let i = 0; i < ndata.length; i++) {
const child = ndata[i];
const parentkey = model.getParentKeyForNodeData(child);
const parent = model.findNodeDataForKey(parentkey);
if (parent !== null) {
// if there is a parent, there should be an edge
// now find corresponding vertexes
const f = dataVertexMap.get(parent);
const t = dataVertexMap.get(child);
if (f === null || t === null) continue; // skip
// create and add VirtualizedTreeEdge
const e = this.createEdge();
e.data = child; // associate this Edge with data, not a Link
e.fromVertex = f;
e.toVertex = t;
this.addEdge(e);
}
}
} else {
throw new Error('can only handle TreeModel data');
}
}
}
// end VirtualizedTreeNetwork class
class VirtualizedTreeEdge extends go.TreeEdge {
constructor(network) {
super(network);
}
static _dummyLink = null;
static _dummyFromNode = null;
static _dummyToNode = null;
commit() {
const parentv = this.fromVertex;
if (!parentv) return;
const routed = parentv.alignment === go.TreeAlignment.Start || parentv.alignment === go.TreeAlignment.End;
if (this.data && routed) {
this.link = VirtualizedTreeEdge._dummyLink;
this.link.fromNode.moveTo(this.fromVertex.x, this.fromVertex.y);
this.link.toNode.moveTo(this.toVertex.x, this.toVertex.y);
this.link.fromNode.ensureBounds();
this.link.toNode.ensureBounds();
this.link.updateRoute();
}
super.commit();
if (this.data && routed) {
this.data.points = this.link.points.copy();
this.link = null;
}
}
}
// end of VirtualizedTree[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('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>TreeModel</a> and a virtualized <a>TreeLayout</a>. The virtualized layout lets the <a>Diagram</a>
draw only necessary <a>Node</a>s and <a>Link</a>s, improving performance.
</p>
Node data in Model: <span id="myMessage1"></span>. Actual Nodes in Diagram: <span id="myMessage2"></span>. Actual Links in Diagram:
<span id="myMessage4"></span>.
</div>
</div>
</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>Collections</h4>
<p>
<b>GoJS</b> provides its own collection classes: <a href="../api/symbols/List.html" target="api">List</a>, <a href="../api/symbols/Set.html" target="api">Set</a>, and <a href="../api/symbols/Map.html" target="api">Map</a>.
You can iterate over a collection by using an <a href="../api/symbols/Iterator.html" target="api">Iterator</a>.
More information can be found in the <a href="../intro/collections.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#collections">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Tree Layout</h4>
<p>
This predefined layout is used for placing Nodes of a tree-structured graph in layers (rows or columns).
For discussion and examples of the most commonly used properties of the <a href="../api/symbols/TreeLayout.html">TreeLayout</a>,
see the <a href="../intro/trees.html">Trees</a> page in the Introduction.
More information can be found in the <a href="../intro/layouts.html#TreeLayout">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#treelayout">Related samples</a>
</p>
<hr>
<!-- 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>ToolTips</h4>
<p>
A tooltip is an <a href="../api/symbols/Adornment.html" target="api">Adornment</a> that is shown when the mouse hovers over an object that has its <a href="../api/symbols/GraphObject.html#toolTip" target="api">GraphObject.toolTip</a> set.
The tooltip part is bound to the same data as the part itself.
</p>
<p>
It is typical to implement a tooltip as a "ToolTip" Panel holding a <a href="../api/symbols/TextBlock.html" target="api">TextBlock</a> or a Panel of TextBlocks and other objects.
Each "ToolTip" is just an "Auto" Panel <a href="../api/symbols/Adornment.html" target="api">Adornment</a> that is shadowed, and where the border is a rectangular <a href="../api/symbols/Shape.html" target="api">Shape</a> with a light gray fill.
However you can implement the tooltip as any arbitrarily complicated Adornment.
</p>
<p>
More information can be found in the <a href="../intro/tooltips.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#tooltips">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>