Open-Lineage

一个企业级的大数据血缘系统,支持 Hive 和 Mysql,前端功能丰富,包括模型放大缩小、下载图片、小地图、文字水印,还支持字段级血缘和完整血缘的切换,另外还支持切换 Sql 编辑区的主题色,修改文字水印,修改线条高亮颜色等功能。

实现细节

这里分享下具体的实现细节,总共有以下几点

  1. 如何实现高亮

  2. 接口数据如何转化为图数据

  3. 表级血缘和字段血缘切换,完整链路和不完整链路切换

接下来我会对每个技术的实现过程进行具体说明

1. 如何实现高亮

1.1 定位事件触发字段

鼠标点击 table 字段,如何定位点击的是那个字段?

解决思路:利用图元的 name 属性,AntV G6 官网建议每个图元都加上 name 属性,name 的值是一个字符串,

所以我们可以在构建表字段的时候为 name 赋上我们的字段名

if (attrs) {
  attrs.forEach((e: any, i: any) => {
    const { key } = e;
    // group部分图形控制
    listContainer.addShape('rect', {
      attrs: {
        x: 0,
        y: i * itemHeight + itemHeight,
        fill: '#ffffff',
        width: width,
        height: itemHeight,
        cursor: 'pointer',
      },
      name: key,
      draggable: true,
    });

    // group文本控制
    listContainer.addShape('text', {
      attrs: {
        x: fontOffsetX,
        y: (i + 1) * itemHeight + fontOffsetY,
        text: handleLabelLength(key),
        fontSize: fontSize,
        fill: '#000',
        fontWeight: 500,
        cursor: 'pointer',
      },
      name: key,
    });
  });
}

上面仅为代码片段,完整请看 https://github.com/lijunping365/Open-Lineage-Web,

注意 name 的值即可。这样就可以在事件触发时定位到触发的是哪个字段了

// 监听节点点击事件
graph.off('node:click').on('node:click', (evt: any) => {
  console.log('node:click');
  const { item, target } = evt;
  const name = target.get('name');
  if (!name) return;

  if (fieldCheckedRef.current) {
    handleNodeEvent(graph, item, name);
  } else {
    handleNodeTableEvent(graph, item, name);
  }
});

1.2 寻找触发字段的所有来源和目标

上面我们已经定位到触发字段,接着我们需要根据定位到的字段寻找它的上下游字段及连线以实现高亮,

我们可以顺着连线去找,我们知道 edge 都有来源和目标,所以可以分为左右两个方向去找

/**
 * 获取选中 label 的所有左关联边
 * @param edges node 的所有 edges
 * @param model node 的 model
 * @param sourceAnchor 选中的 label
 * @param leftActiveEdges 左关联边集合
 */
export const getLeftRelation = (
  edges: any[],
  model: any,
  sourceAnchor: any,
  leftActiveEdges: any[]
) => {
  const source = model['id']; // 当前节点
  edges
    .filter((edge: any) => !leftActiveEdges.includes(edge))
    .forEach((edge: any) => {
      if (
        edge.getModel()['target'] === source &&
        edge.getModel()['targetAnchor'] === sourceAnchor
      ) {
        leftActiveEdges.push(edge);

        const currentNode = edge.getSource();
        const currentModel = currentNode.getModel();
        const currentEdges = currentNode.getInEdges();
        const currentSourceAnchor = edge.getModel()['sourceAnchor'];
        getLeftRelation(
          currentEdges,
          currentModel,
          currentSourceAnchor,
          leftActiveEdges
        );
      }
    });
};

/**
 * 获取选中 label 的所有右关联边
 * @param edges node 的所有 edges
 * @param model node 的 model
 * @param sourceAnchor 选中的 label
 * @param rightActiveEdges 右关联边集合
 */
export const getRightRelation = (
  edges: any[],
  model: any,
  sourceAnchor: any,
  rightActiveEdges: any[]
) => {
  const source = model['id']; // 当前节点
  edges
    .filter((edge: any) => !rightActiveEdges.includes(edge))
    .forEach((edge: any) => {
      if (
        edge.getModel()['source'] === source &&
        edge.getModel()['sourceAnchor'] === sourceAnchor
      ) {
        rightActiveEdges.push(edge);

        const currentNode = edge.getTarget();
        const currentModel = currentNode.getModel();
        const currentEdges = currentNode.getOutEdges();
        const currentTargetAnchor = edge.getModel()['targetAnchor'];
        getRightRelation(
          currentEdges,
          currentModel,
          currentTargetAnchor,
          rightActiveEdges
        );
      }
    });
};

1.3 对寻找到的所有字段和连线进行高亮

在上一步中,我们找到了事件触发的字段以及字段的所有来源和目标字段及连线,接下来就是对找到的这些字段和连线进行高亮

