力导向图及在小程序上的应用

什么是力导向布局?

力导向布局是利用力导向理论的一系列算法,以美观的方式完成图形布局的方法。它的目的是将一个图的节点定位在二维或二维三维空间中,遵循所有的边尽可能都是等长的,交叉的边尽可能最小化的原则[1]。该方法会根据边和节点的相对位置在边和节点的集合中分配力,然后利用这些力模拟边和节点的运动[2]。

力导向方法的历史

力导向方法绘制图形可追溯到Tutte (1963):多面体图形可以在平面上绘制的所有面凸出通过固定的平面的外表面的嵌入图表的入顶点凸位置,在每条边上放置一个类似弹簧的吸引力,让系统达到平衡[3]。 1984年Eades提出了边表示引力,所有节点对之间为斥力的模型。在顶点对之间只使用弹簧力,理想弹簧力等于顶点间的图论距离的模型,是Kamada Kawai在1989年提出的[5]。

力导向图通常的特点

力导向图节点的大小是可变的,可用来代表不同的变量。比如,用节点大小来表示人的年龄,或者人的朋友的数量,投资公司投资行为的数量等等。而连线也可以用不同的颜色、粗细等来表示一些变量。但是,需要注意的是,在力导向图中,连线的长度、角度方向等是不能映射到变量上的(力导向图属于无向图)。这是因为这些元素会影响图的“布局”,而在力导向图中,布局是被力导向算法限制住的。力导向图相对于笼统意义上的网络图,特点就在于这种确定的布局方式。它通过模拟物理上的节点间引力和斥力,来确定点之间的位置关系,然后再连上线[6]。 ​

适用的场景:比如人物关系、计算机网络关系等。更多用来表达抽象关系。

力导向布局图的使用案例

下图是国际知识产权合作委员会(SDN)基金的网络图(Grandjean, 2015)。超过800人连接近6000个边(代表10000多个关系,边按比例增加比例同时出现的人作为同一文件的演员)。节点的大小是人的中心程度(他们维持的连接数)的函数,而颜色表明他们的中间性(社交重要性)。 ​

image.png (图1)国际知识产权合作委员会(SDN)基金的网络图

马丁·格兰德让(PAR MARTIN GRANDJEAN) 在2016年通过收集全球机场航班的连接数据,使用力导图布局绘制了如下的航空运输网络关系图。从图中可以看到全球机场航线数量,作者试图通过借助力导向图的数据可视化能力,明确航空运输背后的网络关系。

Airports-network.gif (图2)世界机场航班连接关系图

使用力导向布局图来可视化数据的优点

高质量的结果 至少对于中等数量节点的图形,通常在布局效果上具有非常好的结果:均匀的边长、均匀的顶点分布和良好的图形对称性。 ​

灵活性 力导向算法可以很容易地进行调整和扩展,以满足额外的审美标准。这使它们成为最通用的图形绘制算法类[4]。 ​

直观性 由于它们基于常见物体(如弹簧)的物理类比,因此算法的行为相对容易预测和理解。其他类型的图形绘制算法并非如此。

简单性 典型的力导向算法很简单,只需几行代码即可实现。其他类别的图形绘制算法,例如用于正交布局的算法等要容易些。 ​

互动性 力导向算法的另一个优点是交互性,通过图形的中间阶段,用户可以跟踪图形的演变,通过类似动画的效果,看到图形是如何从混乱逐步展开,演变成布局合理的形态。通过添加拖拽能力,用户可以和图形建立一种互动关系,拖拽图节点后,图形可以按照局部合理布局迁移到原位。 ​

F6在力导向布局图上的应用

F6的力导向布局算法底层引用了可视化界大名鼎鼎的D3.js的力导向算法,API使用部分保留了d3的习惯,便于F6的开发者上手。 屏幕录制2021-10-13 17.gif (图3)力导向布局图交互展示

F6快速在小程序应用中玩转力导向布局

以下示例可以在F6官网找到完整代码:f6.antv.vision/zh/docs/exa…

支付宝中使用

首先在支付宝中安装F6工具包

npm install @antv/f6 @antv/f6-alipay -S
复制代码

index.json引入f6-canvas组件

{
  "defaultTitle": "基本力导向布局及节点拖拽",
  "usingComponents": {
    "f6-canvas": "@antv/f6-alipay/es/container/container"
  }
}
复制代码

index.axml使用f6-canvas组件

<f6-canvas
  width="{{width}}"
  height="{{height}}"
  forceMini="{{forceMini}}"
  pixelRatio="{{pixelRatio}}"
  onTouchEvent="handleTouch"
  onInit="handleInit"
