myDocs
首页
  • JavaScript小记
  • HTML小记
  • CSS小记
  • 计算机网络
  • React小记
  • Vue小记
  • 手写js
  • 前端工程化
  • 前端性能优化
  • 实际项目开发
  • Typescript面试题
  • Nodejs面试题
  • 小程序
  • 排序
  • 算法题
  • Git小记
  • NodeJs小记
  • TypeScript小记
  • 正则表达式入门
  • Linux基本命令
  • PixiJS的基本使用
  • PixiJS实现一镜到底
  • Canvas入门
  • SVG入门
  • Echarts基本使用
  • antv G6的基础入门及树图的实际应用
  • Three.js
  • 《CSS揭秘》
  • 《Python编程:从入门到实践》
  • 低代码数据可视化平台开发记录
  • 中后台管理系统模板记录
  • 多页签开发记录
  • 浙政钉、浙里办、浙江政务服务网应用上架指南
Github
首页
  • JavaScript小记
  • HTML小记
  • CSS小记
  • 计算机网络
  • React小记
  • Vue小记
  • 手写js
  • 前端工程化
  • 前端性能优化
  • 实际项目开发
  • Typescript面试题
  • Nodejs面试题
  • 小程序
  • 排序
  • 算法题
  • Git小记
  • NodeJs小记
  • TypeScript小记
  • 正则表达式入门
  • Linux基本命令
  • PixiJS的基本使用
  • PixiJS实现一镜到底
  • Canvas入门
  • SVG入门
  • Echarts基本使用
  • antv G6的基础入门及树图的实际应用
  • Three.js
  • 《CSS揭秘》
  • 《Python编程:从入门到实践》
  • 低代码数据可视化平台开发记录
  • 中后台管理系统模板记录
  • 多页签开发记录
  • 浙政钉、浙里办、浙江政务服务网应用上架指南
Github
  • antv G6的基础入门及树图的实际应用

antv G6的基础入门及树图的实际应用

官网:https://g6.antv.antgroup.com/

详细api:https://g6.antv.antgroup.com/api/Graph

简介

G6 是一个简单、易用、完备的图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。旨在让关系变得透明,简单。让用户获得关系数据的 Insight。基于 G6,用户可以快速搭建自己的 图分析 或 图编辑 应用。

特性

G6 作为一款专业的图可视化引擎,具有以下特性:

  • 丰富的元素:内置丰富的节点与边元素,自由配置,支持自定义;
  • 可控的交互:内置 10+ 交互行为,支持自定义交互;
  • 强大的布局:内置了 10+ 常用的图布局,支持自定义布局;
  • 便捷的组件:优化内置组件功能及性能;
  • 友好的体验:根据用户需求分层梳理文档,支持 TypeScript 类型推断。

除了默认好用、配置自由的内置功能,元素、交互、布局均具有高可扩展的自定义机制。

安装 & 引用

1、在项目中使用 NPM 包引入

Step 1: 使用命令行在项目目录下执行以下命令:

npm install --save @antv/g6

Step 2: 在需要用的 G6 的 JS 文件中导入:

import G6 from '@antv/g6';

2、在 HTML 中使用 CDN 引入

// version <= 3.2
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-{$version}/build/g6.js"></script>
// version >= 3.3
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/{$version}/dist/g6.min.js"></script>
// version >= 4.0
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.3.11/dist/g6.min.js"></script>

快速上手

步骤一:创建关系图的 HTML 容器

需要在 HTML 中创建一个用于容纳 G6 绘制的图的容器,通常为 div 标签。G6 在绘制时会在该容器下追加 canvas 标签,然后将图绘制在其中。

<div id="mountNode"></div>

步骤二:数据准备

引入 G6 的数据源为 JSON 格式的对象。该对象中需要有节点(nodes)和边(edges)字段,分别用数组表示:

const data = {
  // 点集
  nodes: [
    {
      id: 'node1', // String,该节点存在则必须,节点的唯一标识
      x: 100, // Number,可选,节点位置的 x 值
      y: 200, // Number,可选,节点位置的 y 值
    },
    {
      id: 'node2', // String,该节点存在则必须,节点的唯一标识
      x: 300, // Number,可选,节点位置的 x 值
      y: 200, // Number,可选,节点位置的 y 值
    },
  ],
  // 边集
  edges: [
    {
      source: 'node1', // String,必须,起始点 id
      target: 'node2', // String,必须,目标点 id
    },
  ],
};

注意

  • nodes 数组中包含节点对象。每个节点对象中唯一的、必要的 id 以标识不同的节点,x、 y 指定该节点的位置;
  • edges 数组中包含边对象。source 和 target 是每条边的必要属性,分别代表了该边的起始点 id 与 目标点 id。
  • 点和边的其他属性参见链接:内置节点 和 内置边。

步骤三:创建关系图

创建关系图(实例化)时,至少需要为图设置容器、宽和高。

const graph = new G6.Graph({
  container: 'mountNode', // String | HTMLElement,必须,在 步骤一 中创建的容器 id 或容器本身
  width: 800, // Number,必须,图的宽度
  height: 500, // Number,必须,图的高度
  // ...其他配置
});

步骤四:配置数据源,渲染

graph.data(data); // 读取 步骤二 中的数据源到图上
graph.render(); // 渲染图

在react中使用g6

import React, { useEffect, useRef } from 'react';
import { Graph } from '@antv/g6';

const data = {
  nodes: [
    { id: '1', label: '公司1' },
    { id: '2', label: '公司2' },
    // 节点数据 ...
  ],
  edges: [
    {
      source: '1',
      target: '2',
      data: { type: 'name1', amount: '100,000,000,00 元', date: '2019-08-03' },
    },
    // 边数据 ...
  ],
};

