UNPKG

gojs

Version:

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

416 lines (378 loc) 17.9 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Virtualized Sample with no Layout</title> <meta name="description" content="An example of virtualization where node bounds information is present in the node data, so no layout is required." /> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Copyright 1998-2020 by Northwoods Software Corporation. --> <script src="../release/go.js"></script> <script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework --> <script id="code"> function init() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this var $ = 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 = $(go.Diagram, "myDiagramDiv", { contentAlignment: go.Spot.Center, initialDocumentSpot: go.Spot.Center, initialViewportSpot: go.Spot.Center, // Assume there's no Layout -- all data.bounds are provided layout: $(go.Layout, { isInitial: false, isOngoing: false }), // never invalidates // Define the template for Nodes, used by virtualization. nodeTemplate: $(go.Node, "Auto", { isLayoutPositioned: false }, // optimization new go.Binding("position", "bounds", function(b) { return b.position; }) .makeTwoWay(function(p, d) { return 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", "", function(d) { return "key: " + d.key + "\nbounds: " + d.bounds.toString(); })) ) } ), groupTemplate: $(go.Group, "Vertical", { isLayoutPositioned: false }, // optimization { locationSpot: go.Spot.TopLeft, locationObjectName: "SHAPE" }, new go.Binding("location", "bounds", function(b) { return b.position; }) .makeTwoWay(function(p, d) { return new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height); }), new go.Binding("isSubGraphExpanded").makeTwoWay(), { subGraphExpandedChanged: function(g) { if (!g.isSubGraphExpanded) return; // only after expanding // invalidate all member and external link routes g.findSubGraphParts().each(function(n) { if (n instanceof go.Node) n.invalidateConnectedLinks(); }); } }, $(go.Panel, "Horizontal", $("SubGraphExpanderButton"), $(go.TextBlock, new go.Binding("text", "key")) ), $(go.Shape, { fill: "lightgray", name: "SHAPE" }, new go.Binding("fill", "color"), new go.Binding("desiredSize", "", function(d) { if (d.isSubGraphExpanded === false) return new go.Size(50, 50); else return d.bounds.size; }) ) ), // Define the template for Links linkTemplate: $(go.Link, { isLayoutPositioned: false }, // optimization $(go.Shape), $(go.Shape, { toArrow: "OpenTriangle" }) ), "animationManager.isEnabled": false }); // This model includes all of the data myWholeModel = $(go.GraphLinksModel); // must match the model used by the Diagram, below // 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 $(go.GraphLinksModel); // must match the model, above // for now, we have to implement virtualization ourselves myDiagram.isVirtualized = true; myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged); myDiagram.addModelChangedListener(onModelChanged); myDiagram.model.makeUniqueKeyFunction = virtualUniqueKey; // ensure uniqueness in myWholeModel myDiagram.commandHandler.selectAll = function() { }; // make Select All command a no-op myDiagram.delayInitialization(function() { spinDuring("mySpinner", load); }); } // implement a wait spinner in HTML with CSS animation function spinDuring(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(myDiagram.div.offsetWidth/2 - spinner.naturalWidth/2); var y = Math.floor(myDiagram.div.offsetHeight/2 - spinner.naturalHeight/2); spinner.style.left = x + "px"; spinner.style.top = y + "px"; spinner.style.display = "inline"; } setTimeout(function() { try { compute(); // do the computation } finally { if (spinner) spinner.style.display = "none"; } }, 20); } function load() { // create a lot of data for the myWholeModel var total = 123456; var sqrt = Math.sqrt(total); var data = []; var ldata = []; for (var i = 0; i < total; i++) { data.push({ key: i, // this node data's key color: go.Brush.randomColor(), // the node's color //!!!???@@@ this needs to be customized to account for your chosen Node template bounds: new go.Rect((i % sqrt) * 100, (i / sqrt) * 100, 70, 20) }); if (i > 0) { // link sequential nodes ldata.push({ from: i - 1, to: i }); } } // create some groups for (var i = 0; i < total - 10; i++) { if (Math.random() > 0.1) continue; // # groups ~10% of total var key = total + i; // key for new group var mems = Math.floor(Math.random() * 5) + 1; // number of member nodes in this group var sgb = new go.Rect(); // Placeholder-like bounds of the member nodes for (var j = 0; j < mems; j++) { var d = data[i + j]; d.group = key; // assign membership if (sgb.isEmpty()) sgb.set(d.bounds); else sgb.unionRect(d.bounds); } sgb.inflate(15, 15); // a bit of padding data.push({ key: key, isGroup: true, color: "rgba(180,180,180, 0.2)", bounds: sgb }); i += mems; // avoid overlapping groups, except for groups that wrap to next row } myWholeModel.nodeDataArray = data; myWholeModel.linkDataArray = ldata; // there is no virtualized layout to perform, but we still // can't depend on regular bounds computation that depends on all Nodes existing myDiagram.fixedBounds = computeDocumentBounds(myWholeModel); } // The following functions implement virtualization of the Diagram // Assume data.bounds is a Rect of the area occupied by the Node in document coordinates. // It's not good enough to ensure keys are unique in the limited model that is myDiagram.model -- // need to ensure uniqueness in myWholeModel. function virtualUniqueKey(model, data) { myWholeModel.makeNodeDataKeyUnique(data); return myWholeModel.getKeyForNodeData(data); } // 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 (b.isEmpty()) 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 (!n.bounds) continue; if (n.bounds.intersectsRect(viewb)) { addNodeAndGroups(diagram, myWholeModel, 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 // so that link to parent appears addNodeAndGroups(diagram, myWholeModel, parent); 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)) { // add N so that link to parent appears addNodeAndGroups(diagram, myWholeModel, n); 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]; 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 addNodeAndGroups(diagram, myWholeModel, from); addNodeAndGroups(diagram, myWholeModel, 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(function() { removeOffscreen(diagram); }, 3000); } updateCounts(); // only for this sample } function addNodeAndGroups(diagram, wholeModel, data) { var model = diagram.model; model.addNodeData(data); var n = diagram.findNodeForData(data); if (n !== null) n.ensureBounds(); var groupkey = wholeModel.getGroupKeyForNodeData(data); while (groupkey !== undefined) { var gd = wholeModel.findNodeDataForKey(groupkey); if (gd !== null) { // is there a containing group data? model.addNodeData(gd); // make sure it's added to the diagram var g = diagram.findNodeForData(gd); if (g !== null) g.ensureBounds(); } // walk up chain of containing group data groupkey = wholeModel.getGroupKeyForNodeData(gd); } } function isPartOrGroupSelected(part) { if (!part) return false; if (part.isSelected) return true; return isPartOrGroupSelected(part.containingGroup); } // 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) && !isPartOrGroupSelected(n)) { // 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(function(l) { return 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(function(l) { if (!isPartOrGroupSelected(l)) model.removeLinkData(l.data); }); } diagram.skipsUndoManager = oldskips; } updateCounts(); // only for this sample } function onModelChanged(e) { // handle insertions and removals if (e.model.skipsUndoManager) return; if (e.change === go.ChangedEvent.Insert) { if (e.propertyName === "nodeDataArray") { myWholeModel.addNodeData(e.newValue); } else if (e.propertyName === "linkDataArray") { myWholeModel.addLinkData(e.newValue); } } else if (e.change === go.ChangedEvent.Remove && e.propertyName === "nodeDataArray") { if (e.propertyName === "nodeDataArray") { myWholeModel.removeNodeData(e.oldValue); } else if (e.propertyName === "linkDataArray") { myWholeModel.removeLinkData(e.oldValue); } } } // 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; } </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> </head> <body onload="init()"> <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> but not any <a>Layout</a>. It demonstrates the virtualization of Links and Groups as well as simple Nodes. Note that the template for Groups does <em>not</em> use a <a>Placeholder</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> </div> </body> </html>