Dear Support,
I am using a custom LayerdDigraphLayout that knows about bands and that positions each node in its respective band, which was provided by Walter during my GoJS evaluation period a couple of months ago. (https://gojs.net/temp/swimBands%20copy3.html)
Now I am using an adapted angular version of the provided layout BandedLayeredDigraphLayout
.
Although the layout very much meets our needs, there is one major issue, which i - so far - could not adequately address.
In the image below is an example of a diagram that shows a history of data objects (version number is shown in the node):
As can be seen, the nodes favor a left-sided layout in each band.
This leads to links not going straight down, but going to the left.
One example are the outgoing connections from version 1478.
With this behavior, it becomes difficult to determine which nodes are connected to 1478.
In order to increase visibility, I would like to spread the connections more into the horizontal direction with connections favoring straight lines, when possible.
The desired outcome should look something like that:
I already played around a bit with changing the values of the property columnSpacing
as well as layerSpacing
, but that did not have the desired effect, since the nodes are only spread out in general, while not changing the general layout behavior.
Could you please help in this regard?
This is the layout I use:
import go from 'gojs';
/**
* A custom LayeredDigraphLayout that knows about "bands"
* and that positions each node in its respective band.
*
* @category Layout Extension
*/
export interface BandDescriptor extends go.ObjectData {
text: string;
creationWeek: number;
creationYear: number;
start: number;
depth: number;
}
// NOTE: "Swimlane" layouts have the natural flow and growth of the layout go in the direction of the lanes.
// Nodes need to be annotated to indicate which lane they should be in.
// "Banded" layouts have the bands around layers of nodes that are laid out perpendicular to the natural flow.
// Nodes need to be annotated to indicate which band they should be in.
// Perform a LayeredDigraphLayout where each node is assigned to a "band", each of which may consist of multiple layers.
// LayeredDigraphLayout.commitLayers is overridden to modify the background Part whose key is "_BANDS".
// It is assumed that the _BANDS's data object's "descriptors" property will hold an Array of Objects, each describing a band.
// This Array determines the order of the bands, not the precedence order given by the nodes and links
// combined with the "band" property on each node data object identifying which band the node must be in.
// The node data "band" value should match one of the band descriptor's "text" property in the _BANDS descriptors.
// If the "band" property is not present or if it names a non-existent band, there will be a console warning,
// and the layout results might look odd.
export class BandedLayeredDigraphLayout extends go.LayeredDigraphLayout {
private bandNames: go.Map<string, any> = new go.Map();
private collectVertexesForBands(): go.Map<string, go.Set<go.LayeredDigraphVertex>> {
const bandVertexes = new go.Map<string, go.Set<go.LayeredDigraphVertex>>();
const vertexIterator = this.network.vertexes.iterator;
while (vertexIterator.next()) {
const vertex: go.LayeredDigraphVertex = vertexIterator.value as go.LayeredDigraphVertex;
if (vertex.node === null || vertex.node.data === null) {
continue;
}
const band = vertex.node.data.bandName;
if (!this.bandNames.get(band)) {
console.log(
'BandedLayeredDigraphLayout: unknown band name on node data: '
+ band + ' on node of key: ' + vertex.node.key
);
}
if (!bandVertexes.get(band)) {
bandVertexes.set(band, new go.Set<go.LayeredDigraphVertex>());
}
bandVertexes.get(band).add(vertex);
}
return bandVertexes;
}
private walkAllWithinBand(name: string, vertex: go.LayeredDigraphVertex, depths: go.Map<go.LayeredDigraphVertex, number>): number {
if (depths.has(vertex)) {
return depths.get(vertex);
}
let depth = 0;
vertex.sourceVertexes.each(sourceVertex => {
if (!sourceVertex.node || !sourceVertex.node.data || sourceVertex.node.data.bandName !== name) {
// ignore vertexes of different band
return;
}
depth = Math.max(depth, this.walkAllWithinBand(name, sourceVertex as go.LayeredDigraphVertex, depths));
});
depths.set(vertex, depth + 1);
return depth + 1;
}
// eslint-disable-next-line max-len
private determineBandDepthsAndLayerCount(bandVertexes: go.Map<string, go.Set<go.LayeredDigraphVertex>>): [go.Map<go.LayeredDigraphVertex, number>, number] {
// for each band, determine how many layers are needed by looking for longest
// chain of vertexes with that band name
let totalLayerCount = 0;
const allBandDepths = new go.Map<go.LayeredDigraphVertex, number>(); // map vertex to relative depth within band
bandVertexes.each(bandVertex => {
const name: string = bandVertex.key;
const vertexes: go.Set<go.LayeredDigraphVertex> = bandVertex.value;
const vertexDepthPairs: go.Map<go.LayeredDigraphVertex, number> = new go.Map(); // map vertex to relative depth within band NAME
vertexes.each(vertex => this.walkAllWithinBand(name, vertex, vertexDepthPairs));
let max = 0;
vertexDepthPairs.each(vertexDepthPair => {
max = Math.max(max, vertexDepthPair.value);
allBandDepths.set(vertexDepthPair.key, vertexDepthPair.value);
});
const bandDescriptor = this.bandNames.get(name);
if (bandDescriptor) {
bandDescriptor.depth = max;
}
totalLayerCount += Math.max(max, 1); // assume a band requires a layer, even if there are no nodes in it
});
return [allBandDepths, totalLayerCount];
}
private assignLayerToVertexes(allBandDepths: go.Map<go.LayeredDigraphVertex, number>, totalLayerCount: number) {
// assign vertex.layer for each vertex
const vertexIterator = this.network.vertexes.iterator;
while (vertexIterator.next()) {
const vertex = vertexIterator.value as go.LayeredDigraphVertex;
if (vertex.node === null || vertex.node.data === null) {
continue;
}
const name = vertex.node.data.bandName;
const bandDescriptor = this.bandNames.get(name);
const depth = allBandDepths.get(vertex);
if (bandDescriptor && typeof depth === 'number') {
// reverse the layer number -- last layer is number zero
vertex.layer = totalLayerCount - (bandDescriptor.start + depth);
}
}
}
protected assignLayers() {
// map band name to band descriptor holding name and layer ranges
this.bandNames = new go.Map<string, any>();
const bands = this.diagram.findPartForKey('_BANDS');
if (!bands) {
return;
}
const bandDescriptors: Array<BandDescriptor> = bands.data.descriptors;
if (!Array.isArray(bandDescriptors)) {
return;
}
for (const bandDescriptor of bandDescriptors) {
this.bandNames.set(bandDescriptor.text, bandDescriptor);
}
// collect all vertexes for each band
const bandVertexes = this.collectVertexesForBands();
// determine depth for each band and total number of needed layers
const [allBandDepths, totalLayerCount] = this.determineBandDepthsAndLayerCount(bandVertexes);
// iterate over the bands and assign layer number ranges
let layer = 0;
for (const bandDescriptor of bandDescriptors) {
bandDescriptor.start = layer;
layer += bandDescriptor.depth;
}
this.assignLayerToVertexes(allBandDepths, totalLayerCount);
}
protected commitLayers(layerRects: Array<go.Rect>, offset: go.Point) {
// update the background object holding the visual "bands"
const bands = this.diagram.findPartForKey('_BANDS');
if (bands) {
const model = this.diagram.model;
bands.location = this.arrangementOrigin.copy().add(offset);
// set the bounds of each band via data binding of the "bounds" property
const bandDescriptors = bands.data.descriptors;
let j = 0;
for (let i = 0; i < bandDescriptors.length && j < layerRects.length; i++) {
const bandDescriptor = bandDescriptors[i];
if (!bandDescriptor){
continue;
}
let k = j;
let thickness = 0;
for (; k < Math.min(j+bandDescriptor.depth, layerRects.length); k++) {
if (this.direction === 90 || this.direction === 270) {
thickness += layerRects[k].height;
} else {
thickness += layerRects[k].width;
}
}
const r = layerRects[j].copy(); // first rect gives shared bounds info
if (this.direction === 90 || this.direction === 270) {
if (this.direction === 270) {
r.y = layerRects[k-1].y;
}
r.height = thickness;
} else {
if (this.direction === 180) {
r.x = layerRects[k-1].x;
}
r.width = thickness;
}
model.set(bandDescriptor, 'bounds', r); // an actual Rect, not stringified
j = k; // next layer/band
}
}
this.bandNames = null; // cleanup
}
}
This is the node template:
$(go.Node, 'Spot',
{
fromSpot: horizontal ? go.Spot.Right : go.Spot.Bottom,
toSpot: horizontal ? go.Spot.Left : go.Spot.Top,
fromEndSegmentLength: 16,
toEndSegmentLength: 16,
movable: false,
resizable: false,
selectionAdorned: false,
},
$(go.Panel, 'Auto',
{
desiredSize: new go.Size(184, 68),
},
$(go.Shape, 'RoundedRectangle',
{
parameter1: 4, // Specifies the corner radius for RoundedRectangle figure.
stretch: go.GraphObject.Fill,
fill: 'white',
stroke: 'black',
strokeWidth: 2,
},
),
$(go.TextBlock,
{
font: 'bold 64px Noto Sans',
},
new go.Binding('text', 'version'),
),
)
);
Please let me know if you want me to provide the node and link data.
The example I prepared is rather large (54KiB node data and 44KiB of link data) and I do not want to insert a block comment containing information of this size without asking first.
Please let me know if you need more information.
Thanks in advance for your help.