export default () => {
  const containerRef = useRef(null);
  const graphRef =useRef();

  useEffect(() => {
    if (graphRef.current || !containerRef.current) return;

    const graph = new Graph({
      container: containerRef.current,
      width: 1200,
      height: 800,
      modes: { default: ['drag-canvas'] },
      layout: { type: 'dagre', direction: 'LR' },
      defaultNode: {
        type: 'node',
        labelCfg: {
          style: { fill: '#000000A6', fontSize: 10 },
        },
        style: { stroke: '#72CC4A', width: 150 },
      },
      defaultEdge: { type: 'polyline' },
    });

    // 绑定数据
    graph.data(data);
    // 渲染图
    graph.render();

    graphRef.current = graph;
  }, []);

  return <div ref={containerRef}></div>;
};

图

初始化/实例化图

通过 new G6.Graph(config) 进行图的实例化。

const graph = new G6.Graph({
  container: 'mountNode', // String | HTMLElement,必须
  width: 800, // Number,必须,图的宽度
  height: 500, // Number,必须,图的高度
  // ...其他配置
});

常用配置项

配置项类型选项 / 示例默认说明
rendererStringcanvas / svgcanvas使用 canvas 或 svg 渲染
fitViewBooleantrue / falsefalse是否将图适配到画布大小,可以防止超出画布或留白太多。
fitViewPaddingNumber / Array20 / [ 20, 40, 50, 20 ]0画布上的四周留白宽度。
animateBooleantrue / falsefalse是否启用图的动画。
modesObject{ default: [ 'drag-node', 'drag-canvas' ] }null图上行为模式的集合。由于比较复杂,按需参见:G6 中的 Mode 教程。
defaultNodeObject{ type: 'circle', color: '#000', style: { ...... } }null节点默认的属性,包括节点的一般属性和样式属性(style)。
defaultEdgeObject{ type: 'polyline', color: '#000', style: { ...... } }null边默认的属性,包括边的一般属性和样式属性(style)。
nodeStateStylesObject{ hover: { ...... }, select: { ...... } }null节点在除默认状态外,其他状态下的样式属性(style)。例如鼠标放置(hover)、选中(select)等状态。
edgeStateStylesObject{ hover: { ...... }, select: { ...... } }null边在除默认状态外,其他状态下的样式属性(style)。例如鼠标放置(hover)、选中(select)等状态。
layoutObject{ type: 'force', preventOverlap:false }null布局相关
pluginsArray[minimap, grid]null插件

常用函数

// 读取数据源 `data` 到图实例 `graph` 中
graph.data(data);

// 渲染图
graph.render();

图元素

图形 Shape

图形具体属性:https://g6.antv.antgroup.com/manual/middle/elements/shape/shape-and-properties

G6 中的元素(节点/边)是由一个或多个图形 Shape组成,主要通过自定义节点或自定义边时在 draw 方法中使用 group.addShape 添加,G6 中支持以下的图形 Shape:

  • circle:圆;
  • rect:矩形;
  • ellipse:椭圆;
  • polygon:多边形;
  • fan:扇形;
  • image:图片;
  • marker:标记;
  • path:路径;
  • text:文本;
  • dom(svg):DOM(图渲染方式 renderer 为 'svg' 时可用)。

用法

group.addShape('rect', {
  attrs: {
    fill: 'red',
    shadowOffsetX: 10,
    shadowOffsetY: 10,
    shadowColor: 'blue',
    shadowBlur: 10,
    opacity: 0.8,
  },
  // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
  name: 'rect-shape',
});

DOM (svg)

仅在 Graph 的 renderer 为 'svg' 时可以使用。

⚠️ 注意:

  • 只支持原生 HTML DOM,不支持各类 react、vue 组件;
  • 使用 'dom' 进行自定义的节点或边,不支持 G6 的交互事件,请使用原生 DOM 的交互事件;
  • 在 Safari 中,若 dom 节点被设置了 position:relative,将会导致渲染异常。该问题与 Safari 的 foreignObject bug 有关。Issues。
group.addShape('dom', {
  attrs: {
    width: cfg.size[0],
    height: cfg.size[1],
    // DOM's html
    html: `
    <div style="background-color: #fff; border: 2px solid #5B8FF9; border-radius: 5px; width: ${
      cfg.size[0] - 5
    }px; height: ${cfg.size[1] - 5}px; display: flex;">
      <div style="height: 100%; width: 33%; background-color: #CDDDFD">
        <img alt="img" style="line-height: 100%; padding-top: 6px; padding-left: 8px;" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ" width="20" height="20" />  
      </div>
      <span style="margin:auto; padding:auto; color: #5B8FF9">${cfg.label}</span>
    </div>
      `,
  },
  // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
  name: 'dom-shape',
  draggable: true,
});

节点

节点的通用属性:https://g6.antv.antgroup.com/manual/middle/elements/nodes/default-node#%E8%8A%82%E7%82%B9%E7%9A%84%E9%80%9A%E7%94%A8%E5%B1%9E%E6%80%A7

节点的配置方法

优先级:使用 graph.node(nodeFn) 配置 > 数据中动态配置 > 实例化图时全局配置

1、实例化图时全局配置

用户在实例化 Graph 时候可以通过 defaultNode 配置节点,这里的配置是全局的配置,将会在所有节点上生效。

const graph = new G6.Graph({
  container: 'mountNode',
  width: 800,
  height: 600,
  defaultNode: {
    type: 'circle',
    // 其他配置
  },
});

2、 数据中动态配置

如果需要为不同节点进行不同的配置,可以将配置写入到节点数据中。这种配置方式可以通过下面代码的形式直接写入数据,也可以通过遍历数据的方式写入。

const data = {
  nodes: [{
    id: 'node0',
    size: 100,
    type: 'rect',
    ...    // 其他属性
    style: {
      ...  // 样式属性,每种节点的详细样式属性参见各节点文档
    }
  },{
    id: 'node1',
    size: [50, 100],
    type: 'ellipse',
    ...    // 其他属性
    style: {
      ...  // 样式属性,每种节点的详细样式属性参见各节点文档
    }
  },
    ... // 其他节点
  ],
  edges: [
    ... // 边
  ]
}

3、使用 graph.node(nodeFn) 配置

该方法可以为不同节点进行不同的配置。

