AntV G6 + React 实现 数据血缘图

1,902 阅读6分钟

阅读时间:7min

G6介绍

G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。旨在让关系变得透明,简单。让用户获得关系数据的 Insight。

框架比较

目前 AntV 有两个关于图的库:G6、X6(详细区别)

  • G6

    • 探索数据、获得洞察以及其他的辅助能力
    • 底层canvas、svg都支持(可选),大量节点优选
    • 节点内部图元复杂的优先
  • X6

    • 创建、编辑数据样式与形状
    • 底层基于svg,200以内节点的图编辑场景优选
    • 节点内部图元简单的优先

本场景主要是展示节点,没有编辑需求,所以最终决定采用G6可视化引擎的决策树图来实现。

什么是数据血缘

数据血缘是指数据之间的关系和依赖。它描述了数据来源和数据流动的路径,可以帮助我们理解数据的来源、变化和影响。

用途

通过分析数据血缘,我们可以追溯数据的历史和变化,并了解数据之间的依赖关系,从而更好地管理和利用数据。在数据工程和数据分析领域,数据血缘是一个重要的概念,可以帮助我们构建数据管道、处理数据质量问题和分析数据影响等。

功能实现

  • 树形图展示数据流向
  • 缩放视口
  • 拖拽画布
  • 收起和展开

代码实现

下载:

npm install --save @antv/g6

模拟数据源:

// mock.ts
const mockData = {
  id: 'g1',
  name: '入库',
  date: '2021-1-1',
  version: '2022-2-2',
  executor: '小明',
  owner: '大d',
  status: 'warehouse',
  children: [
    {
      id: 'g12',
      rate: 0.627,
      status: 'primary',
      versionName: '我是第一次清洗后版本',
      children: [
        {
          id: 'g121',
          name: '清洗策略:第1种清洗策略名',
          date: '2021-1-1',
          version: '2022-2-2',
          collapsed: true,
          status: 'clean',
          executor: '小明',
          owner: '小d',
          children: [
            {
              id: 'g1211',
              rate: 1.0,
              status: 'primary',
              versionName: '我是第二次清洗后版本',
              children: [
                {
                  id: 'g1221',
                  name: '转bin策略:转bin',
                  version: '2022-2-2',
                  date: '2021-1-1',
                  status: 'bin',
                  executor: '小明',
                  owner: '小d',
                },
                {
                  id: 'g1222',
                  name: '质检策略:我是一种质检策略',
                  date: '2021-1-1',
                  version: '2022-2-2',
                  status: 'inspect',
                  executor: '小明',
                  owner: '小d',
                },
                {
                  id: 'g1223',
                  name: '打标策略:我是一种打标策略',
                  date: '2021-1-1',
                  version: '2022-2-2',
                  status: 'marking',
                  executor: '小明',
                  owner: '小d',
                },
                {
                  id: 'g10011',
                  name: '清洗策略:第二种清洗策略',
                  date: '2021-1-1',
                  collapsed: true,
                  version: '2022-2-2',
                  status: 'clean',
                  executor: '小明',
                  owner: '小d',
                  children: [
                    {
                      id: 'g12211',
                      status: 'primary',
                      versionName: '最终版本',
                    },
                  ],
                },
              ],
            },
          ],
        },
      ],
    },
  ],
};

export default mockData;

数据血缘组件:最终作为一个canvas呈现在页面上

// 基本结构
import React, { useEffect, useRef } from 'react';
import G6, { G6GraphEvent } from '@antv/g6';

import mockData from './mock';

const DataLineAge = () => {
  const ref: any = useRef(null);
  let graph: any = null;

  // 组件config
  const config = {
    ...
  };

  // 获取到节点标识值后再开始渲染
  useEffect(() => {
    if (!graph) {  
      // 默认配置
      const defaultConfig = {
          // ...
      };

      // 自定义节点
      G6.registerNode(
        'flow-rect',
        {
          shapeType: 'flow-rect',
          draw(cfg: any, group: any) { // 绘制节点的方法
            //...
            // 向节点添加图形
            const rect = group.addShape('rect', {
              // ..
            });

            // 向节点添加文字
            group.addShape('text', {
              ...
            });
             return rect;
          },
          setState(name:any, value:any, item:any) { // 当外部调用 graph.setItemState时的响应函数
             ...
          },
        },
        'rect',
      );

      graph = new G6.TreeGraph({
        container: ref.current, // 容器
        ...defaultConfig, // 默认配置
        ...config, // 其他配置
      });
    }

    graph.data(mockData); // 配置数据源
    graph.render(); // 渲染
    graph.zoom(config.defaultZoom || 1); // 改变缩放比例

    // 单击扩展卡片事件绑定
    graph.on('collapse-text:click', (e: G6GraphEvent) => {
      ...
    });
    // 单击收起卡片事件绑定
    graph.on('collapse-back:click', (e: G6GraphEvent) => {
      ...
    });
  }, []);
 
  return <div ref={ref}></div>;
};

