背景 / 需求

现有所有图(除树外)的 Layout 都作为插件接入 G6。插件是一种可扩展、可插拔、完全独立于 G6 的一部分。然而,G6 的定位是图可视化引擎,而不是图渲染引擎。图布局作为图可视化的基础,应当是 G6 原生支持的、不可或缺当一部分。

目标及边界

将大多数(业务中、学术上)常用、好用的 Layout 算法内置到 G6 当中。其中工作包括:
1. 现已在插件中的 Layout 的迁移;
2. 其他常用 Layout 的内置实现。

  • Tree Layout
    • Dagre
    • Radial
    • Dendrogram
  • General Graph Layout

    • Force
    • MDS
    • Fruchterman
    • Circular
    • Radial

      形态

  • 一系列 Layout 在 G6 中与 Renderer 同等级;

  • 支持 Layout 的注册、复写、扩展;
  • 统一所有图(包括树)的布局及其流程规范;
  • 统一数据流转中的生命周期、时机。

    用户

  • 前端开发者

  • Researcher

用户操作

  • 创建布局
    • 方法 1 :在 new Graph 时以配置方式创建
    • 方法 2 :单独创建 Layout,独立于 G6
  • 更新数据
    • 不主动更新布局,调用才更新
  • 更新布局配置
    • 更新后主动更新布局
  • 更换布局方法
    • 更换后主动更新布局

数据流转

  • 流入
    • 使用在 new Graph 时以配置方式创建(方法1):根据 graph 上的数据进行布局
    • 单独创建(方法2):单独调用时,根据传入参数 data 进行布局
  • 返回:布局计算后不直接修改原数据模型,而是返回 positions 位置数组,交由 controller 决定更新方式
  • 更新数据:传参

具体设计

类设计

G6 Layout 重构方案 - 图1

接口设计

layout.js 布局基类

  1. Layout.registerLayout = function(type, layout) {
  2. if (!layout) {
  3. throw new Error('please specify handler for this layout:' + type);
  4. }
  5. const base = function(cfg) {
  6. const self = this;
  7. Util.mix(self, self.getDefaultCfg(), cfg);
  8. };
  9. Util.augment(base, {
  10. /**
  11. * 初始化
  12. */
  13. init() {
  14. }
  15. /**
  16. * 执行布局,不改变原数据模型位置,只返回布局后但结果位置
  17. */
  18. excute() {
  19. return positions;
  20. }
  21. /**
  22. * 更新布局配置,但不执行布局
  23. */
  24. updateLayoutCfg(cfg) {
  25. }
  26. /**
  27. * 更换数据
  28. */
  29. changeData(data) {
  30. self.set('data', data);
  31. self.excute();
  32. }
  33. /**
  34. * 销毁
  35. */
  36. destroy() {
  37. }
  38. /**
  39. * 定义自定义行为的默认参数,会与用户传入的参数进行合并
  40. */
  41. getDefaultCfg() {}
  42. }, layout);
  43. Layout[type] = base;
  44. };

layout controller: layout.js

  1. class Layout {
  2. constructor(graph) {
  3. this.graph = graph;
  4. const layout = graph.get('layout');
  5. if (layout === undefined) {
  6. if (graph.data[0].x === undefined) {
  7. // 创建随机布局
  8. const randomLayout = new Random();
  9. this.set('layout', randomLayout);
  10. } else { // 若未指定布局且数据中没有位置信息,则不进行布局,直接按照原数据坐标绘制。
  11. return;
  12. }
  13. }
  14. layout = _getLayout();
  15. this._initLayout();
  16. }
  17. _initLayout() {
  18. const layout = this.layout;
  19. const graph = this.graph;
  20. layout.init();
  21. }
  22. // 执行并绘制
  23. refreshLayout() {
  24. ...
  25. }
  26. // 更新布局参数
  27. updateLayoutCfg(cfg) {
  28. ...
  29. }
  30. // 更换布局
  31. changeLayout(layout) {
  32. ...
  33. }
  34. // 控制布局动画
  35. layoutAnimate() {
  36. ...
  37. }
  38. // 根据 type 创建 Layout 实例
  39. _getLayout() {
  40. ...
  41. }
  42. destroy() {
  43. const layout = this.layout;
  44. layout.destroy();
  45. }
  46. }

Layout 流程 生命周期

  • 创建

    • 方法 1(暴露给用户): 在 new Graph 时配置创建:

      • new Graph 时若指定了内置 layout,则在 graph render 时调用 layout controller 的 refreshLayout();
      • 若未指定布局,则不进行布局,直接按照原数据坐标绘制。
        1. const graph = new G6.Graph({
        2. container: 'mountNode',
        3. width: 1000,
        4. height: 800,
        5. plugins: [ minimap ],
        6. layout: {
        7. type: 'force',
        8. center: [500, 400],
        9. nodeStrength: 1,
        10. edgeStrength: 1,
        11. preventOverlap: true,
        12. nodeRadius: 10
        13. }
        14. });
    • 方法 2 (不暴露,保留):单独使用

      1. const layout = new Force({
      2. center: [500, 400],
      3. nodeStrength: 1,
      4. edgeStrength: 1,
      5. preventOverlap: true,
      6. nodeRadius: 10,
      7. data
      8. );
      9. forcelayout.excute();
  • update / refresh: graph.refreshLayout()

    • 在外部更改节点位置(drag、换数据等)时,不主动重新布局,用户调用 graph.refreshLayout() 才会更新。
  • destroy:用户主动调用销毁或graph销毁时 更换布局

扩展 / 自定义布局

  1. G6.registerLayout("custom-layout", {
  2. excute() {
  3. // ...
  4. return positions;
  5. }
  6. });

时机

更新方法

  • (用方法 1 创建)graph.refreshLayout()
  • (用方法 2 创建)layout.excute()

    更新布局时机

  • 全量更新数据 —— 不主动

  • 一般图的增删元素(addItem / remove)—— 不主动
  • 树的展开合并 (在 behavior 中调用graph.refreshLayout()) —— 主动
  • 更新配置 (graph.updateLayoutCfg) —— 主动
  • 更换布局方法(graph.changeLayout()) —— 主动

    动画

  • 若用户配置了 animate: true,则在每次更新布局的时候动画变化(非力导的布局方法使用插值方式)。

  • 否则,直接习惯按布局结果。