提示:

  • 该方法必须在 render 之前调用,否则不起作用;
  • 由于该方法优先级最高,将覆盖其他地方对节点的配置,这可能将造成一些其他配置不生效的疑惑;
  • 该方法在增加元素、更新元素时会被调用,如果数据量大、每个节点上需要更新的内容多时,可能会有性能问题。
// const data = ...
// const graph = ...
graph.node((node) => {
  return {
    id: node.id,
    type: 'rect',
    style: {
      fill: 'blue',
    },
  };
});

graph.data(data);
graph.render();

内置节点

G6 的内置节点包括 circle,rect,ellipse,diamond,triangle,star,image,modelRect,donut(v4.2.5 起支持)。

https://g6.antv.antgroup.com/manual/middle/elements/nodes/built-in/circle

自定义节点

自定义元素 G6.registerX: https://g6.antv.antgroup.com/api/register-item

当内置节点不满足需求时,可以通过 G6.registerNode(nodeName, options, extendedNodeName) 方法自定义节点。

参数:

名称类型是否必选描述
nodeNameStringtrue自定义节点名称,需保持唯一性。
optionsObjecttrue自定义节点时的配置项,配置项中包括完整的生命周期方法,具体请参考:Shape Doc 和 自定义节点与边 API。
extendNodeNameStringfalse自定义节点时可基于内置节点进行定义,该字段表示内置节点名称,所有内置节点请参考:内置节点 教程。

若给定了 extendNodeName,如 draw,update,setState 等必要的函数若不在 nodeDefinition 中进行复写,将会继承 extendNodeName 中的相关定义。

G6 中自定义节点的 API 如下:

G6.registerNode(
  'nodeName',
  {
    /**
     * 绘制节点,包含文本
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中的图形对象的容器
     * @return {G.Shape} 绘制的图形,通过 node.get('keyShape') 可以获取到
     */
    draw(cfg, group) {},
    /**
     * 绘制后的附加操作,默认没有任何操作
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中的图形对象的容器
     */
    afterDraw(cfg, group) {},
    /**
     * 更新节点,包含文本
     * @override
     * @param  {Object} cfg 节点的配置项
     * @param  {Node} node 节点
     */
    update(cfg, node) {},
    /**
     * 更新节点后的操作,一般同 afterDraw 配合使用
     * @override
     * @param  {Object} cfg 节点的配置项
     * @param  {Node} node 节点
     */
    afterUpdate(cfg, node) {},
    /**
     * 设置节点的状态,主要是交互状态,业务状态请在 draw 方法中实现
     * 单图形的节点仅考虑 selected、active 状态,有其他状态需求的用户自己复写这个方法
     * @param  {String} name 状态名称
     * @param  {Object} value 状态值
     * @param  {Node} node 节点
     */
    setState(name, value, node) {},
    /**
     * 获取锚点(相关边的连入点)
     * @param  {Object} cfg 节点的配置项
     * @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有锚点
     */
    getAnchorPoints(cfg) {},
  },
  'extendedNodeName',
);

使用 DOM 自定义节点

仅在 Graph 的 renderer 为 'svg' 时可以使用。

G6.registerNode(
  'dom-node',
  {
    draw: (cfg: ModelConfig, group: Group) => {
      return group.addShape('dom', {
        attrs: {
          width: cfg.size[0],
          height: cfg.size[1],
          // 传入 DOM 的 html
          html: `
        <div style="background-color: #fff; border: 2px solid #5B8FF9; border-radius: 5px; width: ${
          cfg.size[0] - 5
        }px; height: ${cfg.size[1] - 5}px; display: flex;">
          <div style="height: 100%; width: 33%; background-color: #CDDDFD">
            <img alt="img" style="line-height: 100%; padding-top: 6px; padding-left: 8px;" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ" width="20" height="20" />  
          </div>
          <span style="margin:auto; padding:auto; color: #5B8FF9">${cfg.label}</span>
        </div>
          `,
        },
        name: 'dom-node-keyShape',  // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
        draggable: true,
      });
    },
  },
  'single-node',
);

调整节点的状态样式

调整节点的鼠标选中/悬浮样式:样式变化响应;动画响应;

  • 在 G6 中自定义节点/边时在 setState 方法中进行节点状态变化的响应;
  • 通过 graph.setItemState() 方法来设置状态。

例:基于 rect 扩展出一个 custom 图形,默认填充色为白色,当鼠标点击时变成红色

// 基于 rect 扩展出新的图形
G6.registerNode(
  'custom',
  {
    // 响应状态变化
    setState(name, value, item) {
      const group = item.getContainer();
      const shape = group.get('children')[0]; // 顺序根据 draw 时确定
      if (name === 'selected') {
        if (value) {
          shape.attr('fill', 'red');
        } else {
          shape.attr('fill', 'white');
        }
      }
    },
  },
  'rect',
);

// 点击时选中,再点击时取消
graph.on('node:click', (ev) => {
  const node = ev.item;
  graph.setItemState(node, 'selected', !node.hasState('selected')); // 切换选中
});

节点的连接点 anchorPoint

指的是边连入节点的相对位置,即节点与其相关边的交点位置。anchorPoints 是一个二维数组,每一项表示一个连接点的位置,在一个图形 Shape 中,连接点的位置如下图所示,x 和 y 方向上范围都是 [0, 1]:

img

边可以通过指定 sourceAnchor 和 targetAnchor 分别选择起始点、结束点的 anchorPoint

如何在节点上配置连接点、在边上指定连接点:

const data = {
  nodes: [
    {
      id: 'node1',
      label: 'node1',
      x: 100,
      y: 200,
      // 该节点可选的连接点集合,该点有两个可选的连接点
      anchorPoints: [
        [0, 1],
        [0.5, 1],
      ],
      type: 'rect',
    },
    {
      id: 'node2',
      label: 'node2',
      x: 300,
      y: 400,
      // 该节点可选的连接点集合,该点有两个可选的连接点
      anchorPoints: [
        [0.5, 0],
        [1, 0.5],
      ],
      type: 'rect',
    },
  ],
  edges: [
    {
      source: 'node1',
      target: 'node2',
      // 该边连入 source 点的第 0 个 anchorPoint,
      sourceAnchor: 0,
      // 该边连入 target 点的第 0 个 anchorPoint,
      targetAnchor: 0,
      style: {
        endArrow: true,
      },
    },
    {
      source: 'node2',
      target: 'node1',
      // 该边连入 source 点的第 1 个 anchorPoint,
      sourceAnchor: 1,
      // 该边连入 source 点的第 1 个 anchorPoint,
      targetAnchor: 1,
      style: {
        endArrow: true,
      },
    },
  ],
};