export default DataLineAge;

完整代码:

// dataLineage.tsx
import React, { useEffect, useRef } from 'react';
import G6, { G6GraphEvent } from '@antv/g6';

import mockData from './mock';
type IColorType = {
  warehouse: string; // 入库
  primary: string; // 默认
  clean: string; // 清洗
  bin: string; // 打bin
  inspect: string; // 质检
  marking: string; // 打标
};
interface IObj {
  date: string;
  version: string;
  executor: string;
  owner: string;
}
const colors: IColorType = {
  warehouse: '#7d5edf',
  primary: '#5B8FF9',
  bin: '#EEBC20',
  inspect: '#00ba6f',
  marking: '#A7A7A7',
  clean: '#6dc8dc',
};

const DataLineAge = () => {
  const ref: any = useRef(null);
  let graph: any = null;

  // 组件config
  const config = {
    padding: [20, 50],
    defaultLevel: 5, // 默认层级
    defaultZoom: 0.8, // 默认缩放
  };

  // 控制折叠展开卡片
  const handleCollapse = (e: G6GraphEvent) => {
    const target = e.target;
    const id = target.get('modelId');
    const item = graph.findById(id);
    const nodeModel = item.getModel();
    nodeModel.collapsed = !nodeModel.collapsed;
    graph.layout();
    graph.setItemState(item, 'collapse', nodeModel.collapsed);
  };
  // 获取到节点标识值后再开始渲染
  useEffect(() => {
    if (!graph) {
      // 宽高由内容撑开
      const width = ref.current?.scrollWidth;
      const height = ref.current?.scrollHeight || 1000;
      
      // 默认配置
      const defaultConfig = {
        width,
        height,
        modes: {
          default: ['zoom-canvas', 'drag-canvas'], // 支持缩放、拖拽
        },
        fitView: true, // 开启画布自适应,图适配画布大小
        animate: true, // 启用全局动画
        defaultNode: {
          // 默认状态下节点的配置,会被写入的data覆盖
          type: 'flow-rect',
        },
        defaultEdge: {
          // 默认状态下边的配置
          type: 'cubic-horizontal', // 水平方向的三贝塞尔曲线
          style: {
            stroke: '#CED4D9',
          },
        },
        layout: {
          // 定义布局
          type: 'indented', // 缩进树
          direction: 'LR', // 根节点在左,往右布局
          dropCap: false, // 子节点默认不在下一行
          indent: 300, // 列间间距
          getHeight: () => {
            return 200; // 纵向块与块之间的距离
          },
        },
      };

      // 自定义节点
      G6.registerNode(
        'flow-rect',
        {
          shapeType: 'flow-rect',
          draw(cfg: any, group: any) {
            const {
              name = '',
              date,
              version,
              executor,
              owner,
              versionName = '',
              collapsed,
              status,
            } = cfg;

            // 节点样式
            const rectConfig = {
              width: 202,
              height: 150,
              lineWidth: 1,
              fontSize: 12,
              fill: '#fff',
              radius: 4,
              stroke: '#0077fa',
              opacity: 0.8,
            };
            // 添加按钮的位置,若都为0则原点在节点正上方的中点
            const nodeOrigin = {
              x: -rectConfig.width / 2, // 减往右下,加往左上
              y: -rectConfig.height / 2,
            };
            // 文字默认对齐样式
            const textConfig = {
              textAlign: 'left',
              textBaseline: 'bottom',
            };

            // 向分组添加新的图形
            const rect = group.addShape('rect', {
              attrs: {
                x: nodeOrigin.x,
                y: nodeOrigin.y,
                ...rectConfig,
              },
            });

            const baseOriginY = 28 + nodeOrigin.y;
            const marginTop = 24;
            // name
            group.addShape('text', {
              attrs: {
                ...textConfig,
                x: 12 + nodeOrigin.x,
                y: baseOriginY,
                text:
                  (name as string).length > 10
                    ? (name as string).substr(0, 10) + '...'
                    : name,
                fontSize: 16,
                opacity: 0.85,
                fill: '#000',
                cursor: 'pointer',
              },
              name: 'name-shape',
            });

            // versionName
            group.addShape('text', {
              attrs: {
                ...textConfig,
                textAlign: 'center',
                text:
                  (versionName as string).length > 10
                    ? (versionName as string).substr(0, 10) + '...'
                    : versionName,
                fontSize: 16,
                opacity: 0.85,
                fill: '#000',
                cursor: 'pointer',
              },
            });

            // collapse rect
            if (cfg.children && cfg.children.length) {
              group.addShape('rect', {
                attrs: {
                  x: rectConfig.width / 2 - 8,
                  y: -8,
                  width: 16,
                  height: 16,
                  stroke: 'rgba(0, 0, 0, 0.25)',
                  cursor: 'pointer',
                  fill: '#fff',
                  opacity: 0.8,
                },
                name: 'collapse-back',
                modelId: cfg.id,
              });

              // collpase text
              group.addShape('text', {
                attrs: {
                  x: rectConfig.width / 2,
                  y: -1,
                  textAlign: 'center',
                  textBaseline: 'middle',
                  text: collapsed ? '+' : '-',
                  fontSize: 16,
                  cursor: 'pointer',
                  fill: 'rgba(0, 0, 0, 0.25)',
                },
                name: 'collapse-text',
                modelId: cfg.id,
              });
            }
            const titleMap: IObj = {
              date: '日期',
              version: '版本',
              executor: '执行人',
              owner: '负责人',
            };
            const obj: IObj = {
              date,
              version,
              executor,
              owner,
            };
            Object.keys(obj).forEach((key: string, index: number) => {
              if (obj[key as keyof IObj]) {
                group.addShape('text', {
                  attrs: {
                    ...textConfig,
                    x: 12 + nodeOrigin.x,
                    y: baseOriginY + (index + 1) * marginTop,
                    text: `${titleMap[key as keyof IObj]}: ${
                      obj[key as keyof IObj]
                    }`,
                    fontSize: 14,
                    fill: '#000',
                    opacity: 0.85,
                  },
                });
              }
            });

            const rectBBox = rect.getBBox();
            // 底部边框
            group.addShape('rect', {
              attrs: {
                x: nodeOrigin.x,
                y: rectBBox.maxY - 4,
                width: rectBBox.width,
                height: 4,
                radius: [0, 0, 0, rectConfig.radius],
                fill: colors[status as keyof IColorType],
              },
            });
            return rect;
          },
          setState(name:any, value:any, item:any) { // 当外部调用 graph.setItemState时的响应函数
            if (name === 'collapse') {
              const group = item.getContainer();
              const collapseText = group.find((e:any) => e.get('name') === 'collapse-text');
              if (collapseText) {
                if (!value) {
                  collapseText.attr({
                    text: '-',
                  });
                } else {
                  collapseText.attr({
                    text: '+',
                  });
                }
              }
            }
          },
        },
        'rect',
      );

      graph = new G6.TreeGraph({
        container: ref.current,
        ...defaultConfig,
        ...config,
      });
    }

    graph.data(mockData);
    graph.render();
    graph.zoom(config.defaultZoom || 1);

    // 单击扩展卡片事件绑定
    graph.on('collapse-text:click', (e: G6GraphEvent) => {
      handleCollapse(e);
    });
    // 单击收起卡片事件绑定
    graph.on('collapse-back:click', (e: G6GraphEvent) => {
      handleCollapse(e);
    });
  }, []);
 
  return <div ref={ref}></div>;
};

export default DataLineAge;

最终效果

动图展示(视频转gif有点模糊,求好用gif工具推荐):

0.gif


参考:
AntV G6官网

手摸手使用G6实现(轻)图编辑应用系列-初识G6