></f6-canvas>
复制代码

data.js数据引入 文件内容略多,只列出部分,完整内容移步github.com/antvis/F6/b… 下载

export default {
  nodes: [
    {
      id: 'Myriel',
    },
    {
      id: 'Napoleon',
    },
    {
      id: 'Mlle.Baptistine',
    },
    {
      id: 'Mme.Magloire',
    },
    {
      id: 'CountessdeLo',
    },
    {
      id: 'Geborand',
    },
    {
      id: 'Champtercier',
    },
    {
      id: 'Cravatte',
    },
    {
      id: 'Count',
    },
    {
      id: 'OldMan',
    },
    {
      id: 'Labarre',
    },
    {
      id: 'Valjean',
    },
    {
      id: 'Marguerite',
    },
    {
      id: 'Mme.deR',
    },
    {
      id: 'Isabeau',
    },
    {
      id: 'Gervais',
    },
    {
      id: 'Tholomyes',
    },
    {
      id: 'Listolier',
    },
    {
      id: 'Fameuil',
    },
    {
      id: 'Blacheville',
    },
    {
      id: 'Favourite',
    },
    {
      id: 'Dahlia',
    },
    ......
   ]
    ......
  }
复制代码

index.js编写绘制逻辑

import F6 from '@antv/f6';
import { wrapContext } from '../../../common/utils/context';
import data from './data';
import force from '@antv/f6/dist/extends/layout/forceLayout';

/**
 * 基本力导向布局及节点拖拽
 */

Page({
  canvas: null,
  ctx: null,
  renderer: '', // mini、mini-native等,F6需要,标记环境
  isCanvasInit: false, // canvas是否准备好了
  graph: null,

  data: {
    width: 375,
    height: 600,
    pixelRatio: 2,
    forceMini: false,
  },

  onLoad() {
    F6.registerLayout('force', force);
    // 同步获取window的宽高
    const { windowWidth, windowHeight, pixelRatio } = my.getSystemInfoSync();

    this.setData({
      width: windowWidth,
      height: windowHeight,
      pixelRatio,
    });
  },

  /**
   * 初始化cnavas回调,缓存获得的context
   * @param {*} ctx 绘图context
   * @param {*} rect 宽高信息
   * @param {*} canvas canvas对象,在render为mini时为null
   * @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
   */
  handleInit(ctx, rect, canvas, renderer) {
    this.isCanvasInit = true;
    this.ctx = wrapContext(ctx);
    this.renderer = renderer;
    this.canvas = canvas;
    this.updateChart();
  },

  /**
   * canvas派发的事件,转派给graph实例
   */
  handleTouch(e) {
    this.graph && this.graph.emitEvent(e);
  },

  updateChart() {
    const { width, height, pixelRatio } = this.data;

    // 创建F6实例
    this.graph = new F6.Graph({
      context: this.ctx,
      renderer: this.renderer,
      width,
      height,
      pixelRatio,
      fitView: true,
      layout: {
        type: 'force',
      },
      defaultNode: {
        size: 15,
      },
    });

    // 注册数据
    this.graph.data({
      nodes: data.nodes,
      edges: data.edges.map(function(edge, i) {
        edge.id = `edge${i}`;
        return Object.assign({}, edge);
      }),
    });

    // 更新位置用的函数
    function refreshDragedNodePosition(e) {
      const model = e.item.get('model');
      model.fx = e.x;
      model.fy = e.y;
    }

    // 监听事件
    this.graph.on('node:dragstart', function(e) {
      this.graph.layout();
      refreshDragedNodePosition(e);
    });
    this.graph.on('node:drag', function(e) {
      const forceLayout = this.graph.get('layoutController').layoutMethods[0];
      forceLayout.execute();
      refreshDragedNodePosition(e);
    });
    this.graph.on('node:dragend', function(e) {
      e.item.get('model').fx = null;
      e.item.get('model').fy = null;
    });

    this.graph.render();
    this.graph.fitView();
  },
});

复制代码

微信中使用

首先安装@antv/f6-wx

npm install @antv/f6-wx -S
复制代码

index.json 引入canvas组件

{
  "defaultTitle": "基本力导向布局及节点拖拽",
  "usingComponents": {
    "f6-canvas": "@antv/f6-wx/canvas/canvas"
  }
}
复制代码

index.wxml

