UNPKG

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