自定义布局 Layout

G6 提供了一般图和树图的一些常用布局,使用方式参见:中级教程 使用布局 LayoutLayout API。当这些内置布局无法满足需求时,G6 还提供了一般图的自定义布局的机制,方便用户进行更定制化的扩展。

注意:树图暂时不支持自定义布局。

本文将会通过自定义 Bigraph 布局的例子讲解自定义布局。

自定义布局 API

G6 中自定义布局的 API 如下:

  1. /**
  2. * 注册布局的方法
  3. * @param {string} type 布局类型,外部引用指定必须,不要与已有布局类型重名
  4. * @param {object} layout 布局方法
  5. */
  6. Layout.registerLayout = function(type, {
  7. /**
  8. * 定义自定义行为的默认参数,会与用户传入的参数进行合并
  9. */
  10. getDefaultCfg() {
  11. return {};
  12. },
  13. /**
  14. * 初始化
  15. * @param {object} data 数据
  16. */
  17. init(data) {},
  18. /**
  19. * 执行布局
  20. */
  21. execute() {},
  22. /**
  23. * 根据传入的数据进行布局
  24. * @param {object} data 数据
  25. */
  26. layout(data) {},
  27. /**
  28. * 更新布局配置,但不执行布局
  29. * @param {object} cfg 需要更新的配置项
  30. */
  31. updateCfg(cfg) {},
  32. /**
  33. * 销毁
  34. */
  35. destroy() {},
  36. });

自定义布局

下面,我们将讲解如何自定义布局如下图的二分图 Bigraph。二分图只存在两部分节点之间的边,同属于一个部分的节点之间没有边。我们希望布局能够对两部分节点分别进行排序,减少边的交叉。 img 该二分图数据如下,节点根据 cluster 字段分为 了 'part1''part2',代表二分图的两部分。

  1. const data = {
  2. nodes: [
  3. { id: '0', label: 'A', cluster: 'part1' },
  4. { id: '1', label: 'B', cluster: 'part1' },
  5. { id: '2', label: 'C', cluster: 'part1' },
  6. { id: '3', label: 'D', cluster: 'part1' },
  7. { id: '4', label: 'E', cluster: 'part1' },
  8. { id: '5', label: 'F', cluster: 'part1' },
  9. { id: '6', label: 'a', cluster: 'part2' },
  10. { id: '7', label: 'b', cluster: 'part2' },
  11. { id: '8', label: 'c', cluster: 'part2' },
  12. { id: '9', label: 'd', cluster: 'part2' },
  13. ],
  14. edges: [
  15. { source: '0', target: '6' },
  16. { source: '0', target: '7' },
  17. { source: '0', target: '9' },
  18. { source: '1', target: '6' },
  19. { source: '1', target: '9' },
  20. { source: '1', target: '7' },
  21. { source: '2', target: '8' },
  22. { source: '2', target: '9' },
  23. { source: '2', target: '6' },
  24. { source: '3', target: '8' },
  25. { source: '4', target: '6' },
  26. { source: '4', target: '7' },
  27. { source: '5', target: '9' },
  28. ],
  29. };

需求分析

为了减少边的交叉,可以通过排序,将 'part1' 的节点 A 对齐到所有与 A 相连的 'part2' 中的节点的平均中心;同样将 'part2' 中的节点 a 对齐到所有与 a 相连的 'part1' 中的节点的平均中心。可以描述成如下过程:

  • Step 1:为 'part1''part2' 的节点初始化随机序号 index,都分别从 0 开始;
  • Step 2:遍历 'part1' 的节点,对每一个节点 A:

    • 找到与 A 相连的属于 'part2' 的节点的集合 自定义布局 Layout - 图2,加和 自定义布局 Layout - 图3 中所有节点的 index,并除以 自定义布局 Layout - 图4 的元素个数,得数覆盖 A 的 index 值:自定义布局 Layout - 图5
  • Step 3:遍历 'part1' 的节点,对每一个节点 B(与 Step 2 相似):

    • 找到与 B 相连的属于 'part2' 的节点的集合 自定义布局 Layout - 图6,加和 自定义布局 Layout - 图7 中所有节点的 index,并除以 自定义布局 Layout - 图8 的元素个数,得数覆盖 B 的 index 值:自定义布局 Layout - 图9
  • Step 4:两部分节点分别按照节点的序号 index 进行排序,最终按照节点顺序安排节点位置。

