UNPKG

gojs

Version:

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

452 lines (405 loc) 19.7 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="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 --> <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="VirtualizedPackedLayout.js" target="_blank">VirtualizedPackedLayout.js</a>, to quickly layout a large graph consisting of nested groups. </p> </div> <script type="module" id="code"> import * as go from "../release/go-module.js"; import { VirtualizedPackedLayout } from "./VirtualizedPackedLayout.js"; 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 VirtualizedPackedLayout { constructor() { super(); this.isOngoing = false; this.model = null; // must be set when initializing Diagram and myWholeModel this.sortMode = VirtualizedPackedLayout.Area; this.hasCircularNodes = true; this.topLevelNodes = []; } 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 = $(go.Node, "Auto", { isLayoutPositioned: false }, // optimization new go.Binding("position", "bounds", b => 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", b => new go.Point(b.x - b.width * 0.05, b.y - b.height * 0.05)), new go.Binding("desiredSize", "bounds", b => 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 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 = $(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 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 console debugging </script> </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>