如何让 G6 节点支持统计图表 | 🏆 技术专题第三期征文

1,291 阅读6分钟

如果不想了解具体细节,只要看最佳的使用方式,请直接拉到文章的最后部分。

AntV G6 是一个简单、易用、完备的图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。

效果

在我们的业务中,使用 AntV G6 来开发关系图的需求,但是在有些复杂的业务需求中,可能会希望在节点中展示数据的分布情况,这就要求 G6 的节点能够支持基本的统计图表。
image.png

问题

为了提升性能,我们使用 G6 的 canvas 渲染方式,但使用 canvas 渲染时节点不支持 DOM。

G6 的性能目前已经得到了大幅度的提升,可以参考他们官网的 Demo,在 20000 数据量的情况下都可以顺畅交互:g6.antv.vision/zh/examples…

思考

G6 是底层渲染是基于 G 来实现的,而 AntV G2 也是基于 G 渲染的,那我们是不是可以把 G2 的图表嵌入到 G6 节点中呢?

顺着这个思路,我们发现 G2 中 view、shape、geometry 等支持将 G 的 group 作为容器,这个时候就可以复用 G6 中group。

实现

Step 1:引入必要的 Geometry 及 Component

在 G2 中,实现柱状图的是 Geometry 是 Interval,实现折线图的 Geometry 是 Line,另外折线上面还有圆点,使用的是 Geometry Point,因此,我们需要引入 Interval、Line 及 Point,另外还需要 x 轴来显示各个类别的名称,还需要引入 Axis 组件。

import Line from '@antv/g2/lib/geometry/line';
import Point from '@antv/g2/lib/geometry/point';
import Axis from "@antv/g2/lib/chart/controller/axis";
import Interval from "@antv/g2/lib/geometry/interval";

Step 2:注册 Geometry 及 Component

由于我们不是直接使用 G2 的 Chart,而是基于 View 来实现柱状图,因此需要先注册对应的 Geometry 及 Component,如下所示:

import { registerGeometry } from '@antv/g2/lib/chart/view';
import { registerComponentController } from '@antv/g2/lib/core';

registerGeometry("Line", Line);
registerGeometry("Point", Point);
registerGeometry("Interval", Interval);
registerComponentController("axis", Axis);

Step 3:实例化 View

由于我们是在自定义 G6 节点的时候实例化 View,所以,我们可以将 G6 中的 group 作为 View 的容器使用。

const canvas = group.get('canvas')
const backgroundGroup = group.addGroup();
const middleGroup = group.addGroup();
const foregroundGroup = group.addGroup();

const view = new View({
  parent: null,
  canvas,
  foregroundGroup,
  middleGroup,
  backgroundGroup,
  padding: 5,
  visible: true,
  region: {
    start: {
      x: 0.01,
      y: 0.2
    },
    end: {
      x: 0.8,
      y: 0.35
    }
  },
});

view.data(cfg.trendData);

view
  .point()
  .position('月份*月均降雨量')
  .color('name');

view
  .line()
  .position("月份*月均降雨量")
  .color("name")
  .shape('dash');

view
  .interval()
  .position("月份*月均降雨量")
  .color("name")
  .adjust([
    {
      type: 'dodge',
      marginRatio: 0,
    },
  ]);

view.legend(false);

view.axis('月均降雨量', false)

view.render();

在这一步有以下几点需要特别注意:

  • region 用于控制柱状图的显示范围,x 和 y 的取值都在 [0-1] 范围内;
  • region 中 x 和 y 的取值根据 canvas 宽度和高度的来确定,而不是节点 keyShape 的宽高。

Step 4:自定义节点

在完成了前三步以后,自定义带有柱状图的节点就相对比较简单了,按照 G6 中的要求来实现即可。

G6.registerNode('node-with-multchart', {
  draw(cfg, group) {
    const canvas = group.get('canvas')
    const keyShape = group.addShape('rect', {
      attrs: {
        x: 0,
        y: 0,
        width: 400,
        height: 200,
        fill: cfg.style.fill
      }
    })

    group.addShape('rect', {
      attrs: {
        x: 0,
        y: 0,
        width: 400,
        height: 40,
        fill: '#69c0ff'
      }
    })

    group.addShape('text', {
      attrs: {
        text: '浏览申请完成率',
        x: 10, 
        y:25,
        fontSize: 14,
        fill: '#fff' 
      }
    })

    group.addShape('text', {
      attrs: {
        text: '2020-06-07 ~ 2020-06-14 | 均值',
        x: 20,
        y: 70,
        fontSize: 13,
        fill: '#8c8c8c'
      }
    })

    group.addShape('text', {
      attrs: {
        text: '8.8%',
        x: 20,
        y: 110,
        fontSize: 30,
        fill: '#000'
      }
    })

    const backgroundGroup = group.addGroup();
    const middleGroup = group.addGroup();
    const foregroundGroup = group.addGroup();

    const view = new View({
      parent: null,
      canvas,
      foregroundGroup,
      middleGroup,
      backgroundGroup,
      padding: 5,
      visible: true,
      region: {
        start: {
          x: 0.01,
          y: 0.2
        },
        end: {
          x: 0.8,
          y: 0.35
        }
      },
    });

    view.data(cfg.trendData);
    
    view
      .point()
      .position('月份*月均降雨量')
      .color('name');

    view
      .line()
      .position("月份*月均降雨量")
      .color("name")
      .shape('dash');

    view
      .interval()
      .position("月份*月均降雨量")
      .color("name")
      .adjust([
        {
          type: 'dodge',
          marginRatio: 0,
        },
      ]);
    view.legend(false);

    view.axis('月均降雨量', false)

    view.render();

    return keyShape
  },
  update: null
}, 'single-node')