实现

下面代码展示了自定义名为 'bigraph-layout' 的二分图布局,完整代码参见:自定义布局-二分图。使用该布局的方式与使用内置布局方式相同,都是在实例化图时将其配置到 layout 配置项中,详见:使用布局 Layout

  1. G6.registerLayout('bigraph-layout', {
  2. // 默认参数
  3. getDefaultCfg: function getDefaultCfg() {
  4. return {
  5. center: [0, 0], // 布局的中心
  6. biSep: 100, // 两部分的间距
  7. nodeSep: 20, // 同一部分的节点艰巨
  8. direction: 'horizontal', // 两部分的分布方向
  9. nodeSize: 20, // 节点大小
  10. };
  11. },
  12. // 执行布局
  13. execute: function execute() {
  14. var self = this;
  15. var center = self.center;
  16. var biSep = self.biSep;
  17. var nodeSep = self.nodeSep;
  18. var nodeSize = self.nodeSize;
  19. var part1Pos = 0,
  20. part2Pos = 0;
  21. // 若指定为横向分布
  22. if (self.direction === 'horizontal') {
  23. part1Pos = center[0] - biSep / 2;
  24. part2Pos = center[0] + biSep / 2;
  25. }
  26. var nodes = self.nodes;
  27. var edges = self.edges;
  28. var part1Nodes = [];
  29. var part2Nodes = [];
  30. var part1NodeMap = new Map();
  31. var part2NodeMap = new Map();
  32. // separate the nodes and init the positions
  33. nodes.forEach(function(node, i) {
  34. if (node.cluster === 'part1') {
  35. part1Nodes.push(node);
  36. part1NodeMap.set(node.id, i);
  37. } else {
  38. part2Nodes.push(node);
  39. part2NodeMap.set(node.id, i);
  40. }
  41. });
  42. // 对 part1 的节点进行排序
  43. part1Nodes.forEach(function(p1n) {
  44. var index = 0;
  45. var adjCount = 0;
  46. edges.forEach(function(edge) {
  47. var sourceId = edge.source;
  48. var targetId = edge.target;
  49. if (sourceId === p1n.id) {
  50. index += part2NodeMap.get(targetId);
  51. adjCount++;
  52. } else if (targetId === p1n.id) {
  53. index += part2NodeMap.get(sourceId);
  54. adjCount++;
  55. }
  56. });
  57. index /= adjCount;
  58. p1n.index = index;
  59. });
  60. part1Nodes.sort(function(a, b) {
  61. return a.index - b.index;
  62. });
  63. // 对 part2 的节点进行排序
  64. part2Nodes.forEach(function(p2n) {
  65. var index = 0;
  66. var adjCount = 0;
  67. edges.forEach(function(edge) {
  68. var sourceId = edge.source;
  69. var targetId = edge.target;
  70. if (sourceId === p2n.id) {
  71. index += part1NodeMap.get(targetId);
  72. adjCount++;
  73. } else if (targetId === p2n.id) {
  74. index += part1NodeMap.get(sourceId);
  75. adjCount++;
  76. }
  77. });
  78. index /= adjCount;
  79. p2n.index = index;
  80. });
  81. part2Nodes.sort(function(a, b) {
  82. return a.index - b.index;
  83. });
  84. // 放置节点
  85. var hLength =
  86. part1Nodes.length > part2Nodes.length
  87. ? part1Nodes.length
  88. : part2Nodes.length;
  89. var height = hLength * (nodeSep + nodeSize);
  90. var begin = center[1] - height / 2;
  91. if (self.direction === 'vertical') {
  92. begin = center[0] - height / 2;
  93. }
  94. part1Nodes.forEach(function(p1n, i) {
  95. if (self.direction === 'horizontal') {
  96. p1n.x = part1Pos;
  97. p1n.y = begin + i * (nodeSep + nodeSize);
  98. } else {
  99. p1n.x = begin + i * (nodeSep + nodeSize);
  100. p1n.y = part1Pos;
  101. }
  102. });
  103. part2Nodes.forEach(function(p2n, i) {
  104. if (self.direction === 'horizontal') {
  105. p2n.x = part2Pos;
  106. p2n.y = begin + i * (nodeSep + nodeSize);
  107. } else {
  108. p2n.x = begin + i * (nodeSep + nodeSize);
  109. p2n.y = part2Pos;
  110. }
  111. });
  112. },
  113. });