Custom Layout
G6 provides abundant commonly used built-in layouts for Graph and TreeGraph respectively. The usage can be found in: Utilizing Layout, Layout API. Custom layout mechanism of G6 allows the users to design their own type of layout to meet their special requirements.
⚠️Attention: The TreeGraph does not support custom layout temporarily.
In this document, we will introduce the custom layout by registering a layout for Bigraph.
The API of Cumstom Layout
/**
* Register a Layout
* @param {string} type The layout type is must assigned to an unique string
* @param {object} layout The layout method
*/
Layout.registerLayout = function(type, {
/**
* The default configurations of the custom layout. It will be mixed by the configurations from users
*/
getDefaultCfg() {
return {};
},
/**
* Initialize
* @param {object} data data
*/
init(data) {},
/**
* Execute the layout
*/
execute() {},
/**
* Layout with the data
* @param {object} data 数据
*/
layout(data) {},
/**
* Update the layout configurations, but do not execute the layout
* @param {object} cfg The new configurations
*/
updateCfg(cfg) {},
/**
* Destroy
*/
destroy() {},
});
Custom Layout
Now, we are going to register a layout for Bigraph. Bigraph is the graph with nodes divided into two parts. There will be no edges between the nodes which are belong to the same part. In the custom layout, we sort the nodes according to their topology to reduce the edge crossings.
The data of the Bigraph is shown below, where the nodes are divided into 'part1'
and 'part2'
by the property cluster
.
const data = {
nodes: [
{ id: '0', label: 'A', cluster: 'part1' },
{ id: '1', label: 'B', cluster: 'part1' },
{ id: '2', label: 'C', cluster: 'part1' },
{ id: '3', label: 'D', cluster: 'part1' },
{ id: '4', label: 'E', cluster: 'part1' },
{ id: '5', label: 'F', cluster: 'part1' },
{ id: '6', label: 'a', cluster: 'part2' },
{ id: '7', label: 'b', cluster: 'part2' },
{ id: '8', label: 'c', cluster: 'part2' },
{ id: '9', label: 'd', cluster: 'part2' },
],
edges: [
{ source: '0', target: '6' },
{ source: '0', target: '7' },
{ source: '0', target: '9' },
{ source: '1', target: '6' },
{ source: '1', target: '9' },
{ source: '1', target: '7' },
{ source: '2', target: '8' },
{ source: '2', target: '9' },
{ source: '2', target: '6' },
{ source: '3', target: '8' },
{ source: '4', target: '6' },
{ source: '4', target: '7' },
{ source: '5', target: '9' },
],
};
Requirements Analysis
To reduce the edge crossings, we sort the nodes in part1
and part2
respectively. The process is:
- Step 1: Assign the index from 0 randomly for the nodes in
'part1'
and'part2'
respectively; -
Step 2: Traverse the nodes in
'part1'
. For each node A:- Find the set of related nodes of A (connect to A directly) in
'part2'
. Sum up the indexes of the nodes in , and divided it by the number of elements in . Replace the index of A by the result:
- Find the set of related nodes of A (connect to A directly) in
-
Step 3: Tranverse the nodes in
'part2'
. For each node A(Similar to the Step 2):- Find the set of related nodes of B (connect to B directly) in
'part1'
. Sum up the indexes of the nodes in , and divided it by the number of elements in . Replace the index of A by the result:
- Find the set of related nodes of B (connect to B directly) in
- Step 4: Sort the nodes in
part1
andpart2
respectively according to their indexed. The result order determine the postions of the nodes in the final layout.
Implementation
The following code below register a layout named 'bigraph-layout'
for Bigraph. The complete code can be found in: Cusom Layout-Bigraph. The usage of custom layout is the same as built-in layouts: configure the layout
to the graph when instantiating. Refer to: Utilizing Layout.
G6.registerLayout('bigraph-layout', {
// Default configurations
getDefaultCfg: function getDefaultCfg() {
return {
center: [0, 0], // The center of the layout
biSep: 100, // The separation of these two parts
nodeSep: 20, // The separation between nodes in the same part
direction: 'horizontal', // The direction of the two parts
nodeSize: 20, // The node size
};
},
// Execute the layout
execute: function execute() {
var self = this;
var center = self.center;
var biSep = self.biSep;
var nodeSep = self.nodeSep;
var nodeSize = self.nodeSize;
var part1Pos = 0,
part2Pos = 0;
// Layout the graph in horizontally
if (self.direction === 'horizontal') {
part1Pos = center[0] - biSep / 2;
part2Pos = center[0] + biSep / 2;
}
var nodes = self.nodes;
var edges = self.edges;
var part1Nodes = [];
var part2Nodes = [];
var part1NodeMap = new Map();
var part2NodeMap = new Map();
// Separate the nodes and init the positions
nodes.forEach(function(node, i) {
if (node.cluster === 'part1') {
part1Nodes.push(node);
part1NodeMap.set(node.id, i);
} else {
part2Nodes.push(node);
part2NodeMap.set(node.id, i);
}
});
// Sort the nodes in part1
part1Nodes.forEach(function(p1n) {
var index = 0;
var adjCount = 0;
edges.forEach(function(edge) {
var sourceId = edge.source;
var targetId = edge.target;
if (sourceId === p1n.id) {
index += part2NodeMap.get(targetId);
adjCount++;
} else if (targetId === p1n.id) {
index += part2NodeMap.get(sourceId);
adjCount++;
}
});
index /= adjCount;
p1n.index = index;
});
part1Nodes.sort(function(a, b) {
return a.index - b.index;
});
// Sort the nodes in part2
part2Nodes.forEach(function(p2n) {
var index = 0;
var adjCount = 0;
edges.forEach(function(edge) {
var sourceId = edge.source;
var targetId = edge.target;
if (sourceId === p2n.id) {
index += part1NodeMap.get(targetId);
adjCount++;
} else if (targetId === p2n.id) {
index += part1NodeMap.get(sourceId);
adjCount++;
}
});
index /= adjCount;
p2n.index = index;
});
part2Nodes.sort(function(a, b) {
return a.index - b.index;
});
// Place the ndoes
var hLength =
part1Nodes.length > part2Nodes.length
? part1Nodes.length
: part2Nodes.length;
var height = hLength * (nodeSep + nodeSize);
var begin = center[1] - height / 2;
if (self.direction === 'vertical') {
begin = center[0] - height / 2;
}
part1Nodes.forEach(function(p1n, i) {
if (self.direction === 'horizontal') {
p1n.x = part1Pos;
p1n.y = begin + i * (nodeSep + nodeSize);
} else {
p1n.x = begin + i * (nodeSep + nodeSize);
p1n.y = part1Pos;
}
});
part2Nodes.forEach(function(p2n, i) {
if (self.direction === 'horizontal') {
p2n.x = part2Pos;
p2n.y = begin + i * (nodeSep + nodeSize);
} else {
p2n.x = begin + i * (nodeSep + nodeSize);
p2n.y = part2Pos;
}
});
},
});