在这一步需要注意以下几点:

  • 将柱状图作为节点的一部分,而不要作为 keyShape 来使用;
  • 建议继承 G6 内置的 single-node,这样就可以使用 G6 中的状态管理的能力;
  • 关于节点的更新,有两种方式来做:
    • 将 update 方法设置为 null 或 undefined,这样每次都会执行 draw 方法重绘;
    • 实现 update 方法,在 update 方法中调用 view 的changeData 来更新数据,这样就需要在 draw 时将 view 对象存储下来,具体的实现如下所示:
G6.registerNode('node-with-interval', {
  draw(cfg, group) {
    const canvas = group.get('canvas')
    const keyShape = group.addShape('rect', {
      attrs: {
        x: 0,
        y: 0,
        width: 400,
        height: 200,
        fill: cfg.style.fill
      }
    })

    const view = new View({
      // ...
    });
    
    view.render();

    // 存储 view 对象,在 update 中使用
    keyShape.set('intervalView', view)

    return keyShape
  },
  update(cfg, item) {
    const keyShape = item.getKeyShape()
    const view = keyShape.get('intervalView')
    view.changeData(cfg.trendData)
    
    // keyShape 及其他部分的更新和 G6 普通节点更新方式完全一样
  }
}, 'single-node')

使用

使用 G6 自定义节点方式定义了名称为 node-with-interval 的节点以后,使用的时候和 circle、rect 等普通节点的方式完全相同,可以在实例化 Graph 时通过 defaultNode 的 type 字段指定,也可以在数据中通过 type 字段指定。

const graph = new G6.Graph({
  // 省略其他配置......
  defaultNode: {
    type: 'node-with-interval'
  }
});

使用上面的方式固然可以让 G6 的节点支持 G2 的图表,但实现起来过于复杂,需要我们对 G2 的源码有一定的了解,对于业务开发同学来说特别不友好。那有没有更简单的方法呢?

我们发现 G6 在 3.7.0 版本发布 以后,官网上面多了几个自定义节点的案例,点进去一看,震惊!竟然是节点中支持折线图、柱状图、饼图等的案例,这不正是我们上面介绍的吗?

最佳实现

为了让业务开发者更方便地在节点中支持统计图表,AntV G6 官方提供了 @antv/chart-node-g6 包,使用这个包,我们就再也不用去关心 G2 中的细节了,具体的使用方式清晰明了。

import G6 from '@antv/g6';
import Chart from '@antv/chart-node-g6';


G6.registerNode(
  'node-with-interval',
  {
    draw(cfg, group) {
      const keyShape = group.addShape('rect', {
        attrs: {
          x: 0,
          y: 0,
          width: 400,
          height: 200,
          fill: cfg.style.fill,
        },
      });

      const view = new Chart({
        group,
        padding: 1,
        width: 360,
        height: 70,
        x: 20,
        y: 100
      });

      view.data(cfg.trendData);

      view.interval().position('genre*sold').color('genre');

      view.legend('genre', false);

      view.scale({
        genre: {
          alias: '游戏种类', // 列定义,定义该属性显示的别名
        },
        sold: {
          alias: '销售量',
        },
      });

      view.axis('sold', false);

      // 极坐标下的柱状图
      // view.coordinate('polar');

      view.render();

      keyShape.set('intervalView', view);

      return keyShape;
    },
    update(cfg, item) {
      const keyShape = item.getKeyShape();
      const view = keyShape.get('intervalView');
      view.changeData(cfg.trendData);
    },
  },
  'single-node',
);

具体效果就下图所示:

更多的案例请参考 AntV G6 官网

AntV G6 是一款开源的图可视化引擎,专注于图可视化及图分析领域。
欢迎关注和 star G6 GitHubgithub.com/antvis/G6
官网:g6.antv.vision/zh/

🏆 技术专题第三期 | 数据可视化的那些事......