有两种方式来调整节点上的锚点:

  • 在数据里面指定 anchorPoints。

    const data = {
      nodes: [
        {
          id: 'node1',
          x: 100,
          y: 100,
          anchorPoints: [
            [0, 0.5], // 左侧中间
            [1, 0.5], // 右侧中间
          ],
        },
        //...       // 其他节点
      ],
      edges: [
        //... // 边
      ],
    };
  • 自定义节点中通过 getAnchorPoints 方法指定锚点。

    G6.registerNode(
      'diamond',
      {
        //... // 其他方法
        getAnchorPoints() {
          return [
            [0, 0.5], // 左侧中间
            [1, 0.5], // 右侧中间
          ];
        },
      },
      'rect',
    );

使用类 JSX 语法定义 G6 节点

只需要在使用 G6.registerNode 自定义节点时,将第二个参数设置为字符串或一个返回值为 string 的 function。

使用类 JSX 语法来定义一个简单的矩形:

G6.registerNode(
  'rect-xml',
  (cfg) => `
  <rect style={{
    width: 100, height: 20, fill: '#1890ff', stroke: '#1890ff', radius: [6, 6, 0, 0]
  }} keyshape="true" name="test">
    <text style={{ 
			marginTop: 2, 
			marginLeft: 50, 
      textAlign: 'center', 
      fontWeight: 'bold', 
      fill: '#fff' }} 
			name="title">${cfg.label || cfg.id}</text>
    <polygon style={{
      points:[[ 30, 30 ], [ 40, 20 ], [ 30, 50 ], [ 60, 100 ]],
          fill: 'red'
    }} />
        <polyline style={{ points: [[ 30, 30 ], [ 40, 20 ], [ 60, 100 ]] }} />
        <image style={{ img: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 48, height: 48, marginTop: 100 }} />
  </rect>
`,
);

使用 React 定义节点

https://g6.antv.antgroup.com/manual/middle/elements/nodes/react-node

G6 React Node Docs

首先在安装完 G6 后,你需要额外安装 @antv/g6-react-node

npm install @antv/g6-react-node
// yarn add @antv/g6-react-node
import React from 'react';
import G6 from '@antv/g6';
import { Rect, Text, Circle, Image, Group, createNodeFromReact } from '@antv/g6-react-node';

const Tag = ({ text, color }) => (
  <Rect
    style={{
      fill: color,
      padding: [5, 10],
      width: 'auto',
      radius: [4],
      margin: [0, 8],
    }}
  >
    <Text style={{ fill: '#fff', fontSize: 10 }}>{text}</Text>
  </Rect>
);

const Card = ({ cfg }) => {
  const { collapsed = false } = cfg;

  return (
    <Group draggable>
      <Rect
        style={{
          width: 400,
          height: 'auto',
          fill: '#fff',
          stroke: '#ddd',
          shadowColor: '#eee',
          shadowBlur: 30,
          radius: [8],
          justifyContent: 'center',
          padding: [18, 0],
        }}
        draggable
      >
        <Text
          style={{
            fill: '#000',
            margin: [0, 24],
            fontSize: 16,
            fontWeight: 'bold',
          }}
        >
          这是一个卡片
        </Text>
        <Text style={{ fill: '#ccc', fontSize: 12, margin: [12, 24] }}>
          我是一段特别特别特别特别特别特别特别长的描述
        </Text>
        {collapsed && (
          <Group>
            <Image
              style={{
                img: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
                width: 200,
                height: 200,
                margin: [24, 'auto'],
              }}
            />
            <Rect style={{ width: 'auto', flexDirection: 'row', padding: [4, 12] }}>
              <Tag color="#66ccff" text="我是" />
              <Tag color="#66ccff" text="很多个" />
              <Tag color="#66ccff" text="很多个的" />
              <Tag color="#66ccff" text="标签" />
            </Rect>
          </Group>
        )}
        <Circle
          style={{
            position: 'absolute',
            x: 380,
            y: 20,
            r: 5,
            fill: collapsed ? 'blue' : 'green',
          }}
        >
          <Text
            style={{
              fill: '#fff',
              fontSize: 10,
              margin: [-6, -3, 0],
              cursor: 'pointer',
            }}
            onClick={(evt, node, shape, graph) => {
              graph.updateItem(node, {
                collapsed: !collapsed,
              });
            }}
          >
            {collapsed ? '-' : '+'}
          </Text>
        </Circle>
      </Rect>
    </Group>
  );
};

G6.registerNode('test', createNodeFromReact(Card));

边

边的配置方法

优先级:使用 graph.edge(edgeFn) 配置 > 数据中动态配置 > 实例化图时全局配置

即有相同的配置项时,优先级高的方式将会覆盖优先级低的。

1、实例化图时全局配置

const graph = new G6.Graph({
  container: 'mountNode',
  width: 800,
  height: 600,
  defaultEdge: {
    type: 'line',
    // ... 其他配置
  },
});

2、数据中动态配置

const data = {
  nodes: [
    ... // 节点
  ],
  edges: [{
    source: 'node0',
    target: 'node1'
    type: 'polyline',
    ... // 其他配置
    style: {
      ...  // 样式属性,每种边的详细样式属性参见各边文档
    }
  },{
    source: 'node1',
    target: 'node2'
    type: 'cubic',
    ... // 其他配置
    style: {
      ...  // 样式属性,每种边的详细样式属性参见各边文档
    }
  },
    ... // 其他边
  ]
}

