/*
* 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 part => true
.
* 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 Arranging Layout 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();
}
}
}