UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

538 lines (480 loc) 22 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 TreeLayout sets the bounds for each node data."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>Virtualized Tree with TreeLayout</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"> // this controls whether the tree grows deeper towards the right or downwards: const HORIZONTAL = true; function init() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this // 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", { 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: $(VirtualizedTreeLayout, { angle: (HORIZONTAL ? 0 : 90), nodeSpacing: 10 }), // 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, { fromSpot: (HORIZONTAL ? go.Spot.Right : go.Spot.Bottom), toSpot: (HORIZONTAL ? go.Spot.Left : go.Spot.Top) }, $(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() { super(); this.isOngoing = false; this.model = null; // add this property for holding the whole TreeModel } 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.TreeLayout.AlignmentStart || parentv.alignment === go.TreeLayout.AlignmentEnd); 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>.</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> </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>