3、graph.edge(edgeFn) 配置

// const data = ...
// const graph = ...
graph.edge((edge) => {
  return {
    id: edge.id,
    type: 'polyline',
    style: {
      fill: 'steelblue',
    },
  };
});

graph.data(data);
graph.render();

内置边

https://g6.antv.antgroup.com/manual/middle/elements/edges/built-in/line

  • line:直线,不支持控制点;
  • polyline:折线,支持多个控制点;
  • arc:圆弧线;
  • quadratic:二阶贝塞尔曲线;
  • cubic:三阶贝塞尔曲线;
  • cubic-vertical:垂直方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
  • cubic-horizontal:水平方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
  • loop:自环。

箭头

内置箭头:https://g6.antv.antgroup.com/manual/middle/elements/edges/arrow#%E5%86%85%E7%BD%AE%E7%AE%AD%E5%A4%B4

使用方法:

style: {
  endArrow: {
    path: G6.Arrow.triangle(10, 20, 25), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
    d: 25
  },
  startArrow: {
    path: G6.Arrow.vee(15, 20, 15), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
    d: 15
  },
}

自定义边

自定义元素 G6.registerX: https://g6.antv.antgroup.com/api/register-item

当内置的边不能满足需求时,可以通过 registerEdge(edgeName, options, extendedEdgeName) 方法注册自定义的边。

参数:

edgeNameStringtrue自定义边的名称
optionsObjecttrue自定义边时的配置项,配置项中包括完整的生命周期方法,具体请参考:Shape Doc 和 自定义节点与边 API。
extendedEdgeNameStringfalse自定义边时可基于内置边进行定义,该字段表示内置边的名称,所有内置边请参考:内置边 教程。

需要注意的是,自定义边/节点时,若给定了 extendedEdgeName,如 draw,update,setState 等必要的函数若不在 edgeDefinition 中进行复写,将会继承 extendedEdgeName 中的相关定义

G6.registerEdge(
  'edgeName',
  {
    /**
     * 绘制边,包含文本
     * @param  {Object} cfg 边的配置项
     * @param  {G.Group} group 图形分组,边中的图形对象的容器
     * @return {G.Shape} 绘制的图形,通过 node.get('keyShape') 可以获取到
     */
    draw(cfg, group) {},
    /**
     * 绘制后的附加操作,默认没有任何操作
     * @param  {Object} cfg 边的配置项
     * @param  {G.Group} group 图形分组,边中的图形对象的容器
     */
    afterDraw(cfg, group) {},
    /**
     * 更新边,包含文本
     * @override
     * @param  {Object} cfg 边的配置项
     * @param  {Edge} edge 边
     */
    update(cfg, edge) {},
    /**
     * 更新边后的操作,一般同 afterDraw 配合使用
     * @override
     * @param  {Object} cfg 边的配置项
     * @param  {Edge} edge 边
     */
    afterUpdate(cfg, edge) {},
    /**
     * 设置边的状态,主要是交互状态,业务状态请在 draw 方法中实现
     * 单图形的边仅考虑 selected、active 状态,有其他状态需求的用户自己复写这个方法
     * @param  {String} name 状态名称
     * @param  {Object} value 状态值
     * @param  {Edge} edge 边
     */
    setState(name, value, edge) {},
  },
  'extendedEdgeName',
);

调整边的交互样式

  • 在 G6 中自定义节点/边时在 setState 方法中进行边状态变化的响应;
  • 通过 graph.setItemState() 方法来设置状态。

⚠️注意:

边过细时点击很难被击中,可以设置 lineAppendWidth 来提升击中范围。

// 基于 line 扩展出新的边
G6.registerEdge(
  'custom-edge',
  {
    // 响应状态变化
    setState(name, value, item) {
      const group = item.getContainer();
      const shape = group.get('children')[0]; // 顺序根据 draw 时确定
      if (name === 'active') {
        if (value) {
          shape.attr('stroke', 'red');
        } else {
          shape.attr('stroke', '#333');
        }
      }
      if (name === 'selected') {
        if (value) {
          shape.attr('lineWidth', 3);
        } else {
          shape.attr('lineWidth', 2);
        }
      }
    },
  },
  'line',
);

// 点击时选中,再点击时取消
graph.on('edge:click', (ev) => {
  const edge = ev.item;
  graph.setItemState(edge, 'selected', !edge.hasState('selected')); // 切换选中
});

graph.on('edge:mouseenter', (ev) => {
  const edge = ev.item;
  graph.setItemState(edge, 'active', true);
});

graph.on('edge:mouseleave', (ev) => {
  const edge = ev.item;
  graph.setItemState(edge, 'active', false);
});

图布局

图布局是指图中节点的排布方式,根据图的数据结构不同,布局可以分为两类:一般图布局、树图布局。

一般图布局

一般图 Graph 布局方法总览

const graph = new G6.Graph({
  // ...                      // 其他配置项
  layout: {
    // Object,可选,布局的方法及其配置项,默认为 random 布局。
    type: 'force',
    preventOverlap: true,
    nodeSize: 30,
    // workerEnabled: true, // 是否启用 webworker
    // gpuEnabled: true // 是否使用 gpu 版本的布局算法,G6 4.0 支持,目前仅支持 gForce 及 fruchterman
    // ...                    // 其他配置
  },
});

树图布局

树图 TreeGraph 布局方法总览

const graph = new G6.TreeGraph({
  container: 'mountNode',
  modes: {
    default: [
      {
        // 定义展开/收缩行为
        type: 'collapse-expand',
      },
      'drag-canvas',
    ],
  },
  // 定义布局
  layout: {
    type: 'dendrogram', // 布局类型
    direction: 'LR', // 自左至右布局,可选的有 H / V / LR / RL / TB / BT
    nodeSep: 50, // 节点之间间距
    rankSep: 100, // 每个层级之间的间距
    excludeInvisibles: true, // 布局计算是否排除掉隐藏的节点,v4.8.8 起支持
  },
});

通用配置项

