【实现自己的可视化引擎15】关系图

1,244 阅读3分钟

前言

关系图又叫结构拓扑图,由各个网络节点相互连接形成的网络图谱。其效果图如下:

根据效果图,我们确定我们关系图的数据结构,其代码如下:

{
  roles: {
    USD: { label: '美元', color: '#C8B988' },
    EUR: { label: '欧元', color: '#D8A3B1'},
    JPY: { label: '日元', color: '#90D8DD' },
    AUD: { label: '澳元', color: '#56B643' },
  },
  relation: [
    ['USD', 'EUR', 0.91],
    ['USD', 'JPY', 108.1],
    ['USD', 'AUD', 1.3],
    ['EUR', 'JPY', 109.2],
    ['EUR', 'AUD', 1.4],
  ]
}

数据由两部分组成,roles定义各个节点的描述,relation为各个节点间的关联关系值。

拓扑图层

从效果图上可以看出,拓扑图层由Text、Circle、Line这些组件依据数据关系组合而成。当我们新建拓扑图层时或设置图层数据时,我们的节点描述就已经确定,即效果图上的Circle已经确定,所以我们可以事先将角色图元保存下来,代码如下:

/**
   * 设置data的时候重置角色单位
   * @param data
   */
  set data(data) {
    this.clearEventListener();
    this.roles = data.roles;
    this.relation = data.relation;
    this.roleUnits = {};
    // 构建各个角色
    for(let key in this.roles) {
      const role = this.roles[key];
      const randX = Math.random() * this.width;
      const randY = Math.random() * this.height;
      const txt = new Text(this.canvas, {
        text: role.label,
        color: this.color,
        font: this.fontFamily,
        size: this.fontSize,
      });
      txt.position = new Point(randX - txt.width / 2, randY - txt.height / 2);
      let circle = new Circle(this.canvas, {
        radius: Math.max(txt.width, txt.height) / 2 + 10,
        color: role.color || '#999999',
        position: new Point(randX, randY),
        type: Circle.TYPE.FILL,
      });

      // 构建角色图层,便于管理
      const layer = new Layer(this.canvas, { position: new Point(randX, randY) });
      layer.addEventListener(Event.EVENT_DRAG, (e) => {
        // 重新计算节点位置
        e.node.position = new Point(e.node.position.x + e.distanceX, e.node.position.y + e.distanceY);
        for (let i = 0; i < e.node.childs.length; i++) {
          if (e.node.childs[i] instanceof Text) {
            e.node.childs[i].position = new Point(
              e.node.position.x + e.distanceX - e.node.childs[i].width / 2,
              e.node.position.y + e.distanceY - e.node.childs[i].height / 2
            );
          } else {
            e.node.childs[i].position = e.node.position;
          }

        }
        // 重新绘制画布
        if (e.node.canvas) {
          this.make();
          e.node.canvas.paint();
        }
      });
      layer.addChild(circle, txt);
      this.roleUnits[key] = layer;
    }
}

上面的代码,我们各节点图层添加了拖动动画监听,当我们拖动节点时,以便整个图表的重新绘制。 有了关系节点,我们便可以遍历关系数组relation,绘制两个节点之间的连线,由于两个节点为圆心,所以我们需要计算连线在两个圆之上的坐标。代码如下:

make() {
    this.childs.splice(0, this.childs.length);
    if (this.relation.length < 0) {
      return;
    }
    // 遍历关系数组,连接各个角色
    for (let i = 0; i < this.relation.length; i++) {
      const relation = this.relation[i];
      const role1 = this.roleUnits[relation[0]];
      const role2 = this.roleUnits[relation[1]];
      // 计算两个角色坐标的斜率
      const slope = (role1.position.y - role2.position.y) / (role1.position.x - role2.position.x);
      // 计算与水平线的夹角
      const angle = Math.atan(slope);
      // 计算角色1连线的节点1
      const point1 = new Point(
        role1.position.x + role1.childs[0].radius * Math.cos(angle),
        role1.position.y + role1.childs[0].radius * Math.sin(angle)
      );
      // 计算角色2连线的节点2
      const point2 = new Point(
        role2.position.x - role2.childs[0].radius * Math.cos(angle),
        role2.position.y - role2.childs[0].radius * Math.sin(angle)
      );
      // 绘制连接线段
      const line = new Line(this.canvas, {
        position: point1,
        to: point2,
        lineWidth: this.lineWidth || 2,
        color: this.color
      });
      // 关联数值
      const txt = new Text(this.canvas, {
        font: this.fontFamily,
        size: this.fontSize,
        color: this.fontColor,
        text: relation[2],
        position: new Point((role1.position.x + role2.position.x) / 2, (role1.position.y + role2.position.y) / 2),
      });
      // 旋转角度,使其与直线平行
      txt.rotation = -angle / Math.PI * 180;
      this.addChild(line, txt);
    }
    // 将角色加入图层
    for (let key in this.roleUnits) {
      this.addChild(this.roleUnits[key]);
    }
  }

React 封装

React封装需要DOM的挂载完成,所以我们在生命周期componentDidMount函数中构建我们的图层。

 componentDidMount () {
    const { style = {}, data } = this.props;
    this.canvas = new Canvas({
      ele: this.ref.current,
      canAction: false,
    });
    this.relationLayer = new RelationLayer(this.canvas, style, data);
    this.canvas.addChild(this.relationLayer);
    this.relationLayer.make();
    this.canvas.paint();
  }

组件使用案例

render() {
    return (
      <Relation
        className="chart"
        data={{
          roles: {
            USD: { label: '美元', color: '#C8B988' },
            EUR: { label: '欧元', color: '#D8A3B1'},
            JPY: { label: '日元', color: '#90D8DD' },
            AUD: { label: '澳元', color: '#56B643' },
          },
          relation: [
            ['USD', 'EUR', 0.91],
            ['USD', 'JPY', 108.1],
            ['USD', 'AUD', 1.3],
            ['EUR', 'JPY', 109.2],
            ['EUR', 'AUD', 1.4],
          ]
        }}
        style={{
          color: '#333333',
          fontColor: '#333333',
          lineWidth: 1,
        }}
      />)
  }

目录

【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图