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 |
|
9 | <title>Virtualized Packed Groups Layout</title>
|
10 | </head>
|
11 |
|
12 | <body>
|
13 |
|
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 |
|
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();
|
74 | const $ = go.GraphObject.make;
|
75 |
|
76 |
|
77 |
|
78 | class VirtualizedPackedGroupsLayout extends VirtualizedPackedLayout {
|
79 | constructor() {
|
80 | super();
|
81 | this.isOngoing = false;
|
82 | this.model = null;
|
83 | this.sortMode = VirtualizedPackedLayout.Area;
|
84 | this.hasCircularNodes = true;
|
85 | this.topLevelNodes = [];
|
86 | }
|
87 |
|
88 | doLayout() {
|
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);
|
108 | this.diagram.fixedBounds = this.actualBounds;
|
109 | }
|
110 |
|
111 |
|
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 |
|
129 | g.bounds = new go.Rect(0, 0, 50, 50);
|
130 | }
|
131 | }
|
132 |
|
133 |
|
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 | }
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | const myDiagram =
|
159 | $(go.Diagram, "myDiagramDiv",
|
160 | {
|
161 | "animationManager.isEnabled": false,
|
162 | initialScale: 0.25,
|
163 | layout: new VirtualizedPackedGroupsLayout(),
|
164 | "InitialLayoutCompleted": function(e) {
|
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 },
|
186 | new go.Binding("position", "bounds", function(b) { return b.position; }),
|
187 | { width: 50, height: 50 },
|
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 },
|
202 |
|
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 |
|
217 | const myWholeModel =
|
218 | $(go.GraphLinksModel);
|
219 |
|
220 |
|
221 | myDiagram.layout.model = myWholeModel;
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | myDiagram.model =
|
228 | $(go.GraphLinksModel);
|
229 |
|
230 |
|
231 | myDiagram.isVirtualized = true;
|
232 | myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);
|
233 |
|
234 |
|
235 | const myLoading =
|
236 | $(go.Part,
|
237 | { location: new go.Point(0, 0), scale: 4 },
|
238 | $(go.TextBlock, "loading...",
|
239 | { stroke: "red", font: "20pt sans-serif" }));
|
240 |
|
241 |
|
242 | myDiagram.add(myLoading);
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | myDiagram.delayInitialization(load);
|
248 |
|
249 |
|
250 |
|
251 |
|
252 | function load() {
|
253 |
|
254 | addGraph(myWholeModel, 123456, 50, 4, 1.0);
|
255 |
|
256 |
|
257 | myDiagram.remove(myLoading);
|
258 | }
|
259 |
|
260 | function addGraph(model, totnodes, maxmembers, maxdepth, percentgroup) {
|
261 | model.topGroups = [];
|
262 | addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, 0, null);
|
263 | }
|
264 | function addGraphInternal(model, totnodes, maxmembers, maxdepth, percentgroup, depth, groupdata) {
|
265 |
|
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 |
|
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);
|
286 | } else {
|
287 |
|
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 |
|
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 |
|
311 |
|
312 |
|
313 |
|
314 |
|
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 |
|
331 | function onViewportChanged(e) {
|
332 | var diagram = e.diagram;
|
333 |
|
334 |
|
335 | var viewb = diagram.viewportBounds;
|
336 | var model = diagram.model;
|
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 |
|
368 |
|
369 | model.addNodeData(from);
|
370 | model.addNodeData(to);
|
371 | model.addLinkData(l);
|
372 | var link = diagram.findLinkForData(l);
|
373 | if (link !== null) {
|
374 |
|
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 |
|
386 | myRemoveTimer = setTimeout(function() { removeOffscreen(diagram); }, 3000);
|
387 | }
|
388 |
|
389 | updateCounts();
|
390 | }
|
391 |
|
392 |
|
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 = [];
|
401 | var removeLinks = new go.Set();
|
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 |
|
409 |
|
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();
|
430 | }
|
431 |
|
432 |
|
433 |
|
434 |
|
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;
|
443 | </script>
|
444 | </div>
|
445 |
|
446 |
|
447 | </div>
|
448 | </body>
|
449 |
|
450 | <script src="../assets/js/goSamples.js"></script>
|
451 | </html>
|