名称类型默认值描述
typeStringdendrogram布局类型,支持 dendrogram、compactBox、mindmap 和 indented。
excludeInvisiblesBooleanfalsev4.8.8 起支持。 布局计算是否排除掉隐藏的节点,若配置为 true,则隐藏节点不参与布局计算。
directionStringLR布局方向,有 LR , RL , TB , BT , H , V 可选。 L:左;R:右;T:上;B:下;H:垂直;V:水平。
getChildrenFunction返回当前节点的所有子节点

类型

类型描述图示说明
compactBox紧凑树布局:从根节点开始,同一深度的节点在同一层,并且布局时会将节点大小考虑进去。img
dendrogram树状布局:不管数据的深度多少,总是叶节点对齐。不考虑节点大小,布局时将节点视为 1 个像素点。img
indented缩进布局:每个元素会占一行/一列。img
mindmap脑图布局:深度相同的节点将会被放置在同一层,与 compactBox 不同的是,布局不会考虑节点的大小。img

布局的切换机制

  • updateLayout(params):布局方法或参数的切换;
  • changeData():数据的切换。

交互与事件

监听事件

graph.on('canvas:click', (ev) => {
  const shape = ev.target;
  const item = ev.item;
  if (item) {
    const type = item.getType();
  }
});

事件类型

1、全局事件:只要在画布上范围内发生均会被触发,如 mousedown,mouseup,click,mouseenter,mouseleave 等。

2、canvas 事件:只在 canvas 空白处被触发,如 canvas:mousedown,canvas:click 等,以canvas:eventName 为事件名称。

3、节点/边/combo 上的事件,例如 node:mousedown,edge:click, combo:click 等,以 type:eventName 为事件名称。

4、图形上的事件:指定图形上的事件,如 circle-shape:mousedown,circle-shape:click 等,以 shapeName:eventName 为事件名称。

5、时机事件:时机事件指渲染、视口变换、元素增删改、数据变换等时机。所有时机事件详见 G6 的时机事件列表。如:beforeadditem,afteradditem 等。

6、自定义事件:G6 允许用户自定义任意事件,可在任意位置通过 graph.emit(customEventName: string, event: IG6GraphEvent) 触发一个事件,第一个参数为自定义事件名称。在触发前,通过 graph.on(customEventName: string, callback: Function) 进行监听。

例:

graph.on('some-custom-event-name', (ev) => {
  // ... do sth
});

graph.emit('some-custom-event-name', {
  // some params
})

交互行为Behavior

Behavior 是 G6 提供的定义图上交互事件的机制。它与交互模式 Mode 搭配使用,如何将下文所述各种 Behavior 配置到图上,见 交互模式。

const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500,
  modes: {
    // 支持的 behavior
    default: ['drag-canvas'],
  },
});

内置behavior

常用:

drag-canvas:拖拽画布

zoom-canvas:缩放画布

tooltip:文本节点提示

collapse-expand:只适用于树图,展开或收起子树

自定义behavior

通过 G6.registerBehavior 自定义 Behavior。

G6.registerBehavior('activate-node', {
  getDefaultCfg() {
    return {
      multiple: true
    };
  },
  getEvents() {
    return {
      'node:click': 'onNodeClick',
      'canvas:click': 'onCanvasClick'
    };
  }
  onNodeClick(e) {
    const graph = this.graph;
    const item = e.item;
    if (item.hasState('active')) {
      graph.setItemState(item, 'active', false);
      return;
    }
    // this 上即可取到配置,如果不允许多个 'active',先取消其他节点的 'active' 状态
    if (!this.multiple) {
      this.removeNodesState();
    }
    // 置点击的节点状态 'active' 为 true
    graph.setItemState(item, 'active', true);
  },
  onCanvasClick(e) {
    // shouldUpdate 可以由用户复写,返回 true 时取消所有节点的 'active' 状态,即将 'active' 状态置为 false
    if (this.shouldUpdate(e, self)) {
      removeNodesState();
    }
  },
  removeNodesState() {
    graph.findAllByState('node', 'active').forEach(node => {
        graph.setItemState(node, 'active', false);
      });
  }
});

使用自定义behavior

const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500,
  modes: {
    // 定义的 Behavior 指定到这里,就可以支持 Behavior 中定义的交互
    default: ['activate-node'],
  },
});

交互模式Mode

一个图上可以有存在多种交互模式,每个交互模式包含多种交互行为 Behavior。

配置mode

const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500,
  modes: {
    // 支持的 behavior
    default: ['drag-canvas', 'zoom-canvas'], // 默认模式下的交互行为
    edit: ['click-select'],	// 编辑模式下的交互行为
  },
});

切换mode

graph.setMode('edit');

状态State

https://g6.antv.antgroup.com/manual/middle/states/state

G6 中的 state,指的是节点或边的状态,包括交互状态和业务状态两种。

设置state

graph.setItemState(item, stateName, stateValue)
参数名类型描述
itemNumber需要被设置状态的节点/边实例
stateNameString状态名称,可以是任意字符串
stateValueBooeleantrue 代表该状态是被激活,false 代表该状态被灭活

调用state

该函数可以在监听函数 graph.on 中被调用,也可以在自定义 Behavior 中调用,或在其他任意地方用于响应交互/业务的变化。

1、graph.on

在回调函数中使定义的交互状态 hover 生效。

graph.on('node:mouseenter', (evt) => {  const { item } = evt;  graph.setItemState(item, 'hover', true);});
graph.on('node:mouseleave', (evt) => {  const { item } = evt;  graph.setItemState(item, 'hover', false);});

2、Behavior

在自定义 Behavior 中使定义的交互状态 selected 生效。

G6.registerBehavior('nodeClick', {
  getEvents() {
    return {
      'node:click': 'onClick',
    };
  },
  onClick(e) {
    e.preventDefault();
    if (!this.shouldUpdate.call(this, e)) {
      return;
    }

    const { item } = e;
    const graph = this.graph;
    graph.setItemState(item, 'selected', true);
  },
});

基础动画

全局动画

