--- title: 自定义布局 Layout order: 6 --- G6 提供了一般图和树图的一些常用布局,使用方式参见:中级教程  [一般图布局 Layout](/zh/docs/manual/middle/layout/graph-layout),[树图布局 Layout](/zh/docs/manual/middle/layout/tree-graph-layout),[图布局 API](/zh/docs/api/graphLayout/guide) 或 [树图布局 API](/zh/docs/api/treeGraphLayout/guide)。当这些内置布局无法满足需求时,G6 还提供了一般图的自定义布局的机制,方便用户进行更定制化的扩展。   ⚠️ 注意: 树图暂时不支持自定义布局。 本文将会通过自定义 Bigraph 布局的例子讲解自定义布局。 ## 自定义布局 API G6 中自定义布局的 API 如下: ```javascript /** * 注册布局的方法 * @param {string} type 布局类型,外部引用指定必须,不要与已有布局类型重名 * @param {object} layout 布局方法 */ Layout.registerLayout = function(type, { /** * 定义自定义行为的默认参数,会与用户传入的参数进行合并 */ getDefaultCfg() { return {}; }, /** * 初始化 * @param {object} data 数据 */ init(data) {}, /** * 执行布局 */ execute() {}, /** * 根据传入的数据进行布局 * @param {object} data 数据 */ layout(data) {}, /** * 更新布局配置,但不执行布局 * @param {object} cfg 需要更新的配置项 */ updateCfg(cfg) {}, /** * 销毁 */ destroy() {}, }); ``` ## 自定义布局 下面,我们将讲解如何自定义布局如下图的二分图 Bigraph。二分图只存在两部分节点之间的边,同属于一个部分的节点之间没有边。我们希望布局能够对两部分节点分别进行排序,减少边的交叉。
img 该二分图数据如下,节点根据 `cluster` 字段分为 了 `'part1'` 和 `'part2'`,代表二分图的两部分。 ```javascript 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' }, ], }; ``` ### 需求分析 为了减少边的交叉,可以通过排序,将 `'part1'`  的节点 A 对齐到所有与 A 相连的 `'part2'` 中的节点的平均中心;同样将 `'part2'` 中的节点 a 对齐到所有与 a 相连的 `'part1'` 中的节点的平均中心。可以描述成如下过程: - Step 1:为  `'part1'`  和  `'part2'`  的节点初始化随机序号 index,都分别从 0 开始; - Step 2:遍历  `'part1'` 的节点,对每一个节点 A: - 找到与 A 相连的属于  `'part2'`  的节点的集合 ![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-WOhQIGg9l8AAAAAAAAAAABkARQnAQ),加和  ![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-WOhQIGg9l8AAAAAAAAAAABkARQnAQ) 中所有节点的 index,并除以 ![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-WOhQIGg9l8AAAAAAAAAAABkARQnAQ) 的元素个数,得数覆盖 A 的 index 值:![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*VfXiSK1f02cAAAAAAAAAAABkARQnAQ) - Step 3:遍历  `'part2'` 的节点,对每一个节点 B(与  Step 2 相似): - 找到与 B 相连的属于  `'part1'`  的节点的集合 ![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*GqZiSKI-nB8AAAAAAAAAAABkARQnAQ),加和  ![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*GqZiSKI-nB8AAAAAAAAAAABkARQnAQ)  中所有节点的 index,并除以 ![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*GqZiSKI-nB8AAAAAAAAAAABkARQnAQ)  的元素个数,得数覆盖 B 的 index 值:![](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-8b2Spfb4HIAAAAAAAAAAABkARQnAQ) - Step 4:两部分节点分别按照节点的序号 index 进行排序,最终按照节点顺序安排节点位置。 ## 实现 下面代码展示了自定义名为  `'bigraph-layout'` 的二分图布局,完整代码参见:自定义布局-二分图。使用该布局的方式与使用内置布局方式相同,都是在实例化图时将其配置到 `layout` 配置项中,详见:[一般图布局](/zh/docs/manual/middle/layout/graph-layout)。 ```javascript G6.registerLayout('bigraph-layout', { // 默认参数 getDefaultCfg: function getDefaultCfg() { return { center: [0, 0], // 布局的中心 biSep: 100, // 两部分的间距 nodeSep: 20, // 同一部分的节点间距 direction: 'horizontal', // 两部分的分布方向 nodeSize: 20, // 节点大小 }; }, // 执行布局 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; // 若指定为横向分布 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); } }); // 对 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; }); // 对 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; }); // 放置节点 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; } }); }, }); ```