<f6-canvas
  width="{{width}}"
  height="{{height}}"
  forceMini="{{forceMini}}"
  pixelRatio="{{pixelRatio}}"
  bind:onTouchEvent="handleTouch"
  bind:onInit="handleInit"
></f6-canvas>
复制代码

data.js 数据源 文件内容略多,只列出部分,完整内容移步github.com/antvis/F6/b… 下载 ​

index.js 编写绘制逻辑

import F6 from '@antv/f6-wx';

import data from './data';
import force from '@antv/f6-wx/extends/layout/forceLayout';

/**
 * 基本力导向布局及节点拖拽
 */

Page({
  canvas: null,
  ctx: null,
  renderer: '', // mini、mini-native等,F6需要,标记环境
  isCanvasInit: false, // canvas是否准备好了
  graph: null,

  data: {
    width: 375,
    height: 600,
    pixelRatio: 1,
    forceMini: false,
  },

  onLoad() {
    F6.registerLayout('force', force);
    // 同步获取window的宽高
    const { windowWidth, windowHeight, pixelRatio } = wx.getSystemInfoSync();

    this.setData({
      width: windowWidth,
      height: windowHeight,
      // pixelRatio,
    });
  },

  /**
   * 初始化cnavas回调,缓存获得的context
   * @param {*} ctx 绘图context
   * @param {*} rect 宽高信息
   * @param {*} canvas canvas对象,在render为mini时为null
   * @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
   */
  handleInit(event) {
    const {ctx, rect, canvas, renderer} = event.detail
    this.isCanvasInit = true;
    this.ctx = ctx;
    this.renderer = renderer;
    this.canvas = canvas;
    this.updateChart();
  },

  /**
   * canvas派发的事件,转派给graph实例
   */
  handleTouch(e) {
    this.graph && this.graph.emitEvent(e.detail);
  },

  updateChart() {
    const { width, height, pixelRatio } = this.data;

    // 创建F6实例
    this.graph = new F6.Graph({
      context: this.ctx,
      renderer: this.renderer,
      width,
      height,
      pixelRatio,
      fitView: true,
      layout: {
        type: 'force',
      },
      defaultNode: {
        size: 15,
      },
    });

    // 注册数据
    this.graph.data({
      nodes: data.nodes,
      edges: data.edges.map(function(edge, i) {
        edge.id = `edge${i}`;
        return Object.assign({}, edge);
      }),
    });

    // 更新位置用的函数
    function refreshDragedNodePosition(e) {
      const model = e.item.get('model');
      model.fx = e.x;
      model.fy = e.y;
    }

    // 监听事件
    this.graph.on('node:dragstart', function(e) {
      this.graph.layout();
      refreshDragedNodePosition(e);
    });
    this.graph.on('node:drag', function(e) {
      const forceLayout = this.graph.get('layoutController').layoutMethods[0];
      forceLayout.execute();
      refreshDragedNodePosition(e);
    });
    this.graph.on('node:dragend', function(e) {
      e.item.get('model').fx = null;
      e.item.get('model').fy = null;
    });

    this.graph.render();
    this.graph.fitView();
  },
});
复制代码

欢迎讨论

如果你对于力导向图的还有疑问,或者对图可视化感兴趣,可以添加我的微信 openwayne 进入我们的微信群讨论。

参考文献:

  1. [^](zh.wikipedia.org/wiki/%E5%8A…) Chernobelskiy, R.; Cunningham, K.; Goodrich, M. T.; Kobourov, S. G.; Trott, L., Force-directed Lombardi-style graph drawing, Proc. 19th Symposium on Graph Drawing (PDF): 78–90, 2011.
  2. [^](zh.wikipedia.org/wiki/%E5%8A…) Bannister, M. J.; Eppstein, D.; Goodrich, M. T.; Trott, L., Force-directed graph drawing using social gravity and scaling, Proc. 20th Int. Symp. Graph Drawing, 2012, Bibcode:2012arXiv1209.0748B, arXiv:1209.0748.
  3. Tutte, W. T., How to draw a graph, Proceedings of the London Mathematical Society, 1963, 13 (52): 743–768, doi:10.1112/plms/s3-13.1.743.
  4. Vose, Aaron. 3D Phylogenetic Tree Viewer. [3 June 2012].
  5. Kamada, Tomihisa; Kawai, Satoru, An algorithm for drawing general undirected graphs, Information Processing Letters (Elsevier), 1989, 31 (1): 7–15, doi:10.1016/0020-0190(89)90102-6.
  6. tuzhidian.com/chart?id=5c…

猜你喜欢

转载自juejin.im/post/7018757358423638023