const graph = new G6.Graph({
  // ...                   // 图的其他配置项
  animate: true, // Boolean,切换布局时是否使用动画过度,默认为 false
  animateCfg: {
    duration: 500, // Number,一次动画的时长
    easing: 'linearEasing', // String,动画函数
  },
});
配置项类型默认值描述
durationNumber500一次动画的时长
easingboolean'linearEasing'动画函数,见 easing 函数
delayNumber0延迟一段时间后执行动画
repeatbooleanfalse是否重复执行动画
callbackFunctionundefined动画执行完时的回调函数
pauseCallbackFunctionundefined动画暂停时(shape.pause())的回调函数
resumeCallbackFunctionundefined动画恢复时(shape.resume())的回调函数

元素(边和节点)动画

由于 G6 的内置节点和边是没有动画的,需要实现节点和边上的动画需要通过自定义节点、自定义边时复写 afterDraw 实现。

https://g6.antv.antgroup.com/manual/middle/animation#%E5%85%83%E7%B4%A0%E5%8A%A8%E7%94%BB

树图实际应用

需求说明:

  • 按集团架构分层展示组织名称及其碳强度信息,默认展开第一个二级企业及其下级企业的卡片

  • 每个二级企业及其下级企业的卡片为一种颜色

  • 可展开收起卡片,收起时显示子项数量

实现效果:

集团架构碳强度分析

完整代码:TreeGraph

1、初始化图、数据处理

const container = document.getElementById('container');
const width = container.scrollWidth;
const height = container.scrollHeight || 500;

ref.current = new G6.TreeGraph({
  container: 'container',
  width,
  height,
  defaultZoom: 0.8,	// 默认缩放比例
  minZoom: 0.5,	// 最小可缩放比例
  modes: {
    default: ['zoom-canvas', 'drag-canvas'],	// 可缩放,可拖拽
  },
  fitView: true,
  animate: true,
  renderer: 'svg',	// 使用svg渲染(用于自定义dom节点)
  defaultNode: {
    type: 'flow-rect',	// 配置自定义节点名称
  },
  defaultEdge: {
    type: 'indentedEdge',	// 配置自定义边名称
  },
  layout: {
    type: 'indented', // 布局类型 缩进树
    direction: 'LR', // 自左至右布局
    dropCap: false, // 每个节点的第一个自节点是否位于下一行
    indent: 350, // 列间间距
    getHeight: (cfg) => {
      return 70 + Math.floor(cfg?.name?.length / 12) * 14;
    },
  },
});

const dataTransform = (data) => {
  const changeData = (d, level = 0, color) => {
    const data = {
      ...d,
    };
    // 给定 branchColor 和 0-2 层节点 depth
    if (data.children?.length) {
      data.depth = 0;
      data.children.forEach((subtree, i) => {
        subtree.branchColor = colors[i % colors.length];
        // dfs
        let currentDepth = 1;
        subtree.depth = currentDepth;
        Util.traverseTree(subtree, (child) => {
          child.branchColor = colors[i % colors.length];

          if (!child.depth) {
            child.depth = currentDepth + 1;
          } else currentDepth = subtree.depth;
          if (child.children) {
            child.children.forEach((subChild) => {
              subChild.depth = child.depth + 1;
            });
          }
          return true;
        });
      });
    }

    return data;
  };
  return changeData(data);
};

