UNPKG

18.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="TypeScript: Arrange nodes into rectangular or elliptical areas, ignoring any links."/>
7<link rel="stylesheet" href="../assets/css/style.css"/>
8<!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
9<title>Packed 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 style="margin-bottom: 5px; padding: 5px; background-color: aliceblue">
55 <span style="display: inline-block; vertical-align: top; padding: 5px">
56 <b>General Properties</b><br />
57 PackShape:<br /> <input type="radio" name="packShape" onclick="layout()" value="Elliptical" checked> Elliptical<br />
58 <input type="radio" name="packShape" onclick="layout()" value="Rectangular"> Rectangular<br />
59 <input type="radio" name="packShape" onclick="layout()" value="Spiral"> Spiral<br />
60 PackMode:<br /> <input type="radio" name="packMode" onclick="layout()" value="AspectOnly" checked> Aspect Ratio<br />
61 <input type="radio" name="packMode" onclick="layout()" value="ExpandToFit"> Expand to Fit<br />
62 <input type="radio" name="packMode" onclick="layout()" value="Fit"> Fit<br />
63
64 <table>
65 <tr>
66 <td>Aspect ratio: </td>
67 <td><input type="number" size="2" id="aspectRatio" value="1" onchange="layout()"></td>
68 </tr>
69 <tr>
70 <td>Layout width: </td>
71 <td><input type="number" size="2" id="width" value="600" onchange="layout()"></td>
72 </tr>
73 <tr>
74 <td>Layout height: </td>
75 <td><input type="number" size="2" id="height" value="600" onchange="layout()"></td>
76 </table>
77 </span>
78 <span style="display: inline-block; vertical-align: top; padding: 5px">
79 <b>Node Sorting Properties</b><br />
80 SortOrder:<br /> <input type="radio" name="sortOrder" onclick="layout()" value="Descending" checked> Descending<br />
81 <input type="radio" name="sortOrder" onclick="layout()" value="Ascending"> Ascending<br />
82 SortMode: <br />
83 <input type="radio" name="sortMode" onclick="layout()" id="modeNone" value="None"> None (do not sort nodes)<br />
84 <input type="radio" name="sortMode" onclick="layout()" id="modeMaxSide" value="MaxSide"> Max Side Length<br />
85 <input type="radio" name="sortMode" onclick="layout()" id="modeArea" value="Area" checked> Area<br />
86
87 <b>Padding between nodes</b><br />
88 Spacing: <input type="number" id="nodeSpacing" value="0" onchange="layout()"><br />
89 <b>Circle Packing</b><br />
90 hasCircularNodes <input type="checkbox" id="hasCircularNodes" onclick="layout()">
91 </span>
92
93 <span style="display: inline-block; vertical-align: top; padding: 5px">
94 <b>Node Generation</b><br />
95 <table>
96 <tr>
97 <td>Number of nodes: </td>
98 <td><input type="number" id="numNodes" value="100" onchange="layout()"><br /></td>
99 </tr>
100 <tr>
101 <td>Node shape:<br /><input type="radio" name="shapeToPack" onclick="layout()" value="Rectangle" checked> Rectangles<br />
102 <input type="radio" name="shapeToPack" onclick="layout()" value="Ellipse"> Ellipses<br />
103 </tr>
104 <tr>
105 <td>Minimum side length: </td>
106 <td><input type="number" id="nodeMinSide" value="30" onchange="layout()"><br /></td>
107 </tr>
108 <tr>
109 <td>Maximum side length: </td>
110 <td><input type="number" id="nodeMaxSide" value="50" onchange="layout()"><br /></td>
111 </tr>
112 </table>
113 Same width/height <input type="checkbox" id="sameSides" onclick="layout()"><br />
114 <button type="button" id="randomizeGraph" style="margin-top: 5px;">Randomize Graph</button>
115 </span>
116 </div>
117 <div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 500px"></div>
118 <p>
119 This sample demonstrates a custom Layout, PackedLayout, which attempts to pack nodes as close together as possible without overlap.
120 Each node is assumed to be either rectangular or circular (dictated by the 'nodesAreCircles' property). This layout supports packing
121 nodes into either a rectangle or an ellipse, with the shape determined by the PackShape and the aspect ratio determined by either the
122 aspectRatio property, or the specified width and height (depending on the PackMode).
123 </p>
124 <p>
125 The layout is defined in its own file, as <a href="PackedLayout.ts">PackedLayout.ts</a>, with an additional dependency on <a href="Quadtree.ts">Quadtree.ts</a>.
126 </p>
127 </div>
128
129 <script type="module" id="code">
130 import * as go from "../release/go-module.js";
131 import { PackedLayout } from './PackedLayout.js';
132
133 if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
134 const $ = go.GraphObject.make; // for conciseness in defining templates
135
136 // nodes need to be randomized again if any of these change
137 let minSidePrevious = 0.0;
138 let maxSidePrevious = 0.0;
139 let sameSidesPrevious = false;
140
141 const myDiagram =
142 $(go.Diagram, 'myDiagramDiv', // must be the ID or reference to div
143 {
144 'animationManager.isEnabled': true,
145 layout: $(PackedLayout),
146 scale: 0.75, isReadOnly: true
147 });
148 // Nodes have a template with bindings for size, shape, and fill color
149 myDiagram.nodeTemplate =
150 $(go.Node, 'Auto', new go.Binding('visible', 'visible'), $(go.Shape, { strokeWidth: 0 }, new go.Binding('figure', 'figure'), new go.Binding('width', 'width'), new go.Binding('height', 'height'), new go.Binding('fill', 'fill')));
151 myDiagram.model = new go.GraphLinksModel([]);
152 // find the elements in the DOM which control configuration
153 const aspectRatio = document.getElementById('aspectRatio');
154 aspectRatio.onkeydown = aspectRatioHandler;
155 const layoutWidth = document.getElementById('width');
156 const layoutHeight = document.getElementById('height');
157 const nodeSpacing = document.getElementById('nodeSpacing');
158 const hasCircularNodes = document.getElementById('hasCircularNodes');
159 const numNodes = document.getElementById('numNodes');
160 const nodeMinSide = document.getElementById('nodeMinSide');
161 const nodeMaxSide = document.getElementById('nodeMaxSide');
162 const sameSides = document.getElementById('sameSides');
163 // create a layout with the default values
164 rebuildGraph();
165
166 // when arrow keys are pressed and the aspect ratio is below 1, increment using the harmonic sequence
167 // this makes the aspect ratio change as follows: 3:1, 2:1, 1:1, 1:2, 1:3, etc.
168 function aspectRatioHandler(e) {
169 if (e.key === 'ArrowUp' && parseFloat(aspectRatio.value) < 1) {
170 e.preventDefault();
171 const denom = Math.round(1 / parseFloat(aspectRatio.value));
172 aspectRatio.value = (+(1 / (denom - 1)).toFixed(2)) + '';
173 rebuildGraph();
174 }
175 else if (e.key === 'ArrowDown' && parseFloat(aspectRatio.value) <= 1) {
176 e.preventDefault();
177 const denom = Math.round(1 / parseFloat(aspectRatio.value));
178 if (denom < 10) {
179 aspectRatio.value = (+(1 / (denom + 1)).toFixed(2)) + '';
180 rebuildGraph();
181 }
182 }
183 }
184 function validateInput() {
185 if (!aspectRatio.value || parseFloat(aspectRatio.value) <= 0) {
186 aspectRatio.value = '0.1';
187 }
188 if (!layoutWidth.value || parseFloat(layoutWidth.value) <= 0) {
189 layoutWidth.value = '1';
190 }
191 if (!layoutHeight.value || parseFloat(layoutHeight.value) <= 0) {
192 layoutHeight.value = '1';
193 }
194 if (!nodeSpacing.value) {
195 nodeSpacing.value = '0';
196 }
197 if (!numNodes.value || parseInt(numNodes.value) < 1) {
198 numNodes.value = '1';
199 }
200 if (!nodeMinSide.value || parseFloat(nodeMinSide.value) < 1) {
201 nodeMinSide.value = '1';
202 }
203 if (!nodeMaxSide.value || parseFloat(nodeMaxSide.value) < 1) {
204 nodeMaxSide.value = '1';
205 }
206 }
207
208 function rebuildGraph() {
209 validateInput();
210 let packShape = PackedLayout.Elliptical;
211 switch (getRadioValue('packShape')) {
212 case 'Elliptical':
213 packShape = PackedLayout.Elliptical;
214 break;
215 case 'Rectangular':
216 packShape = PackedLayout.Rectangular;
217 break;
218 case 'Spiral':
219 packShape = PackedLayout.Spiral;
220 break;
221 }
222 let packMode = PackedLayout.AspectOnly;
223 switch (getRadioValue('packMode')) {
224 case 'AspectOnly':
225 packMode = PackedLayout.AspectOnly;
226 break;
227 case 'Fit':
228 packMode = PackedLayout.Fit;
229 break;
230 case 'ExpandToFit':
231 packMode = PackedLayout.ExpandToFit;
232 break;
233 }
234 let sortMode = PackedLayout.None;
235 switch (getRadioValue('sortMode')) {
236 case 'None':
237 sortMode = PackedLayout.None;
238 break;
239 case 'MaxSide':
240 sortMode = PackedLayout.MaxSide;
241 break;
242 case 'Area':
243 sortMode = PackedLayout.Area;
244 break;
245 }
246 let sortOrder = PackedLayout.Descending;
247 switch (getRadioValue('sortOrder')) {
248 case 'Descending':
249 sortOrder = PackedLayout.Descending;
250 break;
251 case 'Ascending':
252 sortOrder = PackedLayout.Ascending;
253 break;
254 }
255 const params = {
256 packMode: packMode,
257 packShape: packShape,
258 sortMode: sortMode,
259 sortOrder: sortOrder,
260 aspectRatio: parseFloat(aspectRatio.value),
261 size: new go.Size(parseFloat(layoutWidth.value), parseFloat(layoutHeight.value)),
262 spacing: parseFloat(nodeSpacing.value),
263 hasCircularNodes: hasCircularNodes.checked,
264 arrangesToOrigin: false
265 };
266 disableInputs(params);
267 if (sameSides.checked !== sameSidesPrevious
268 || parseFloat(nodeMinSide.value) !== minSidePrevious
269 || parseFloat(nodeMaxSide.value) !== maxSidePrevious) {
270 sameSidesPrevious = sameSides.checked;
271 minSidePrevious = parseFloat(nodeMinSide.value);
272 maxSidePrevious = parseFloat(nodeMaxSide.value);
273 randomize();
274 return;
275 }
276 myDiagram.startTransaction('packed layout');
277 generateNodeData();
278 myDiagram.layout = go.GraphObject.make(PackedLayout, params /* defined above */);
279 myDiagram.commitTransaction('packed layout');
280 }
281
282 function randomize() {
283 myDiagram.model = new go.GraphLinksModel([]);
284 rebuildGraph();
285 }
286 window.randomize = randomize;
287
288 function generateNodeData() {
289 const nodeDataArray = myDiagram.model.nodeDataArray;
290 const count = parseInt(numNodes.value);
291 const min = parseFloat(nodeMinSide.value);
292 const max = parseFloat(nodeMaxSide.value);
293 const shapeToPack = getRadioValue('shapeToPack');
294 if (count > nodeDataArray.length) {
295 const arr = new Array();
296 for (let i = nodeDataArray.length; i < count; i++) {
297 const width = Math.floor(Math.random() * (max - min + 1)) + min;
298 const height = sameSides.checked ? width : Math.floor(Math.random() * (max - min + 1)) + min;
299 const color = go.Brush.randomColor(128, 235);
300 arr.push({ width: width, height: height, fill: color, figure: shapeToPack });
301 }
302 myDiagram.model.addNodeDataCollection(arr);
303 }
304 else if (count < nodeDataArray.length) {
305 while (count < nodeDataArray.length) {
306 myDiagram.model.removeNodeData(nodeDataArray[nodeDataArray.length - 1]);
307 }
308 }
309 else {
310 for (const data of nodeDataArray) {
311 myDiagram.model.set(data, 'figure', shapeToPack);
312 }
313 }
314 sameSidesPrevious = sameSides.checked;
315 minSidePrevious = min;
316 maxSidePrevious = max;
317 }
318 var hasCircularNodesSavedState = null;
319 var sameSidesSavedState = null;
320 function disableInputs(params) {
321 setRadioButtonsDisabled('packMode', params.packShape === PackedLayout.Spiral);
322 aspectRatio.disabled = params.packMode !== PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral;
323 layoutWidth.disabled = params.packMode === PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral;
324 layoutHeight.disabled = params.packMode === PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral;
325 nodeSpacing.disabled = params.packMode === PackedLayout.ExpandToFit;
326 hasCircularNodes.disabled = params.packShape === PackedLayout.Spiral;
327 if (params.packShape === PackedLayout.Spiral) {
328 if (hasCircularNodesSavedState === null) {
329 hasCircularNodesSavedState = hasCircularNodes.checked;
330 }
331 hasCircularNodes.checked = true;
332 params.hasCircularNodes = true;
333 }
334 else if (hasCircularNodesSavedState !== null) {
335 hasCircularNodes.checked = hasCircularNodesSavedState;
336 params.hasCircularNodes = false;
337 hasCircularNodesSavedState = null;
338 }
339 sameSides.disabled = params.hasCircularNodes;
340 if (params.hasCircularNodes) {
341 if (sameSidesSavedState === null) {
342 sameSidesSavedState = sameSides.checked;
343 }
344 sameSides.checked = true;
345 }
346 else if (sameSidesSavedState !== null) {
347 sameSides.checked = sameSidesSavedState;
348 sameSidesSavedState = null;
349 }
350 }
351 function getRadioValue(name) {
352 const radio = document.getElementsByName(name);
353 for (let i = 0; i < radio.length; i++) {
354 if (radio[i].checked)
355 return radio[i].value;
356 }
357 }
358 function setRadioButtonsDisabled(name, disabled) {
359 const radio = document.getElementsByName(name);
360 for (let i = 0; i < radio.length; i++) {
361 radio[i].disabled = disabled;
362 }
363 }
364
365 document.getElementById("randomizeGraph").onclick = randomize;
366
367 function layout() {
368 rebuildGraph();
369 }
370 window.layout = layout; // make accessible to button onclick event handlers
371
372 window.myDiagram = myDiagram; // Attach to the window for console debugging
373 </script>
374 </div>
375 <!-- * * * * * * * * * * * * * -->
376 <!-- End of GoJS sample code -->
377 </div>
378</body>
379<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
380<script src="../assets/js/goSamples.js"></script>
381</html>