UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

641 lines (564 loc) 25.8 kB
<!DOCTYPE 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." /> <meta itemprop="description" content="An example of virtualization where a virtualized ForceDirectedLayout sets the bounds for each node data." /> <meta property="og:description" content="An example of virtualization where a virtualized ForceDirectedLayout sets the bounds for each node data." /> <meta name="twitter:description" content="An example of virtualization where a virtualized ForceDirectedLayout 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 Diagram with Force Directed Layout" /> <meta property="og:title" content="Virtualized Diagram with Force Directed Layout" /> <meta name="twitter:title" content="Virtualized Diagram with Force Directed Layout" /> <meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedforcelayout.png" /> <meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedforcelayout.png" /> <meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/virtualizedforcelayout.png" /> <meta property="og:url" content="https://gojs.net/latest/samples/virtualizedForceLayout.html" /> <meta property="twitter:url" content="https://gojs.net/latest/samples/virtualizedForceLayout.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 Force Directed 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 --> <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script> <div id="allSampleContent" class="p-4 w-full"> <script id="code"> 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', { // use a virtualized ForceDirectedLayout which does not require // that the Nodes and Links exist first for an accurate layout layout: new VirtualizedForceDirectedLayout({ maxIterations: 200 }), // Define the template for Nodes, used by virtualization. nodeTemplate: new go.Node('Auto', { isLayoutPositioned: false, // optimization width: 70, height: 20, toolTip: go.GraphObject.build("ToolTip") .add( new go.TextBlock({ margin: 3 }) .bind('text', '', d => 'key: ' + d.key + '\nbounds: ' + d.bounds.toString()) ) }) .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({ isLayoutPositioned: false }) // optimization .add(new 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(init) { super(); this.isOngoing = false; this.model = null; // add this property for holding the whole GraphLinksModel if (init) Object.assign(this, init); } 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> <br> <p> See also <a href="./virtualized.html">Virtualized</a>. </p> </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>Force Directed Layout</h4> <p> This predefined layout treats the graph as if it were a system of physical bodies with forces acting on and between them. The layout iteratively moves nodes and links to minimize the total sum of forces on each node. The resulting layout will normally not contain overlapping Nodes, excluding cases where the graph is densely interconnected. More information can be found in the <a href="../intro/layouts.html#ForceDirectedLayout">GoJS Intro</a>. </p> <p> <a href="../samples/index.html#force-directed">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>