ref.current.data(dataTransform(data);
ref.current.render();

2、自定义节点

  • 扩展现有内置节点
  • 使用 文本、矩形、圆形、路径图形自定义节点
  • 使用 DOM 自定义节点
// 扩展现有内置节点
G6.registerNode(
  'flow-rect',
  {
    shapeType: 'flow-rect',
    draw(cfg, group) {
      const {
        name = '',
        value,
        collapsed,
        branchColor = '#008599',
        depth,
        notEmissionControlFlag,
      } = cfg;

      const newName = name.replace(/\s/g, '').replace(/(.{12})/g, '$1\n');

      const isRoot = depth === 0;
      const grey = '#CED4D9';
      const rectConfig = {
        width: 240,
        height: 92 + Math.floor(name?.length / 12) * 14,
        lineWidth: isRoot ? 20 : 1,
        fontSize: 12,
        fill: isRoot ? '#008599' : '#fff',
        radius: 4,
        stroke: grey,
        opacity: 1,
      };

      // 卡片原点
      const nodeOrigin = {
        x: isRoot ? -rectConfig.width / 2 - 50 : -rectConfig.width / 2 - 40,
        y: -rectConfig.height / 2,
      };

      const textConfig = {
        textAlign: 'left',
        textBaseline: 'bottom',
      };

      const rect = group.addShape('rect', {
        attrs: {
          x: nodeOrigin.x,
          y: nodeOrigin.y,
          ...rectConfig,
          strokeOpacity: isRoot ? 0.1 : 1,
          stroke: isRoot ? 'rgba(0, 133, 153)' : branchColor,
        },
      });

      const rectBBox = rect.getBBox();

      // 标题
      group.addShape('text', {
        attrs: {
          ...textConfig,
          x: 20 + nodeOrigin.x,
          y: 42 + Math.floor(name?.length / 12) * 14 + nodeOrigin.y,
          text: name.length > 12 ? newName : name,
          fontSize: 16,
          fill: isRoot ? '#fff' : '#292929',
        },
        name: 'name-shape',
      });

      // 数值 价格+单位
      group.addShape('dom', {
        attrs: {
          x: 20 + nodeOrigin.x,
          y: 56 + Math.floor(name?.length / 12) * 14 + nodeOrigin.y,
          width: 200,
          height: 30,
          html: `
            <div style="display: ${
              value ? 'flex' : 'none'
            };align-items:center;width: 100%;height: 24px;">
              <span style="color: ${
                isRoot ? '#fff' : branchColor
              };font-family: Bebas Neue;font-size: 24px;padding-right: 8px;line-height: 16px;background: ${
            isRoot ? 'rgba(255, 255, 255, 0.20)' : '#F7F8FC'
          };border-radius: 4px 0 0 4px;">${formatAmount(value)}</span>

              <span style="color: ${
                isRoot ? '#fff' : branchColor
              };font-family: PingFang SC;font-size: 14px;line-height: 16px;background: ${
            isRoot ? 'rgba(255, 255, 255, 0.20)' : '#F7F8FC'
          };border-radius: 0 4px 4px 0;">${unit}</span>
            </div>
            <div style="display: ${
              value ? 'none' : 'block'
            };font-family: PingFang SC;font-size: 14px;color: #B7B7B7;">${
            notEmissionControlFlag ? '非控排企业' : '暂未更新'
          }</div>
            `,
        },
        name: 'value-shape',
      });

      // 展开收起
      if (cfg.children && cfg.children.length) {
        // 侧边
        group.addShape('rect', {
          attrs: {
            x: rectBBox.maxX - 4,
            y: rectBBox.minY,
            width: 4,
            height: rectBBox.height,
            radius: [0, rectConfig.radius, rectConfig.radius, 0],
            fill: isRoot ? 'transparent' : branchColor,
          },
        });

        group.addShape('circle', {
          attrs: {
            x: rectConfig.width / 2,
            y: 0,
            r: 11,
            lineWidth: 2,
            stroke: branchColor,
            cursor: 'pointer',
            fill: collapsed ? branchColor : '#fff',
          },
          name: 'collapse-icon',
          modelId: cfg.id,
        });

        // 子节点数量
        group.addShape('text', {
          attrs: {
            x: rectConfig.width / 2,
            y: collapsed ? 0 : -1,
            textAlign: 'center',
            textBaseline: 'middle',
            text: collapsed ? cfg.children?.length : '-',
            num: cfg.children?.length,
            fontSize: 14,
            cursor: 'pointer',
            fill: collapsed ? '#fff' : branchColor,
          },
          name: 'collapse-text',
          modelId: cfg.id,
        });

        // 连接 展开收起 的线段
        group.addShape('path', {
          attrs: {
            path: [
              ['M', rectConfig.width / 2 - 40, 0],
              ['L', rectConfig.width / 2 - 10, 0],
            ],
            stroke: branchColor,
            lineWidth: 2,
          },
          name: 'count-link',
        });
      }
      return rect;
    },
    // setState 配置放下面说明
  },
  'rect',
);

3、自定义边

G6.registerEdge(
  'indentedEdge',
  {
    afterDraw: (cfg, group) => {
      const sourceNode = cfg.sourceNode?.getModel();
      const targetNode = cfg.targetNode?.getModel();
      const color = sourceNode.branchColor || targetNode.branchColor || cfg.color || '#000';
      const keyShape = group.get('children')[0];
      // 修改边的颜色
      keyShape.attr({
        stroke: color,
        lineWidth: 2,
      });
    },
    // 修改控制点
    getControlPoints: (cfg) => {
      const startPoint = cfg.startPoint;
      const endPoint = cfg.endPoint;
      return [
        {
          x: startPoint.x + 80,
          y: startPoint.y,
        },
        {
          x: startPoint.x + 80,
          y: endPoint.y,
        },
        {
          x: endPoint.x,
          y: endPoint.y,
        },
      ];
    },
    update: undefined,
  },
  'polyline',
);

4、监听点击事件实现展开收起

G6.registerNode(
  'flow-rect',
  {
    // ...其他配置看上面的自定义节点
    // 调整节点状态
    setState(name, value, item) {
      if (name === 'collapse') {
        const group = item.getContainer();
        const collapseText = group.find((e) => e.get('name') === 'collapse-text');
        const collapseCircle = group.find((e) => e.get('name') === 'collapse-icon');
        const collapseCircleStyle = item._cfg.originStyle['collapse-icon'];

        if (collapseText && collapseCircle) {
          if (!value) {
            collapseText.attr({
              text: '-',
              fill: collapseCircleStyle.stroke,
            });
            collapseCircle.attr({
              fill: '#fff',
            });
          } else {
            collapseText.attr({
              text: collapseText.attrs.num,
              fill: '#fff',
            });
            collapseCircle.attr({
              fill: collapseCircleStyle.stroke,
            });
          }
        }
      }
    },
  },
  'rect',
);

const handleCollapse = (e) => {
  const target = e.target;
  const id = target.get('modelId');
  const item = ref.current.findById(id);
  const nodeModel = item.getModel();
  nodeModel.collapsed = !nodeModel.collapsed;
  ref.current.layout();
  ref.current.setItemState(item, 'collapse', nodeModel.collapsed);
};
// 给展开收起icon和文本绑定事件
ref.current.on('collapse-text:click', (e) => {
  handleCollapse(e);
});
ref.current.on('collapse-icon:click', (e) => {
  handleCollapse(e);
});

5、数据更新

useEffect(() => {
    if (ref.current) {
       // 数据更新
       ref.current.changeData(data);
    }
}, [data]);

踩坑记录

1、布局问题:内置的树图布局并不能计算出节点准确的坐标,当节点宽高不统一时,节点可能会重叠在一起。

目前是通过主动去设定间隔和节点宽高去调整布局,但个人感觉这种方法不是特别的优雅。

layout: {
    type: 'indented', // 布局类型 缩进树
    direction: 'LR', // 自左至右布局
    dropCap: false, // 每个节点的第一个自节点是否位于下一行
    indent: 350, // 列间间距
    // 动态设置节点高度(文本每12个字换行,增加14px高度)
    getHeight: (cfg) => {
      return 70 + Math.floor(cfg?.name?.length / 12) * 14;
    },
  },

2、节点在首次展开收起时出现问题:节点要加唯一标识id

总结

G6作为一款国产图可视化引擎,可以实现很多d3才能实现的可视化图表,相比较d3来说,它拥有中文官方文档,对于阅读和学习会更加方便。虽然还有些小的问题,场景也不够多远,但是G6提供的布局和交互能力基本可以覆盖常见的业务需求,开箱即用同时又有较好的扩展性。需要快速开发的情况下G6是个不错的选择。

最近更新: 2025/4/23 09:06
Contributors: csmSimona