UNPKG

19.8 kBHTMLView Raw
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="utf-8"/>
5<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
6<meta name="description" content="Quickly layout and show part of a large graph of nested groups."/>
7<link rel="stylesheet" href="../assets/css/style.css"/>
8<!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
9<title>Virtualized Packed Groups Layout</title>
10</head>
11
12<body>
13 <!-- This top nav is not part of the sample code -->
14 <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary">
15 <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">
16 <div class="md:pl-4">
17 <a class="text-white hover:text-white no-underline hover:no-underline
18 font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../">
19 <h1 class="mb-0 p-1 ">GoJS</h1>
20 </a>
21 </div>
22 <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation">
23 <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
24 <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>
25 <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>
26 </svg>
27 </button>
28 <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20">
29 <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0">
30 <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li>
31 <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li>
32 <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li>
33 <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li>
34 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li>
35 <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li>
36 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li>
37 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html"
38 target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li>
39 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html"
40 target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li>
41 </ul>
42 </div>
43 </div>
44 <hr class="border-b border-gray-600 opacity-50 my-0 py-0" />
45 </nav>
46 <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
47 <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div>
48 <!-- * * * * * * * * * * * * * -->
49 <!-- Start of GoJS sample code -->
50
51
52 <div class="p-4 w-full">
53 <div id="sample">
54 <div id="myDiagramDiv" style="width:100%; height:800px; border: solid 1px black"></div>
55 <p>
56 Node data in Model: <span id="myMessage1"></span>.
57 Actual Nodes in Diagram: <span id="myMessage2"></span>.<br />
58 Link data in model: <span id="myMessage3"></span>.
59 Actual Links in Diagram: <span id="myMessage4"></span>.
60 </p>
61 <p>
62 This uses the <a>VirtualizedPackedLayout</a> extension,
63 defined <a href="VirtualizedPackedLayout.ts">VirtualizedPackedLayout.ts</a>,
64 to quickly layout a large graph consisting of
65 nested groups.
66 </p>
67 </div>
68
69 <script type="module" id="code">
70 import * as go from "../release/go-module.js";
71 import { VirtualizedPackedLayout } from "./VirtualizedPackedLayout.js";
72
73 if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
74 const $ = go.GraphObject.make;
75
76 // This custom layout that applies to myWholeModel.
77 // It customizes the VirtualizedPackedLayout to account for Groups.
78 class VirtualizedPackedGroupsLayout extends VirtualizedPackedLayout {
79 constructor() {
80 super();
81 this.isOngoing = false;
82 this.model = null; // must be set when initializing Diagram and myWholeModel
83 this.sortMode = VirtualizedPackedLayout.Area;
84 this.hasCircularNodes = true;
85 this.topLevelNodes = [];
86 }
87
88 doLayout() { // ignore arg
89 if (!this.model) return;
90 var nodes = this.model.nodeDataArray;
91 var topGroups = this.model.topGroups;
92 var maxdiam = 0;
93 if (Array.isArray(topGroups)) {
94 for (var i = 0; i < topGroups.length; i++) {
95 var g = topGroups[i];
96 this.walkGroups(g);
97 maxdiam = Math.max(maxdiam, Math.max(g.bounds.width, g.bounds.height));
98 }
99 }
100 this.topLevelNodes.length = 0;
101 for (var i = 0; i < nodes.length; i++) {
102 var n = nodes[i];
103 if (n.group === undefined) this.topLevelNodes.push(n);
104 maxdiam = Math.max(maxdiam, Math.max(n.bounds.width, n.bounds.height));
105 }
106 this.spacing = Math.max(50, maxdiam * 0.2);
107 this.performLayout(this.topLevelNodes); // only top-level nodes
108 this.diagram.fixedBounds = this.actualBounds;
109 }
110
111 // depth-first walk
112 walkGroups(g) {
113 if (!g || !g.isGroup || !g._members) throw new Error("not a group data: " + g);
114 var mems = g._members;
115 if (Array.isArray(mems) && mems.length > 0) {
116 var maxdiam = 0;
117 for (var i = 0; i < mems.length; i++) {
118 var n = mems[i];
119 if (n.isGroup) {
120 this.walkGroups(n);
121 }
122 maxdiam = Math.max(maxdiam, Math.max(n.bounds.width, n.bounds.height));
123 }
124 this.spacing = Math.max(50, maxdiam * 0.2);
125 this.performLayout(mems);
126 g.bounds = this.actualBounds.copy();
127 } else {
128 //!!!???@@@ this needs to be customized to account for your chosen Group template
129 g.bounds = new go.Rect(0, 0, 50, 50);
130 }
131 }
132
133 // override moveNode to handle groups
134 moveNode(node, nx, ny) {
135 const dx = nx - node.bounds.x;
136 const dy = ny - node.bounds.y;
137 this.shiftNode(node, dx, dy);
138 }
139
140 shiftNode(node, dx, dy) {
141 node.bounds.x += dx;
142 node.bounds.y += dy;
143 if (node.isGroup) {
144 var mems = node._members;
145 if (Array.isArray(mems)) {
146 for (var i = 0; i < mems.length; i++) {
147 var n = mems[i];
148 this.shiftNode(n, dx, dy);
149 }
150 }
151 }
152 }
153 } // end VirtualizedPackedGroupsLayout
154
155 // The Diagram just shows what should be visible in the viewport.
156 // Its model does NOT include node data for the whole graph, but only that
157 // which might be visible in the viewport.
158 const myDiagram =
159 $(go.Diagram, "myDiagramDiv",
160 {
161 "animationManager.isEnabled": false, // don't have any unnecessary initial scrolling
162 initialScale: 0.25,
163 layout: new VirtualizedPackedGroupsLayout(),
164 "InitialLayoutCompleted": function(e) { // initial scroll so that we see some nodes
165 var first = null;
166 var arr = myWholeModel.nodeDataArray;
167 for (var i = 0; i < arr.length; i++) {
168 var d = arr[i];
169 if (!d.isGroup) { first = d; break; }
170 }
171 if (first) {
172 e.diagram.centerRect(first.bounds);
173 }
174 }
175 });
176
177 function fillBinding(depth) { if (depth >= myColors.length) depth = 0; return "rgba(" + myColors[depth] + ",0.1)"; }
178 function strokeBinding(depth) { if (depth >= myColors.length) depth = 0; return "rgb(" + myColors[depth] + ")"; }
179
180 var myColors = ["0,0,0", "0,255,0", "255,0,0", "0,0,255"];
181 var myLayoutFactors = [16, 8, 4, 2];
182
183 myDiagram.nodeTemplate =
184 $(go.Node, "Auto",
185 { isLayoutPositioned: false }, // optimization
186 new go.Binding("position", "bounds", function(b) { return b.position; }),
187 { width: 50, height: 50 }, // in cooperation with the load function, below
188 $(go.Shape, "Circle",
189 {
190 spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight,
191 portId: "", fill: "white", stroke: "gray"
192 },
193 new go.Binding("fill", "depth", fillBinding),
194 new go.Binding("stroke", "depth", strokeBinding)),
195 $(go.TextBlock,
196 new go.Binding("text", "key"))
197 );
198
199 myDiagram.groupTemplate =
200 $(go.Group, "Auto",
201 { isLayoutPositioned: false }, // optimization
202 // note no Placeholder and no .layout, since VirtualizedPackedGroupsLayout will compute everything
203 new go.Binding("position", "bounds", function(b) { return new go.Point(b.x - b.width * 0.05, b.y - b.height * 0.05); }),
204 new go.Binding("desiredSize", "bounds", function(b) { return new go.Size(b.size.width * 1.1, b.size.height * 1.1); }),
205 $(go.Shape, "Ellipse",
206 {
207 spot1: new go.Spot(0.05, 0.05), spot2: new go.Spot(0.95, 0.95),
208 portId: "", fill: "white", stroke: "gray"
209 },
210 new go.Binding("fill", "depth", fillBinding),
211 new go.Binding("stroke", "depth", strokeBinding)),
212 $(go.TextBlock,
213 new go.Binding("text", "key"))
214 );
215
216 // This model includes all of the data
217 const myWholeModel =
218 $(go.GraphLinksModel); // must match the model used by the Diagram, below
219
220 // The virtualized layout works on the full model, not on the Diagram Nodes and Links
221 myDiagram.layout.model = myWholeModel;
222
223 // Do not set myDiagram.model = myWholeModel -- that would create a zillion Nodes and Links!
224 // In the future Diagram may have built-in support for virtualization.
225 // For now, we have to implement virtualization ourselves by having the Diagram's model
226 // be different than the "real" model.
227 myDiagram.model = // this only holds nodes that should be in the viewport
228 $(go.GraphLinksModel); // must match the model, above
229
230 // for now, we have to implement virtualization ourselves
231 myDiagram.isVirtualized = true;
232 myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);
233
234 // This is a status message
235 const myLoading =
236 $(go.Part, // this has to set the location or position explicitly
237 { location: new go.Point(0, 0), scale: 4 },
238 $(go.TextBlock, "loading...",
239 { stroke: "red", font: "20pt sans-serif" }));
240
241 // temporarily add the status indicator
242 myDiagram.add(myLoading);
243
244 // Allow the myLoading indicator to be shown now,
245 // but allow objects added in load to also be considered part of the initial Diagram.
246 // If you are not going to add temporary initial Parts, don't call delayInitialization.
247 myDiagram.delayInitialization(load);
248
249
250 // The following code creates a large randomized graph with nested groups in myWholeModel.
251
252 function load() {
253 // create a lot of data for the myWholeModel
254 addGraph(myWholeModel, 123456, 50, 4, 1.0);
255
256 // remove the status indicator
257 myDiagram.remove(myLoading);
258 }
259
260 function addGraph(model, totnodes, maxmembers, maxdepth, percentgroup) {
261 model.topGroups = []; // add this property to GraphLinksModel
262 addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, 0, null);
263 }
264 function addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, depth, groupdata) {
265 // groupdata may be null for top-level nodes
266 var nkey = model.nodeDataArray.length;
267 if (nkey >= totnodes) return;
268 var numnodes = Math.floor(Math.random() * (maxmembers - 1)) + 2;
269 if (nkey + numnodes >= totnodes) numnodes = totnodes - nkey;
270 var nodes = [];
271 var links = [];
272 for (var i = 0; i < numnodes; i++) {
273 var data = { key: nkey + i, bounds: undefined, depth: depth };
274 if (groupdata) {
275 if (!groupdata.isGroup || !groupdata._members) {
276 throw new Error("not a group data: " + groupdata);
277 }
278 // initially no .bounds property for group data
279 data.group = groupdata.key;
280 groupdata._members.push(data);
281 }
282 if (depth < maxdepth && Math.random() < percentgroup) {
283 data.isGroup = true;
284 data._members = [];
285 if (!groupdata) model.topGroups.push(data); // only remember top-level groups
286 } else {
287 //!!!???@@@ this needs to be customized to account for your chosen Node template
288 data.bounds = new go.Rect(0, 0, 50, 50);
289 }
290 nodes.push(data);
291 if (i > 0) links.push({ from: nkey, to: nkey + i });
292 }
293 for (var i = 1; i <= numnodes / 3; i++) {
294 // additional links between nodes other than the first one
295 var from = Math.floor(Math.random() * (numnodes - 1)) + 1;
296 var to = Math.floor(Math.random() * (numnodes - 1)) + 1;
297 links.push({ from: nodes[from].key, to: nodes[to].key });
298 }
299 model.addNodeDataCollection(nodes);
300 model.addLinkDataCollection(links);
301 for (var i = 0; i < numnodes; i++) {
302 var data = nodes[i];
303 if (data.isGroup) {
304 addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, depth + 1, data);
305 }
306 }
307 }
308
309
310 // The following functions implement virtualization of the Diagram
311 // Assume data.bounds is a Rect of the area occupied by the Node in document coordinates.
312
313 // The normal mechanism for determining the size of the document depends on all of the
314 // Nodes and Links existing, so we need to use a function that depends only on the model data.
315 function computeDocumentBounds(model) {
316 var b = new go.Rect();
317 var ndata = model.nodeDataArray;
318 for (var i = 0; i < ndata.length; i++) {
319 var d = ndata[i];
320 if (!d.bounds) continue;
321 if (i === 0) {
322 b.set(d.bounds);
323 } else {
324 b.unionRect(d.bounds);
325 }
326 }
327 return b;
328 }
329
330 // As the user scrolls or zooms, make sure the Parts (Nodes and Links) exist in the viewport.
331 function onViewportChanged(e) {
332 var diagram = e.diagram;
333 // make sure there are Nodes for each node data that is in the viewport
334 // or that is connected to such a Node
335 var viewb = diagram.viewportBounds; // the new viewportBounds
336 var model = diagram.model; // assume a GraphLinksModel
337
338 var oldskips = diagram.skipsUndoManager;
339 diagram.skipsUndoManager = true;
340
341 var b = new go.Rect();
342 var ndata = myWholeModel.nodeDataArray;
343 for (var i = 0; i < ndata.length; i++) {
344 var n = ndata[i];
345 if (!n.bounds) continue;
346 if (n.bounds.intersectsRect(viewb)) {
347 model.addNodeData(n);
348 }
349 }
350
351 var ldata = myWholeModel.linkDataArray;
352 for (var i = 0; i < ldata.length; i++) {
353 var l = ldata[i];
354 var fromkey = myWholeModel.getFromKeyForLinkData(l);
355 if (fromkey === undefined) continue;
356 var from = myWholeModel.findNodeDataForKey(fromkey);
357 if (from === null || !from.bounds) continue;
358
359 var tokey = myWholeModel.getToKeyForLinkData(l);
360 if (tokey === undefined) continue;
361 var to = myWholeModel.findNodeDataForKey(tokey);
362 if (to === null || !to.bounds) continue;
363
364 b.set(from.bounds);
365 b.unionRect(to.bounds);
366 if (b.intersectsRect(viewb)) {
367 // also make sure both connected nodes are present,
368 // so that link routing is authentic
369 model.addNodeData(from);
370 model.addNodeData(to);
371 model.addLinkData(l);
372 var link = diagram.findLinkForData(l);
373 if (link !== null) {
374 // do this now to avoid delayed routing outside of transaction
375 link.fromNode.ensureBounds();
376 link.toNode.ensureBounds();
377 link.updateRoute();
378 }
379 }
380 }
381
382 diagram.skipsUndoManager = oldskips;
383
384 if (myRemoveTimer === null) {
385 // only remove offscreen nodes after a delay
386 myRemoveTimer = setTimeout(function() { removeOffscreen(diagram); }, 3000);
387 }
388
389 updateCounts(); // only for this sample
390 }
391
392 // occasionally remove Parts that are offscreen from the Diagram
393 var myRemoveTimer = null;
394
395 function removeOffscreen(diagram) {
396 myRemoveTimer = null;
397
398 var viewb = diagram.viewportBounds;
399 var model = diagram.model;
400 var remove = []; // collect for later removal
401 var removeLinks = new go.Set(); // links connected to a node data to remove
402 var it = diagram.nodes;
403 while (it.next()) {
404 var n = it.value;
405 var d = n.data;
406 if (d === null) continue;
407 if (!n.actualBounds.intersectsRect(viewb) && !n.isSelected) {
408 // even if the node is out of the viewport, keep it if it is selected or
409 // if any link connecting with the node is still in the viewport
410 if (!n.linksConnected.any(function(l) { return l.actualBounds.intersectsRect(viewb); })) {
411 remove.push(d);
412 if (model instanceof go.GraphLinksModel) {
413 removeLinks.addAll(n.linksConnected);
414 }
415 }
416 }
417 }
418
419 if (remove.length > 0) {
420 var oldskips = diagram.skipsUndoManager;
421 diagram.skipsUndoManager = true;
422 model.removeNodeDataCollection(remove);
423 if (model instanceof go.GraphLinksModel) {
424 removeLinks.each(function(l) { if (!l.isSelected) model.removeLinkData(l.data); });
425 }
426 diagram.skipsUndoManager = oldskips;
427 }
428
429 updateCounts(); // only for this sample
430 }
431 // end of virtualized Diagram
432
433 // This function is only used in this sample to demonstrate the effects of the virtualization.
434 // In a real application you would delete this function and all calls to it.
435 function updateCounts() {
436 document.getElementById("myMessage1").textContent = myWholeModel.nodeDataArray.length;
437 document.getElementById("myMessage2").textContent = myDiagram.nodes.count;
438 document.getElementById("myMessage3").textContent = myWholeModel.linkDataArray.length;
439 document.getElementById("myMessage4").textContent = myDiagram.links.count;
440 }
441
442 window.myDiagram = myDiagram; // Attach to the window for console debugging
443 </script>
444 </div>
445 <!-- * * * * * * * * * * * * * -->
446 <!-- End of GoJS sample code -->
447 </div>
448</body>
449<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
450<script src="../assets/js/goSamples.js"></script>
451</html>