Hi, I am working on layout the diagram. I have nodes (with already known lanes) and edges.
I used SwimLaneLayout to have lanes sitting up and down of each other and to layout diagram using go.LayeredDiagraphLayout. they both work very well when they are separate but when I try to combine both of them then I lost lanes.
myDiagram = new go.Diagram('myDiagramDiv', {
initialAutoScale: go.AutoScale.Uniform,
allowCopy: false,
layout: new SwimLaneLayout({
laneProperty: 'group',
direction: DIRECTION,
setsPortSpots: true,
layerSpacing: 20,
columnSpacing: 5,
commitLayers: function (layerRects, offset) {
if (layerRects.length === 0) return;
var horiz = this.direction === 0 || this.direction === 180;
var forwards = this.direction === 0 || this.direction === 90;
var rect = layerRects[forwards ? layerRects.length - 1 : 0];
var totallength = horiz ? rect.right : rect.bottom;
if (horiz) {
offset.y -= this.columnSpacing * 3 / 2;
} else {
offset.x -= this.columnSpacing * 3 / 2;
}
for (var i = 0; i < this.laneNames.length; i++) {
var lane = this.laneNames[i];
var group = this.diagram.findNodeForKey(lane);
if (group === null) {
this.diagram.model.addNodeData({ key: lane, isGroup: true });
group = this.diagram.findNodeForKey(lane);
}
if (horiz) {
group.location = new go.Point(-this.layerSpacing / 2, this.lanePositions.get(lane) * this.columnSpacing + offset.y);
} else {
group.location = new go.Point(this.lanePositions.get(lane) * this.columnSpacing + offset.x, -this.layerSpacing / 2);
}
var ph = group.findObject('PLACEHOLDER');
if (ph === null) ph = group;
if (horiz) {
ph.desiredSize = new go.Size(totallength, this.laneBreadths.get(lane) * this.columnSpacing);
} else {
ph.desiredSize = new go.Size(this.laneBreadths.get(lane) * this.columnSpacing, totallength);
}
}
},
doLayout: function (coll) {
var diagram = this.diagram;
if (diagram === null) return;
diagram.startTransaction("custom layout");
console.log(this, this?.iterator)
diagram.nodes.each(function (n) {
coll.add(n);
});
console.log(coll)
// Custom logic to adjust node positioning
var layout = new go.LayeredDigraphLayout();
layout.direction = this.direction; // use the same direction as the SwimLaneLayout
layout.setsPortSpots = false;
layout.layerSpacing = this.layerSpacing;
layout.columnSpacing = this.columnSpacing;
layout.doLayout(coll);
diagram.commitTransaction("custom layout");
}
})
});
Desired Result:
I want diagram with lanes and arranged nodes to have as less cross links as we can either forward links or back links (links going to any node which has already processed/past for the current node).
Are you only asking about the positioning of the nodes? Not about how the links are routed?
Yes, the second application of LayeredDigraphLayout has no knowledge about lanes, which is why you are getting those results – it is just discarding all of the work done by SwimLaneLayout.
So the change in the ordering of nodes has to happen within SwimLaneLayout. If you look at the implementation of SwimLaneLayout, you will see an override of LayeredDigraphLayout.reduceCrossings that calls reduceCrossings on a SwimLaneLayout.reducer object.
Try setting that property to an instance of this experimental class, below. For example:
layout:
new SwimLaneLayout({
reducer: new CrossingsReducer(),
. . .
})
CrossingsReducer.js:
/*
* Copyright (C) 1998-2024 by Northwoods Software Corporation. All Rights Reserved.
*/
class CrossingsReducer {
constructor() {
this.dataFunction = x => x.data;
this._laneIndexes = new go.Map();
// internal state
this._aset = new go.Set();
this._bset = new go.Set();
}
get laneIndexes() { return this._laneIndexes; }
set laneIndexes(value) { this._laneIndexes = value; }
// assume the AARR is sorted by lane
reduceCrossings(aarr, barr) {
if (aarr.length <= 1) return false;
// iterate over continguous sections of the Array that have the same lane
let count = 0;
let s = 0;
let lane = this.findLane(aarr[s]);
let changed = true;
while (count < 10 && changed) {
changed = false;
let e = s;
while (e < aarr.length) {
while (e < aarr.length && this.findLane(aarr[e]) === lane) e++;
changed |= this._reduceCrossingsPartial(aarr, s, e, barr);
if (e < aarr.length) lane = this.findLane(aarr[e]);
s = e;
}
count++;
s = 0;
}
this._aset.clear();
this._bset.clear();
return count > 1;
}
findLane(node) {
return this.dataFunction(node).lane || '';
}
getLaneIndex(node) {
return this.laneIndexes.get(this.findLane(node));
}
getIndex(node) {
return this.dataFunction(node).index || 0;
}
getBary(node) {
return this.dataFunction(node).bary || 0;
}
setBary(node, val) {
this.dataFunction(node).bary = val;
}
getConnectedNodesIterator(node) {
if (node instanceof go.Node) {
return node.findNodesConnected();
} else {
return new go.List().iterator;
}
}
_reduceCrossingsPartial(aarr, start, end, barr) {
if (!Array.isArray(aarr)) throw new Error('expected an Array, not ' + arr);
if (aarr.length <= 1) return false; // nothing to do
if (start < 0) start = 0;
if (end > aarr.length) end = aarr.length;
if (start >= end - 1) return false; // nothing to do
this._aset.clear();
for (let i = start; i < end; i++) this._aset.add(aarr[i]);
const arr = this._aset.toArray(); // just the nodes of AARR from START up to END
this._bset.clear();
this._bset.addAll(barr);
const roots = this._aset.copy();
const conns = [];
while (roots.count > 0) {
const root = roots.first();
if (this.getConnectedNodesIterator(root).count > 0) {
const conn = new go.Set();
this._follow(root, this._aset, this._bset, conn);
conns.push(conn);
roots.removeAll(conn);
} else {
this.setBary(root, this._aset.count);
roots.remove(root);
}
}
conns.sort((c1, c2) => {
if (c1.count < c2.count) return -1;
if (c1.count > c2.count) return 1;
return 0;
});
conns.forEach(conn => {
conn.each(node => {
if (this._aset.has(node)) {
let bary = 0;
const it = this.getConnectedNodesIterator(node);
const num = it.count;
it.each(m => {
if (!this._bset.has(m)) return;
bary += this.getLaneIndex(m) * 10000 + this.getIndex(m);
});
this.setBary(node, num ? bary / num : arr.length);
}
});
});
arr.sort((x, y) => {
const xc = this.getBary(x);
const yc = this.getBary(y);
if (xc < yc) return -1;
if (xc > yc) return 1;
return 0;
});
let modified = false;
for (let i = 0; i < arr.length; i++) {
if (aarr[start + i] !== arr[i]) modified = true;
aarr[start + i] = arr[i];
}
return modified;
}
_follow(node, aset, bset, conn) {
if (conn.has(node)) return;
conn.add(node);
this.getConnectedNodesIterator(node).each(m => {
if (bset.has(m)) {
if (!conn.has(m)) conn.add(m);
//this._follow(m, bset, aset, conn);
}
});
}
} // end of CrossingsReducer
Thanks Walter for quick response really appreciate it.
So there are two things
Is it possible to use both LayeredDiagraph and SwimLane layout applied to a diagram? if so, can you help with above given code as its not applying both for me.
Other is to reduce the links crossing where possible or move node close to its target node lane. for example in below first example I added images with before and after (manually arranged) nodes. The Crossing Reducer didn’t help with the below two example.
I want the nodes to be close to the lane of its target, as in example 1 there are 4 nodes coming out from XOR Rule1 node two have no further nodes attached so then can be in center and the node (1)Very high risk country Payment not sent to prohibited country/regions has target node in lane after and the node (2)Very high risk country Payment sent to prohibited country/regions has target node in before lane but their current positions make their outgoing links to cross each other but if somehow I can move any node to close to its lane (in this case swap both nodes can help a alot),]. Is there any idea/help for this problem i can get?
In below example there is possibility that nodes can be arranged without crossing each other links (as we can see in before and after image) but they still cross which I want to avoid.
I also have an example where there are two lanes, when there is a large group of nodes just in one lane which itself look like a a complete diagram but using SwimLane layout it doesn’t look very well maybe because of not enough spacing.
Both diagrams are generated using same data but first one using SwimLane (there are no x, y assigned to any node) and other one is generated by assigning x, y values of each node using custom algorithm.
yeah that algorithm don’t work completely correct as you can see there are multiple selected nodes in the image which were at the below white space which I selected and move up to show what I’m looking for.
Can I achieve something like this with SwimLaneLayout?
I have updated the code for that experimental CrossingsReducer class. But it requires an updated SwimLaneLayout too. Here’s that code:
/*
* Copyright (C) 1998-2024 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* 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 LayeredDigraphLayout that knows about "lanes"
* and that positions each node in its respective lane.
* This assumes that each Node.data.lane property is a string that names the lane the node should be in.
* You can set the {@link laneProperty} property to use a different data property name.
* It is commonplace to set this property to be the same as the {@link go.GraphLinksModel.nodeGroupKeyProperty},
* so that the one property indicates that a particular node data is a member of a particular group
* and thus that that group represents a lane.
* The lanes can be sorted by specifying the {@link laneComparer} function.
* You can add extra space between the lanes by increasing {@link laneSpacing} from its default of zero.
* That number's unit is columns, {@link go.LayeredDigraphLayout.columnSpacing}, not in document coordinates.
*
* If you want to experiment with this extension, try the <a href="../../samples/SwimLaneLayout.html">SwimLaneLayout</a> sample.
* @category Layout Extension
*/
class SwimLaneLayout extends go.LayeredDigraphLayout {
constructor(init) {
super();
this._laneProperty = 'lane'; // how to get lane identifier string from node data
this._laneNames = []; // lane names, may be sorted using this.laneComparer
this._laneIndexes = new go.Map();
this._laneComparer = null;
this._laneSpacing = 0; // in columns
this._router = { linkSpacing: 4 };
this._reducer = null;
// computed, read-only state
this._lanePositions = new go.Map(); // lane names --> start columns, left to right
this._laneBreadths = new go.Map(); // lane names --> needed width in columns
// internal state
this._layers = [[]];
this._neededSpaces = [];
if (init)
Object.assign(this, init);
}
/**
* Gets or sets the name of the data property that holds the string which is the name of the lane that the node should be in.
* The default value is "lane".
*/
get laneProperty() {
return this._laneProperty;
}
set laneProperty(val) {
if (typeof val !== 'string' && typeof val !== 'function')
throw new Error('new value for SwimLaneLayout.laneProperty must be a property name, not: ' + val);
if (this._laneProperty !== val) {
this._laneProperty = val;
this.invalidateLayout();
}
}
/**
* Gets or sets an Array of lane names.
* If you set this before a layout happens, it will use those lanes in that order.
* Any additional lane names that it discovers will be added to the end of this Array.
*
* This property is reset to an empty Array at the end of each layout.
* The default value is an empty Array.
*/
get laneNames() {
return this._laneNames;
}
set laneNames(val) {
if (!Array.isArray(val))
throw new Error('new value for SwimLaneLayout.laneNames must be an Array, not: ' + val);
if (this._laneNames !== val) {
this._laneNames = val;
this.invalidateLayout();
}
}
/**
* Gets or sets a function by which to compare lane names, for ordering the lanes within the {@link laneNames} Array.
* By default the function is null -- the lanes are not sorted.
*/
get laneComparer() {
return this._laneComparer;
}
set laneComparer(val) {
if (typeof val !== 'function')
throw new Error('new value for SwimLaneLayout.laneComparer must be a function, not: ' + val);
if (this._laneComparer !== val) {
this._laneComparer = val;
this.invalidateLayout();
}
}
/**
* Gets or sets the amount of additional space it allocates between the lanes.
* This number specifies the number of columns, with the same meaning as {@link go.LayeredDigraphLayout.columnSpacing}.
* The number unit is not in document coordinate or pixels.
* The default value is zero columns.
*/
get laneSpacing() {
return this._laneSpacing;
}
set laneSpacing(val) {
if (typeof val !== 'number')
throw new Error('new value for SwimLaneLayout.laneSpacing must be a number, not: ' + val);
if (this._laneSpacing !== val) {
this._laneSpacing = val;
this.invalidateLayout();
}
}
/**
* @hidden
*/
get router() {
return this._router;
}
set router(val) {
if (this._router !== val) {
this._router = val;
this.invalidateLayout();
}
}
/**
* @hidden
*/
get reducer() {
return this._reducer;
}
set reducer(val) {
if (this._reducer !== val) {
this._reducer = val;
if (val) {
const lay = this;
val.findLane = (v) => lay.getLane(v);
val.getIndex = (v) => v.index;
val.getBary = (v) => v.bary || 0;
val.setBary = (v, value) => (v.bary = value);
val.getConnectedNodesIterator = (v) => v.vertexes;
}
this.invalidateLayout();
}
}
/**
* The computed positions of each lane,
* in the form of a {@link go.Map} mapping lane names (strings) to numbers.
*/
get lanePositions() {
return this._lanePositions;
}
/**
* The computed breadths (widths or heights depending on the direction) of each lane,
* in the form of a {@link go.Map} mapping lane names (strings) to numbers.
*/
get laneBreadths() {
return this._laneBreadths;
}
/**
* @hidden
* @param coll
*/
doLayout(coll) {
this.lanePositions.clear(); // lane names --> start columns, left to right
this.laneBreadths.clear(); // lane names --> needed width in columns
this._layers = [[]];
this._neededSpaces = [];
super.doLayout(coll);
this.lanePositions.clear();
this.laneBreadths.clear();
this._layers = [[]];
this._neededSpaces = [];
}
/**
* @hidden
* @param v
* @param topleft
*/
nodeMinLayerSpace(v, topleft) {
if (!this._neededSpaces)
this._neededSpaces = this.computeNeededLayerSpaces(this.network);
if (v.node === null)
return 0;
let lay = v.layer;
if (!topleft) {
if (lay > 0)
lay--;
}
const overlaps = (this._neededSpaces[lay] || 0) / 2;
const edges = this.countEdgesForDirection(v, this.direction > 135 ? !topleft : topleft);
const needed = Math.max(overlaps, edges) * this.router.linkSpacing * 1.5;
if (this.direction === 90 || this.direction === 270) {
if (topleft) {
return v.focus.y + 10 + needed;
}
else {
return v.bounds.height - v.focus.y + 10 + needed;
}
}
else {
if (topleft) {
return v.focus.x + 10 + needed;
}
else {
return v.bounds.width - v.focus.x + 10 + needed;
}
}
}
countEdgesForDirection(vertex, topleft) {
let c = 0;
const lay = vertex.layer;
vertex.edges.each((e) => {
if (topleft) {
if (e.getOtherVertex(vertex).layer >= lay)
c++;
}
else {
if (e.getOtherVertex(vertex).layer <= lay)
c++;
}
});
return c;
}
computeNeededLayerSpaces(net) {
// group all edges by their connected vertexes' least layer
const layerMinEdges = [];
net.edges.each((e) => {
// consider all edges, including dummy ones!
const f = e.fromVertex;
const t = e.toVertex;
if (f.column === t.column)
return; // skip edges that don't go between columns
if (Math.abs(f.layer - t.layer) > 1)
return; // skip edges that don't go between adjacent layers
const lay = Math.min(f.layer, t.layer);
let arr = layerMinEdges[lay];
if (!arr)
arr = layerMinEdges[lay] = [];
arr.push(e);
});
// sort each array of edges by their lowest connected vertex column
// for edges with the same minimum column, sort by their maximum column
const layerMaxEdges = []; // same as layerMinEdges, but sorted by maximum column
layerMinEdges.forEach((arr, lay) => {
if (!arr)
return;
arr.sort((e1, e2) => {
const f1c = e1.fromVertex.column;
const t1c = e1.toVertex.column;
const f2c = e2.fromVertex.column;
const t2c = e2.toVertex.column;
const e1mincol = Math.min(f1c, t1c);
const e2mincol = Math.min(f2c, t2c);
if (e1mincol > e2mincol)
return 1;
if (e1mincol < e2mincol)
return -1;
const e1maxcol = Math.max(f1c, t1c);
const e2maxcol = Math.max(f2c, t2c);
if (e1maxcol > e2maxcol)
return 1;
if (e1maxcol < e2maxcol)
return -1;
return 0;
});
layerMaxEdges[lay] = arr.slice(0);
layerMaxEdges[lay].sort((e1, e2) => {
const f1c = e1.fromVertex.column;
const t1c = e1.toVertex.column;
const f2c = e2.fromVertex.column;
const t2c = e2.toVertex.column;
const e1maxcol = Math.max(f1c, t1c);
const e2maxcol = Math.max(f2c, t2c);
if (e1maxcol > e2maxcol)
return 1;
if (e1maxcol < e2maxcol)
return -1;
const e1mincol = Math.min(f1c, t1c);
const e2mincol = Math.min(f2c, t2c);
if (e1mincol > e2mincol)
return 1;
if (e1mincol < e2mincol)
return -1;
return 0;
});
});
// run through each array of edges to count how many overlaps there might be
const layerOverlaps = [];
layerMinEdges.forEach((arr, lay) => {
const mins = arr; // sorted by min column
const maxs = layerMaxEdges[lay]; // sorted by max column
let maxoverlap = 0; // maximum count for this layer
if (mins && maxs && mins.length > 1 && maxs.length > 1) {
let mini = 0;
let min = null;
let maxi = 0;
let max = null;
while (mini < mins.length || maxi < maxs.length) {
if (mini < mins.length)
min = mins[mini];
const mincol = min
? Math.min(min.fromVertex.column, min.toVertex.column)
: 0;
if (maxi < maxs.length)
max = maxs[maxi];
const maxcol = max
? Math.max(max.fromVertex.column, max.toVertex.column)
: Infinity;
maxoverlap = Math.max(maxoverlap, Math.abs(mini - maxi));
if (mincol <= maxcol && mini < mins.length) {
mini++;
}
else if (maxi < maxs.length) {
maxi++;
}
}
}
layerOverlaps[lay] = maxoverlap * 1.5; // # of parallel links
});
return layerOverlaps;
}
setupLanes() {
// set up some data structures
const layout = this;
const laneIndexes = new go.Map(); // lane name --> index when sorted
// initialize temporarily with any known lane names
this.laneNames.forEach((name, idx) => laneIndexes.set(name, idx));
const vit = this.network.vertexes.iterator;
while (vit.next()) {
const v = vit.value;
// discover any more lane names
const lane = this.getLane(v); // cannot call findLane yet
if (lane !== null && !laneIndexes.has(lane)) {
this.laneNames.push(lane);
}
const layer = v.layer;
if (layer >= 0) {
const arr = this._layers[layer];
if (!arr) {
this._layers[layer] = [v];
}
else {
arr.push(v);
}
}
}
// sort laneNames and initialize laneIndexes with sorted indexes
if (typeof this.laneComparer === 'function')
this.laneNames.sort(this.laneComparer);
for (let i = 0; i < this.laneNames.length; i++) {
laneIndexes.set(this.laneNames[i], i);
}
this._laneIndexes = laneIndexes;
// now OK to call findLane
// sort vertexes so that vertexes are grouped by lane
for (let i = 0; i <= this.maxLayer; i++) {
this._layers[i].sort((a, b) => layout.compareVertexes(a, b));
}
}
/**
* @hidden
* Replace the standard reduceCrossings behavior so that it respects lanes.
*/
reduceCrossings() {
this.setupLanes();
// this just cares about the .index and ignores .column
const layers = this._layers;
const red = this.reducer;
if (red) {
red.laneIndexes = this._laneIndexes;
for (let i = 1; i < layers.length; i++) {
red.reduceCrossings(layers[i], layers[i - 1]);
layers[i].forEach((v, j) => (v.index = j));
}
for (let i = layers.length - 2; i >= 0; i--) {
red.reduceCrossings(layers[i], layers[i + 1]);
layers[i].forEach((v, j) => (v.index = j));
}
}
this.computeLanes(); // and recompute all vertex.column values
}
computeLanes() {
// compute needed width for each lane, in columns
for (let i = 0; i < this.laneNames.length; i++) {
const lane = this.laneNames[i];
this.laneBreadths.set(lane, this.computeMinLaneWidth(lane));
}
const lwidths = new go.Map(); // reused for each layer
for (let i = 0; i <= this.maxLayer; i++) {
const arr = this._layers[i];
if (arr) {
const layout = this;
// now run through Array finding width (in columns) of each lane
// and max with this.laneBreadths.get(lane)
for (let j = 0; j < arr.length; j++) {
const v = arr[j];
const w = this.nodeMinColumnSpace(v, true) + 1 + this.nodeMinColumnSpace(v, false);
const ln = this.findLane(v) || '';
const totw = lwidths.get(ln);
if (totw === null) {
lwidths.set(ln, w);
}
else {
lwidths.set(ln, totw + w);
}
}
lwidths.each((kvp) => {
const lane = kvp.key;
const colsInLayer = kvp.value;
const colsMax = layout.laneBreadths.get(lane) || 0;
if (colsInLayer > colsMax)
layout.laneBreadths.set(lane, colsInLayer);
});
lwidths.clear();
}
}
// compute starting positions for each lane
let x = 0;
for (let i = 0; i < this.laneNames.length; i++) {
const lane = this.laneNames[i];
this.lanePositions.set(lane, x);
const w = this.laneBreadths.get(lane) || 0;
x += w + this.laneSpacing;
}
this.renormalizeColumns();
}
renormalizeColumns() {
// set new column and index on each vertex
for (let i = 0; i < this._layers.length; i++) {
let prevlane = null;
let c = 0;
const arr = this._layers[i];
for (let j = 0; j < arr.length; j++) {
const v = arr[j];
v.index = j;
const l = this.findLane(v);
if (l && prevlane !== l) {
c = this.lanePositions.get(l) || 0;
const w = this.laneBreadths.get(l) || 0;
// compute needed breadth within lane, in columns
let z = this.nodeMinColumnSpace(v, true) + 1 + this.nodeMinColumnSpace(v, false);
let k = j + 1;
while (k < arr.length && this.findLane(arr[k]) === l) {
const vz = arr[k];
z += this.nodeMinColumnSpace(vz, true) + 1 + this.nodeMinColumnSpace(vz, false);
k++;
}
// if there is extra space, shift the vertexes to the middle of the lane
if (z < w) {
c += Math.floor((w - z) / 2);
}
}
c += this.nodeMinColumnSpace(v, true);
v.column = c;
c += 1;
c += this.nodeMinColumnSpace(v, false);
prevlane = l;
}
}
}
/**
* Return the minimum lane width, in columns
* @param lane
*/
computeMinLaneWidth(lane) {
return 0;
}
/**
* @hidden
* Disable normal straightenAndPack behavior, which would mess up the columns.
*/
straightenAndPack() { }
/**
* Given a vertex, get the lane (name) that its node belongs in.
* If the lane appears to be undefined, this returns the empty string.
* For dummy vertexes (with no node) this will return null.
* @param v
*/
getLane(v) {
if (v === null)
return null;
const node = v.node;
if (node !== null) {
const data = node.data;
if (data !== null) {
let lane = null;
if (typeof this.laneProperty === 'function') {
lane = this.laneProperty(data);
}
else {
lane = data[this.laneProperty];
}
if (typeof lane === 'string')
return lane;
return ''; // default lane
}
}
return null;
}
/**
* This is just like {@link getLane} but handles dummy vertexes
* for which the {@link getLane} returns null by returning the
* lane of the edge's source or destination vertex.
* This can only be called after the lanes have been set up internally.
* @param v
*/
findLane(v) {
if (v !== null) {
const lane = this.getLane(v);
if (lane !== null) {
return lane;
}
else {
const srcv = this.findRealSource(v.sourceEdges.first());
const dstv = this.findRealDestination(v.destinationEdges.first());
const srcLane = this.getLane(srcv);
const dstLane = this.getLane(dstv);
if (srcLane !== null || dstLane !== null) {
if (srcLane === dstLane)
return srcLane;
if (srcLane !== null)
return srcLane;
if (dstLane !== null)
return dstLane;
}
}
}
return null;
}
findRealSource(e) {
if (e === null)
return null;
const fv = e.fromVertex;
if (fv && fv.node)
return fv;
return this.findRealSource(fv.sourceEdges.first());
}
findRealDestination(e) {
if (e === null)
return null;
const tv = e.toVertex;
if (tv.node)
return tv;
return this.findRealDestination(tv.destinationEdges.first());
}
compareVertexes(v, w) {
let laneV = this.findLane(v);
if (laneV === null)
laneV = '';
let laneW = this.findLane(w);
if (laneW === null)
laneW = '';
const idxV = this._laneIndexes.get(laneV) || 0;
const idxW = this._laneIndexes.get(laneW) || 0;
if (idxV < idxW)
return -1;
if (idxV > idxW)
return 1;
// OPTIONAL: sort dummy vertexes before vertexes representing real nodes
if (v.node === null && w.node !== null)
return -1;
if (v.node !== null && w.node === null)
return 1;
if (v.column < w.column)
return -1;
if (v.column > w.column)
return 1;
return 0;
}
}
If you need the TypeScript code instead, I can provide it. In any case this will be in the next release, 3.0.12.