字段高亮:将字段字体加粗

连线高亮:将连线颜色改为其他颜色

要实现高亮,那就要动态修改元素的样式(状态),通过阅读 AntV G6 的文档,知道了通过动态设置元素的 state 可以实现我们想要的效果

关键点:在修改状态的时候要拼接上要高亮的字段,这样在处理高亮的时候就可以知道要操作哪个字段了

/**
 * 设置左边关联节点及边状态
 * @param graph
 * @param edges 连线
 * @param color 连线高亮颜色
 * @param name 状态名称
 */
export const setLeftStats = (
  graph: any,
  edges: any[],
  color: string,
  name: string
) => {
  if (!graph) return;
  edges.forEach(function (edge: any) {
    graph.setItemState(edge, `highlight-${color}`, true);
    edge.toFront();

    const sourceAnchor = edge.getModel()['sourceAnchor'];
    graph.setItemState(edge.getSource(), name + '-' + sourceAnchor, true);
  });
};

处理字段高亮代码如下,在设置高亮时截取到高亮字段,找到高亮元素进行高亮即可,注意这里是通过 keyShape 去查找的

setState(name, value, item: any) {
  // 字段高亮
  if (name && name.startsWith('highlight')) {
    const anchor = name.split('-')[1];
    const shape = item.get('keyShape');
    // 查找 label 下标
    const anchorIndex = item.getModel().attrs.findIndex((e: any) => e.key === anchor);
    // 查找 label 元素,通过下标来找
    const label = shape.get('parent').get('children')[3].get('children')[
    anchorIndex * 2 + 1
      ];

    if (value) {
      //label.attr('fill', '#A3B1BF');
      //label.attr('fill', 'red');
      label.attr('fontWeight', 800);
    } else {
      //label.attr('fill', '#A3B1BF');
      //label.attr('fill', 'red');
      label.attr('fontWeight', 500);
    }
  }
}

处理连线高亮代码如下:

/**
 * 设置状态,主要用于高亮
 * @param name 状态
 * @param value true | false
 * @param item 要改变状态的边
 */
setState(name, value, item: any) {
  const shape = item.get('keyShape');
  // 字段连线高亮或表连线高亮
  if (name && name.startsWith('highlight')) {
    const highlightColor = name.split('-')[1];
    if (value) {
      //shape.attr('opacity', 0.2);

      shape.attr('stroke', highlightColor);
      shape.attr('lineWidth', 3);
    } else {
      //shape.attr('opacity', 1);

      shape.attr('stroke', '#6C6B6B');
      shape.attr('lineWidth', 2);
    }
  }
}

2. 接口数据转化为图数据

2.1 接口数据如下,我简单说明下每个字段的含义

targetField:目标,对应图的 target

refFields:来源:对应图的 source

一个目标可以有多个来源

fieldName: 数据库、表、字段使用 “.“ 分隔

level:层次布局层级

index:同一层的 order

final:是否是最后一层,true 最后一层,false:不是最后一层

[
  {
    "refFields": [
      {
        "fieldName": "default._u1.cal_date",
        "final": false,
        "index": 0,
        "level": 1
      }
    ],
    "targetField": {
      "fieldName": "dws.dws_comm_shop_linkshop_da.cal_date",
      "final": false,
      "index": 0,
      "level": 0
    }
  },
  {
    "refFields": [
      {
        "fieldName": "default._u1.brand_code",
        "final": false,
        "index": 0,
        "level": 1
      }
    ],
    "targetField": {
      "fieldName": "dws.dws_comm_shop_linkshop_da.brand_code",
      "final": false,
      "index": 0,
      "level": 0
    }
  }
]

2.2 图由节点(Node)和连线(Edge)组成,节点和连线的数据结构分别如下

图节点数据结构如下,id、key、label、x、y、attrs、size 是节点必须要有的属性,level 和 order 是我们自定义的属性,

level 用在实现 table 不同颜色,order 用在自定义布局

{
  "id": key,
  "key": key,
  "label": key,
  "x": 100,
  "y": 100,
  "level": level,
  "order": order,
  "attrs": attrs,
  "size": [400, height]
}

图连线数据结构如下

{
  "source": sourceName,
  "sourceAnchor":sourceAnchor,
  "target": targetName,
  "targetAnchor" : targetAnchor,
  "label": ref.label
}

2.3 数据转换

具体代码在 /utils/common.ts 中,有兴趣的可以看下

3. 表级血缘和字段血缘切换,完整链路和不完整链路切换

这个其实比较简单,思路对了就很好解决,比如表级血缘和字段血缘切换,其实就是在展示字段血缘的时候把节点的 attrs 属性的值置为空数组即可

不完整链路需要接口返回。