UNPKG

10.4 kBJavaScriptView Raw
1"use strict";
2/*
3* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
4*/
5
6/*
7* This is an extension and not part of the main GoJS library.
8* Note that the API for this class may change with any version, even point releases.
9* If you intend to use an extension in production, you should copy the code to your own source directory.
10* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
11* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
12*/
13
14/**
15* @constructor
16* @extends Layout
17* @class
18* Given a root Node this arranges connected nodes in concentric rings,
19* layered by the minimum link distance from the root.
20*/
21function RadialLayout() {
22 go.Layout.call(this);
23 this._root = null;
24 this._layerThickness = 100; // how thick each ring should be
25 this._maxLayers = Infinity;
26}
27go.Diagram.inherit(RadialLayout, go.Layout);
28
29/**
30* @ignore
31* Copies properties to a cloned Layout.
32* @this {RadialLayout}
33* @param {Layout} copy
34*/
35RadialLayout.prototype.cloneProtected = function(copy) {
36 go.Layout.prototype.cloneProtected.call(this, copy);
37 // don't copy .root
38 copy._layerThickness = this._layerThickness;
39 copy._maxLayers = this._maxLayers;
40};
41
42/*
43* The Node to act as the root or central node of the radial layout.
44* @name RadialLayout#root
45
46* @return {Node}
47*/
48Object.defineProperty(RadialLayout.prototype, "root", {
49 get: function() { return this._root; },
50 set: function(value) {
51 if (this._root !== value) {
52 this._root = value;
53 this.invalidateLayout();
54 }
55 }
56});
57
58/*
59* The thickness of each ring representing a layer.
60* @name RadialLayout#layerThickness
61
62* @return {number}
63*/
64Object.defineProperty(RadialLayout.prototype, "layerThickness", {
65 get: function() { return this._layerThickness; },
66 set: function(value) {
67 if (this._layerThickness !== value) {
68 this._layerThickness = value;
69 this.invalidateLayout();
70 }
71 }
72});
73
74/*
75* The maximum number of layers to be shown, in addition to the root node at layer zero.
76* The default value is Infinity.
77* @name RadialLayout#maxLayers
78
79* @return {number}
80*/
81Object.defineProperty(RadialLayout.prototype, "maxLayers", {
82 get: function() { return this._maxLayers; },
83 set: function(value) {
84 if (this._maxLayers !== value) {
85 this._maxLayers = value;
86 this.invalidateLayout();
87 }
88 }
89});
90
91/**
92* Use a LayoutNetwork that always creates RadialVertexes.
93* @this {RadialLayout}
94* @return {LayoutNetwork}
95*/
96RadialLayout.prototype.createNetwork = function() {
97 var net = new go.LayoutNetwork(this);
98 net.createVertex = function() { return new RadialVertex(net); };
99 return net;
100}
101
102/**
103* @this {RadialLayout}
104* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
105*/
106RadialLayout.prototype.doLayout = function(coll) {
107 if (this.network === null) {
108 this.network = this.makeNetwork(coll);
109 }
110 if (this.network.vertexes.count === 0) {
111 this.network = null;
112 return;
113 }
114
115 if (this.root === null) {
116 // If no root supplied, choose one without any incoming edges
117 var it = this.network.vertexes.iterator;
118 while (it.next()) {
119 var v = it.value;
120 if (v.node !== null && v.sourceEdges.count === 0) {
121 this.root = v.node;
122 break;
123 }
124 }
125 }
126 if (this.root === null) {
127 // If could not find any default root, choose a random one
128 this.root = this.network.vertexes.first().node;
129 }
130 if (this.root === null) { // nothing to do
131 this.network = null;
132 return;
133 }
134
135 var rootvert = this.network.findVertex(this.root);
136 if (rootvert === null) throw new Error("RadialLayout.root must be a Node in the LayoutNetwork that the RadialLayout is operating on")
137
138 this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
139 this.findDistances(rootvert);
140
141 // sort all results into Arrays of RadialVertexes with the same distance
142 var verts = [];
143 var maxlayer = 0;
144 var it = this.network.vertexes.iterator;
145 while (it.next()) {
146 var v = it.value;
147 v.laid = false;
148 var layer = v.distance;
149 if (layer === Infinity) continue; // Infinity used as init value (set in findDistances())
150 if (layer > maxlayer) maxlayer = layer;
151 var layerverts = verts[layer];
152 if (layerverts === undefined) {
153 layerverts = [];
154 verts[layer] = layerverts;
155 }
156 layerverts.push(v);
157 }
158
159 // now recursively position nodes (using radlay1()), starting with the root
160 rootvert.centerX = this.arrangementOrigin.x;
161 rootvert.centerY = this.arrangementOrigin.y;
162 this.radlay1(rootvert, 1, 0, 360);
163
164 // Update the "physical" positions of the nodes and links.
165 this.updateParts();
166 this.network = null;
167}
168
169/**
170* @ignore
171* recursively position vertexes in a radial layout
172* @this {RadialLayout}
173* @param {RadialVertex} vert
174* @param {number} layer
175* @param {number} angle
176* @param {number} sweep
177*/
178RadialLayout.prototype.radlay1 = function(vert, layer, angle, sweep) {
179 if (layer > this.maxLayers) return; // no need to position nodes outside of maxLayers
180 var verts = []; // array of all RadialVertexes connected to 'vert' in layer 'layer'
181 vert.vertexes.each(function(v) {
182 if (v.laid) return;
183 if (v.distance === layer) verts.push(v);
184 });
185 var found = verts.length;
186 if (found === 0) return;
187
188 var radius = layer * this.layerThickness;
189 var separator = sweep / found; // distance between nodes in their sweep portion
190 var start = angle - sweep / 2 + separator / 2;
191 // for each vertex in this layer, place it in its correct layer and position
192 for (var i = 0; i < found; i++) {
193 var v = verts[i];
194 var a = start + i * separator; // the angle to rotate the node to
195 if (a < 0) a += 360; else if (a > 360) a -= 360;
196 // the point to place the node at -- this corresponds with the layer the node is in
197 // all nodes in the same layer are placed at a constant point, then rotated accordingly
198 var p = new go.Point(radius, 0);
199 p.rotate(a);
200 v.centerX = p.x + this.arrangementOrigin.x;
201 v.centerY = p.y + this.arrangementOrigin.y;
202 v.laid = true;
203 v.angle = a;
204 v.sweep = separator;
205 v.radius = radius;
206 // keep going for all layers
207 this.radlay1(v, layer + 1, a, sweep / found);
208 }
209};
210
211/**
212* @ignore
213* Update RadialVertex.distance for every vertex.
214* @this {RadialLayout}
215* @param {RadialVertex} source
216*/
217RadialLayout.prototype.findDistances = function(source) {
218 var diagram = this.diagram;
219 // keep track of distances from the source node
220 this.network.vertexes.each(function(v) { v.distance = Infinity; });
221 // the source node starts with distance 0
222 source.distance = 0;
223 // keep track of nodes for we have set a non-Infinity distance,
224 // but which we have not yet finished examining
225 var seen = new go.Set(/*go.RadialVertex*/);
226 seen.add(source);
227
228 // local function for finding a vertex with the smallest distance in a given collection
229 function leastVertex(coll) {
230 var bestdist = Infinity;
231 var bestvert = null;
232 var it = coll.iterator;
233 while (it.next()) {
234 var v = it.value;
235 var dist = v.distance;
236 if (dist < bestdist) {
237 bestdist = dist;
238 bestvert = v;
239 }
240 }
241 return bestvert;
242 }
243
244 // keep track of vertexes we have finished examining;
245 // this avoids unnecessary traversals and helps keep the SEEN collection small
246 var finished = new go.Set(/*go.RadialVertex*/);
247 while (seen.count > 0) {
248 // look at the unfinished vertex with the shortest distance so far
249 var least = leastVertex(seen);
250 var leastdist = least.distance;
251 // by the end of this loop we will have finished examining this LEAST vertex
252 seen.remove(least);
253 finished.add(least);
254 // look at all edges connected with this vertex
255 least.edges.each(function(e) {
256 var neighbor = e.getOtherVertex(least);
257 // skip vertexes that we have finished
258 if (finished.contains(neighbor)) return;
259 var neighbordist = neighbor.distance;
260 // assume "distance" along a link is unitary, but could be any non-negative number.
261 var dist = leastdist + 1;
262 if (dist < neighbordist) {
263 // if haven't seen that vertex before, add it to the SEEN collection
264 if (neighbordist == Infinity) {
265 seen.add(neighbor);
266 }
267 // record the new best distance so far to that node
268 neighbor.distance = dist;
269 }
270 });
271 }
272}
273
274/**
275* This override positions each Node and also calls {@link #rotateNode}.
276* @this {RadialLayout}
277*/
278RadialLayout.prototype.commitLayout = function() {
279 go.Layout.prototype.commitLayout.call(this);
280
281 var it = this.network.vertexes.iterator;
282 while (it.next()) {
283 var v = it.value;
284 var n = v.node;
285 if (n !== null) {
286 n.visible = (v.distance <= this.maxLayers);
287 this.rotateNode(n, v.angle, v.sweep, v.radius);
288 }
289 }
290
291 this.commitLayers();
292};
293
294/**
295* Override this method in order to modify each node as it is laid out.
296* By default this method does nothing.
297* @this {RadialLayout}
298* @param {Node} node
299* @param {number} angle in degrees relative to the center point
300* @param {number} sweep in degrees
301* @param {number} radius the inner radius for this node's layer
302*/
303RadialLayout.prototype.rotateNode = function(node, angle, sweep, radius) {
304};
305
306/**
307* Override this method in order to create background circles indicating the layers of the radial layout.
308* By default this method does nothing.
309* @this {RadialLayout}
310*/
311RadialLayout.prototype.commitLayers = function() {
312};
313// end RadialLayout
314
315
316/**
317* @ignore
318* @constructor
319* @extends LayoutVertex
320* @class
321*/
322function RadialVertex(network) {
323 go.LayoutVertex.call(this, network);
324 this.distance = Infinity; // number of layers from the root, non-negative integers
325 this.laid = false; // used internally to keep track
326 this.angle = 0; // the direction at which the node is placed relative to the root node
327 this.sweep = 0; // the angle subtended by the vertex
328 this.radius = 0; // the inner radius of the layer containing this vertex
329}
330go.Diagram.inherit(RadialVertex, go.LayoutVertex);
331