-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
Copy pathArrangingLayout.js
425 lines (424 loc) · 18.2 KB
/
ArrangingLayout.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/*
* Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* The source code for this is at extensionsJSM/ArrangingLayout.ts.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* A custom Layout that provides one way to have a layout of layouts.
* It partitions nodes and links into separate subnetworks, applies a primary
* layout to each subnetwork, and then arranges those results by an
* arranging layout. Any disconnected nodes are laid out later by a
* side layout, by default in a grid underneath the main body of subnetworks.
*
* This layout uses three separate Layouts.
*
* One is used for laying out nodes and links that are connected together: {@link primaryLayout}.
* This defaults to null and must be set to an instance of a {@link go.Layout},
* such as a {@link go.TreeLayout} or a {@link go.ForceDirectedLayout} or a custom Layout.
*
* One is used to arrange separate subnetworks of the main graph: {@link arrangingLayout}.
* This defaults to an instance of {@link go.GridLayout}.
*
* One is used for laying out the additional nodes along one of the sides of the main graph: {@link sideLayout}.
* This also defaults to an instance of {@link go.GridLayout}.
* A filter predicate, {@link filter}, splits up the collection of nodes and links into two subsets,
* one for the main layout and one for the side layout.
* By default, when there is no filter, it puts all nodes that have no link connections into the
* subset to be processed by the side layout.
*
* If all pairs of nodes in the main graph can be reached by some path of undirected links,
* there are no separate subnetworks, so the {@link arrangingLayout} need not be used and
* the {@link primaryLayout} would apply to all of those nodes and links.
*
* But if there are disconnected subnetworks, the {@link primaryLayout} is applied to each subnetwork,
* and then all of those results are arranged by the {@link arrangingLayout}.
* If you don't want to use an {@link arrangingLayout} and you want to force the {@link primaryLayout} to
* operate on all of the subnetworks, set {@link arrangingLayout} to null.
*
* In either case if there are any nodes in the side graph, those are arranged by the {@link sideLayout}
* to be on the side of the arrangement of the main graph of nodes and links.
* The {@link side} property controls which side they will be placed -- the default is BottomSide.
*
* Note: if you do not want to have singleton nodes be arranged by {@link sideLayout},
* set {@link filter} to <code>part => true</code>.
* That will cause all singleton nodes to be arranged by {@link arrangingLayout} as if they
* were each their own subnetwork.
*
* If you both don't want to use {@link sideLayout} and you don't want to use {@link arrangingLayout}
* to lay out connected subnetworks, don't use this ArrangingLayout at all --
* just use whatever Layout you would have assigned to {@link primaryLayout}.
*
* If you want to experiment with this extension, try the <a href="../../samples/Arranging.html">Arranging Layout</a> sample.
* @category Layout Extension
*/
class ArrangingLayout extends go.Layout {
constructor(init) {
super();
this._filter = null;
const play = new go.GridLayout();
play.cellSize = new go.Size(1, 1);
this._primaryLayout = play;
const alay = new go.GridLayout();
alay.cellSize = new go.Size(1, 1);
this._arrangingLayout = alay;
const slay = new go.GridLayout();
slay.cellSize = new go.Size(1, 1);
this._sideLayout = slay;
this._side = go.Spot.BottomSide;
this._spacing = new go.Size(20, 20);
if (init)
Object.assign(this, init);
}
/**
* @hidden @internal
* Copies properties to a cloned Layout.
*/
cloneProtected(copy) {
super.cloneProtected(copy);
copy._filter = this._filter;
if (this._primaryLayout !== null)
copy._primaryLayout = this._primaryLayout.copy();
if (this._arrangingLayout !== null)
copy._arrangingLayout = this._arrangingLayout.copy();
if (this._sideLayout !== null)
copy._sideLayout = this._sideLayout.copy();
copy._side = this._side.copy();
copy._spacing = this._spacing.copy();
}
/**
* @hidden @internal
* @param coll - the collection of Parts to layout.
*/
doLayout(coll) {
const coll2 = this.collectParts(coll);
if (coll2.count === 0)
return;
let diagram = this.diagram;
if (diagram === null)
diagram = coll2.first().diagram;
if (diagram === null)
return;
// implementations of doLayout that do not make use of a LayoutNetwork
// need to perform their own transactions
diagram.startTransaction('Arranging Layout');
const maincoll = new go.Set();
const sidecoll = new go.Set();
this.splitParts(coll2, maincoll, sidecoll);
let mainnet = null;
let subnets = null;
if (this.arrangingLayout !== null) {
this.arrangingLayout.diagram = diagram;
mainnet = this.makeNetwork(maincoll);
subnets = mainnet.splitIntoSubNetworks();
}
if (this.primaryLayout !== null)
this.primaryLayout.diagram = diagram;
if (this.sideLayout !== null)
this.sideLayout.diagram = diagram;
let bounds = null;
if (this.arrangingLayout !== null &&
mainnet !== null &&
subnets !== null &&
subnets.count > 1) {
const groups = new go.Map();
const it = subnets.iterator;
while (it.next()) {
const net = it.value;
const subcoll = net.findAllParts();
this.preparePrimaryLayout(this.primaryLayout, subcoll);
this.primaryLayout.doLayout(subcoll);
this._addMainNode(groups, subcoll, diagram);
}
const mit = mainnet.vertexes.iterator;
while (mit.next()) {
const v = mit.value;
if (v.node) {
const subcoll = new go.Set();
subcoll.add(v.node);
this.preparePrimaryLayout(this.primaryLayout, subcoll);
this.primaryLayout.doLayout(subcoll);
this._addMainNode(groups, subcoll, diagram);
}
}
this.arrangingLayout.doLayout(groups.toKeySet());
const git = groups.iterator;
while (git.next()) {
const grp = git.key;
const ginfo = git.value;
this.moveSubgraph(ginfo.parts, ginfo.bounds, new go.Rect(grp.position, grp.desiredSize));
}
bounds = diagram.computePartsBounds(groups.toKeySet()); // not maincoll due to links without real bounds
}
else {
// no this.arrangingLayout
this.preparePrimaryLayout(this.primaryLayout, maincoll);
this.primaryLayout.doLayout(maincoll);
bounds = diagram.computePartsBounds(maincoll);
this.moveSubgraph(maincoll, bounds, bounds);
}
if (!bounds.isReal())
bounds = new go.Rect(0, 0, 0, 0);
this.prepareSideLayout(this.sideLayout, sidecoll, bounds);
if (sidecoll.count > 0) {
this.sideLayout.doLayout(sidecoll);
let sidebounds = diagram.computePartsBounds(sidecoll);
if (!sidebounds.isReal())
sidebounds = new go.Rect(0, 0, 0, 0);
this.moveSideCollection(sidecoll, bounds, sidebounds);
}
diagram.commitTransaction('Arranging Layout');
}
/**
* @hidden @internal
* @param subcoll
*/
_addMainNode(groups, subcoll, diagram) {
const grp = new go.Node();
grp.locationSpot = go.Spot.Center;
const grpb = diagram.computePartsBounds(subcoll);
grp.desiredSize = grpb.size;
grp.position = grpb.position;
groups.set(grp, { parts: subcoll, bounds: grpb });
}
/**
* Assign all of the Parts in the given collection into either the
* set of Nodes and Links for the main graph or the set of Nodes and Links
* for the side graph.
*
* By default this just calls the {@link filter} on each non-Link to decide,
* and then looks at each Link's connected Nodes to decide.
*
* A null filter assigns all Nodes that have connected Links to the main graph, and
* all Links will be assigned to the main graph, and the side graph will only contain
* Parts with no connected Links.
* @param coll
* @param maincoll
* @param sidecoll
*/
splitParts(coll, maincoll, sidecoll) {
// first consider all Nodes
const pred = this.filter;
coll.each((p) => {
if (p instanceof go.Link)
return;
let main;
if (pred)
main = pred(p);
else if (p instanceof go.Node)
main = p.linksConnected.count > 0;
else
main = p instanceof go.Link;
if (main) {
maincoll.add(p);
}
else {
sidecoll.add(p);
}
});
// now assign Links based on which Nodes they connect with
coll.each((p) => {
if (p instanceof go.Link) {
if (!p.fromNode || !p.toNode)
return;
if (maincoll.has(p.fromNode) && maincoll.has(p.toNode)) {
maincoll.add(p);
}
else if (sidecoll.has(p.fromNode) && sidecoll.has(p.toNode)) {
sidecoll.add(p);
}
}
});
}
/**
* This method is called just before the primaryLayout is performed so that
* there can be adjustments made to the primaryLayout, if desired.
* By default this method makes no adjustments to the primaryLayout.
* @param primaryLayout - the primaryLayout that may be modified for the achieving different results from that layout
* @param mainColl - the Nodes and Links to be laid out by primaryLayout after being separated into subnetworks
*/
preparePrimaryLayout(primaryLayout, mainColl) {
// by default this is a no-op
}
/**
* Move a Set of Nodes and Links to the given area.
* @param subColl - the Set of Nodes and Links that form a separate connected subnetwork
* @param subbounds - the area occupied by the subColl
* @param bounds - the area where they should be moved according to the arrangingLayout
*/
moveSubgraph(subColl, subbounds, bounds) {
const diagram = this.diagram;
if (!diagram)
return;
diagram.moveParts(subColl, bounds.position.subtract(subbounds.position));
}
/**
* This method is called just after the main layouts (the primaryLayouts and arrangingLayout)
* have been performed and just before the sideLayout is performed so that there can be
* adjustments made to the sideLayout, if desired.
* By default this method makes no adjustments to the sideLayout.
* @param sideLayout - the sideLayout that may be modified for the results of the main layouts
* @param sideColl - the Nodes and Links filtered out to be laid out by sideLayout
* @param mainBounds - the area occupied by the nodes and links of the main layout, after it was performed
*/
prepareSideLayout(sideLayout, sideColl, mainBounds) {
// by default this is a no-op
}
/**
* This method is called just after the sideLayout has been performed in order to move
* its parts to the desired area relative to the results of the main layouts.
* By default this calls {@link go.Diagram.moveParts} on the sidecoll collection to the {@link side} of the mainbounds.
* This won't get called if there are no Parts in the sidecoll collection.
* @param sidecoll - a collection of Parts that were laid out by the sideLayout
* @param mainbounds - the area occupied by the results of the main layouts
* @param sidebounds - the area occupied by the results of the sideLayout
*/
moveSideCollection(sidecoll, mainbounds, sidebounds) {
const diagram = this.diagram;
if (!diagram)
return;
let pos = null;
if (this.side.equals(go.Spot.Bottom)) {
pos = new go.Point(mainbounds.centerX - sidebounds.width / 2, mainbounds.y + mainbounds.height + this.spacing.height);
}
else if (this.side.equals(go.Spot.Right)) {
pos = new go.Point(mainbounds.x + mainbounds.width + this.spacing.width, mainbounds.centerY - sidebounds.height / 2);
}
else if (this.side.equals(go.Spot.Top)) {
pos = new go.Point(mainbounds.centerX - sidebounds.width / 2, mainbounds.y - sidebounds.height - this.spacing.height);
}
else if (this.side.equals(go.Spot.Left)) {
pos = new go.Point(mainbounds.x - sidebounds.width - this.spacing.width, mainbounds.centerY - sidebounds.height / 2);
}
else if (this.side.includesSide(go.Spot.BottomSide)) {
pos = new go.Point(mainbounds.x, mainbounds.y + mainbounds.height + this.spacing.height);
}
else if (this.side.includesSide(go.Spot.RightSide)) {
pos = new go.Point(mainbounds.x + mainbounds.width + this.spacing.width, mainbounds.y);
}
else if (this.side.includesSide(go.Spot.TopSide)) {
pos = new go.Point(mainbounds.x, mainbounds.y - sidebounds.height - this.spacing.height);
}
else if (this.side.includesSide(go.Spot.LeftSide)) {
pos = new go.Point(mainbounds.x - sidebounds.width - this.spacing.width, mainbounds.y);
}
if (pos !== null) {
diagram.moveParts(sidecoll, pos.subtract(sidebounds.position));
}
}
// Public properties
/**
* Gets or sets the predicate function to call on each non-Link.
* If the predicate returns true, the part will be laid out by the main layouts,
* the primaryLayouts and the arrangingLayout, otherwise by the sideLayout.
* The default value is a function that is true when there are any links connecting with the node.
* Such default behavior will have the sideLayout position all of the singleton nodes.
*/
get filter() {
return this._filter;
}
set filter(val) {
if (this._filter !== val) {
if (val && typeof val !== 'function')
throw new Error('new value for ArrangingLayout.filter must be a function, not: ' + val);
this._filter = val;
this.invalidateLayout();
}
}
/**
* Gets or sets the side {@link go.Spot} where the side nodes and links should be laid out,
* relative to the results of the main Layout.
* The default value is Spot.BottomSide.
*
* If the value is Spot.Bottom, Spot.Top, Spot.Right, or Spot.Left,
* the side nodes will be centered along that side.
*
* Currently only handles a single side.
*/
get side() {
return this._side;
}
set side(val) {
if (!this._side.equals(val)) {
if (!(val instanceof go.Spot) ||
!(val.isSide() ||
val.equals(go.Spot.Top) ||
val.equals(go.Spot.Right) ||
val.equals(go.Spot.Bottom) ||
val.equals(go.Spot.Left))) {
throw new Error('new value for ArrangingLayout.side must be a side or middle-side Spot, not: ' + val);
}
this._side = val.copy();
this.invalidateLayout();
}
}
/**
* Gets or sets the space between the main layout and the side layout.
* The default value is Size(20, 20).
*/
get spacing() {
return this._spacing;
}
set spacing(val) {
if (!this._spacing.equals(val)) {
if (!(val instanceof go.Size))
throw new Error('new value for ArrangingLayout.spacing must be a Size, not: ' + val);
this._spacing = val.copy();
this.invalidateLayout();
}
}
/**
* Gets or sets the Layout used for the main part of the diagram.
* The default value is an instance of GridLayout.
* Any new value must not be null.
*/
get primaryLayout() {
return this._primaryLayout;
}
set primaryLayout(val) {
if (this._primaryLayout !== val) {
if (!(val instanceof go.Layout))
throw new Error('layout does not inherit from go.Layout: ' + val);
this._primaryLayout = val;
this.invalidateLayout();
}
}
/**
* Gets or sets the Layout used to arrange multiple separate connected subnetworks of the main graph.
* The default value is an instance of GridLayout.
* Set this property to null in order to get the {@link primaryLayout} to operate on all
* connected graphs as a whole.
*/
get arrangingLayout() {
return this._arrangingLayout;
}
set arrangingLayout(val) {
if (this._arrangingLayout !== val) {
if (val && !(val instanceof go.Layout))
throw new Error('layout does not inherit from go.Layout: ' + val);
this._arrangingLayout = val;
this.invalidateLayout();
}
}
/**
* Gets or sets the Layout used to arrange the 'side' nodes and links -- those outside of the main layout.
* The default value is an instance of GridLayout.
* Any new value must not be null.
*/
get sideLayout() {
return this._sideLayout;
}
set sideLayout(val) {
if (this._sideLayout !== val) {
if (!(val instanceof go.Layout))
throw new Error('layout does not inherit from go.Layout: ' + val);
this._sideLayout = val;
this.invalidateLayout();
}
}
}