datahub 中血缘图的实现分析,在react中使用airbnb的visx可视化库来画有向无环图

2023-10-26 15:16:19 浏览数 (2)

背景

做大数据的项目,必不可少的是要接触到数据血缘图,它在大数据项目中有着很重要的作用。 之前在公司也做过一些案例,也看过很多友商的产品,阿里的DataWork,领英的Datahub, datawork的血缘图使用的是 G6,自家的产品 Datahub使用的是 爱彼邻的 可视化库 visx 本篇文章就来谈谈datahub中的血缘图。

查看源码

点击此处链接你将看到 datahub中的血缘图,

由于是demo环境,数据有可能会被删掉,读者可以自行寻找。

该血缘图的特性如下

  • 上下游
  • 自定义节点
  • 节点可点击,操作
  • 线的样式有多种
  • 鼠标放置线上有辅助信息
  • 可以展开上下游
  • 最基本的放大,缩小视图

F12 节点的源码,发现使用的是SVG 实现的

标签的类前缀都是vx,但直接搜没有搜到,于是去项目的package.json中寻找使用的库。

查看package.json

在项目中 找到了答案 https://github.com/datahub-project/datahub/blob/master/datahub-web-react/package.json

使用的是 https://airbnb.io/visx

github 地址 https://github.com/airbnb/visx visx 是一个为 React 应用程序提供可视化功能的库。它提供了一系列低级可视化元素或组件,被称为 expressive, low-level visualization primitives,这些元素或组件可以用于创建各种可视化效果,例如饼图等。使用 VISX 可以方便地将设计元素添加到 React 应用程序中。它是由 Airbnb 构建的。

提前关键词,该库具有的特征

  • 为react
  • 低级元素
  • 可视化

低级元素是说它不直接提供一个个完整的图表,而且要使用多个元素组装实现,这也意味着 要使用它,还是有一点门槛的,但人家的审美确实在线。visx的gallery 都很美观。让人看了就像用,但一用就头大,提供的api太底层了。

大家看几个官网的示例

查看组件源码

上面介绍了一下 visx库,我们回到datahub这个项目 血缘图 都放在https://github.com/datahub-project/datahub/blob/master/datahub-web-react/src/app/lineage这个目录

节点组件 https://github.com/datahub-project/datahub/blob/master/datahub-web-react/src/app/lineage/LineageEntityNode.tsx

visx库文档

因为这个库并不是一个专业的Graph库,所有在图的布局算法,自定义接的,自定义线,或者图的交互 都不如g6做的丰富。 选型还需慎重,依赖大量svg的api,标签。

案例

最后提供一个 使用visx 画的一个 Graph案例

代码语言:javascript复制
import React, { useState } from 'react';
import { Group } from '@visx/group';
import { hierarchy, Tree } from '@visx/hierarchy';
import { LinearGradient } from '@visx/gradient';
import { pointRadial } from 'd3-shape';
import useForceUpdate from './useForceUpdate';
import LinkControls from './LinkControls';
import getLinkComponent from './getLinkComponent';

interface TreeNode {
  name: string;
  isExpanded?: boolean;
  children?: TreeNode[];
}

const data: TreeNode = {
  name: 'T',
  children: [
    {
      name: 'A',
      children: [
        { name: 'A1' },
        { name: 'A2' },
        { name: 'A3' },
        {
          name: 'C',
          children: [
            {
              name: 'C1',
            },
            {
              name: 'D',
              children: [
                {
                  name: 'D1',
                },
                {
                  name: 'D2',
                },
                {
                  name: 'D3',
                },
              ],
            },
          ],
        },
      ],
    },
    { name: 'Z' },
    {
      name: 'B',
      children: [{ name: 'B1' }, { name: 'B2' }, { name: 'B3' }],
    },
  ],
};

const defaultMargin = { top: 30, left: 30, right: 30, bottom: 70 };

export type LinkTypesProps = {
  width: number;
  height: number;
  margin?: { top: number; right: number; bottom: number; left: number };
};

export default function Example({
  width: totalWidth,
  height: totalHeight,
  margin = defaultMargin,
}: LinkTypesProps) {
  const [layout, setLayout] = useState<string>('cartesian');
  const [orientation, setOrientation] = useState<string>('horizontal');
  const [linkType, setLinkType] = useState<string>('diagonal');
  const [stepPercent, setStepPercent] = useState<number>(0.5);
  const forceUpdate = useForceUpdate();

  const innerWidth = totalWidth - margin.left - margin.right;
  const innerHeight = totalHeight - margin.top - margin.bottom;

  let origin: { x: number; y: number };
  let sizeWidth: number;
  let sizeHeight: number;

  if (layout === 'polar') {
    origin = {
      x: innerWidth / 2,
      y: innerHeight / 2,
    };
    sizeWidth = 2 * Math.PI;
    sizeHeight = Math.min(innerWidth, innerHeight) / 2;
  } else {
    origin = { x: 0, y: 0 };
    if (orientation === 'vertical') {
      sizeWidth = innerWidth;
      sizeHeight = innerHeight;
    } else {
      sizeWidth = innerHeight;
      sizeHeight = innerWidth;
    }
  }

  const LinkComponent = getLinkComponent({ layout, linkType, orientation });

  return totalWidth < 10 ? null : (
    <div>
      <LinkControls
        layout={layout}
        orientation={orientation}
        linkType={linkType}
        stepPercent={stepPercent}
        setLayout={setLayout}
        setOrientation={setOrientation}
        setLinkType={setLinkType}
        setStepPercent={setStepPercent}
      />
      <svg width={totalWidth} height={totalHeight}>
        <LinearGradient id="links-gradient" from="#fd9b93" to="#fe6e9e" />
        <rect width={totalWidth} height={totalHeight} rx={14} fill="#272b4d" />
        <Group top={margin.top} left={margin.left}>
          <Tree
            root={hierarchy(data, (d) => (d.isExpanded ? null : d.children))}
            size={[sizeWidth, sizeHeight]}
            separation={(a, b) => (a.parent === b.parent ? 1 : 0.5) / a.depth}
          >
            {(tree) => (
              <Group top={origin.y} left={origin.x}>
                {tree.links().map((link, i) => (
                  <LinkComponent
                    key={i}
                    data={link}
                    percent={stepPercent}
                    stroke="rgb(254,110,158,0.6)"
                    strokeWidth="1"
                    fill="none"
                  />
                ))}

                {tree.descendants().map((node, key) => {
                  const width = 40;
                  const height = 20;

                  let top: number;
                  let left: number;
                  if (layout === 'polar') {
                    const [radialX, radialY] = pointRadial(node.x, node.y);
                    top = radialY;
                    left = radialX;
                  } else if (orientation === 'vertical') {
                    top = node.y;
                    left = node.x;
                  } else {
                    top = node.x;
                    left = node.y;
                  }

                  return (
                    <Group top={top} left={left} key={key}>
                      {node.depth === 0 && (
                        <circle
                          r={12}
                          fill="url('#links-gradient')"
                          onClick={() => {
                            node.data.isExpanded = !node.data.isExpanded;
                            console.log(node);
                            forceUpdate();
                          }}
                        />
                      )}
                      {node.depth !== 0 && (
                        <rect
                          height={height}
                          width={width}
                          y={-height / 2}
                          x={-width / 2}
                          fill="#272b4d"
                          stroke={node.data.children ? '#03c0dc' : '#26deb0'}
                          strokeWidth={1}
                          strokeDasharray={node.data.children ? '0' : '2,2'}
                          strokeOpacity={node.data.children ? 1 : 0.6}
                          rx={node.data.children ? 0 : 10}
                          onClick={() => {
                            node.data.isExpanded = !node.data.isExpanded;
                            console.log(node);
                            forceUpdate();
                          }}
                        />
                      )}
                      <text
                        dy=".33em"
                        fontSize={9}
                        fontFamily="Arial"
                        textAnchor="middle"
                        style={{ pointerEvents: 'none' }}
                        fill={node.depth === 0 ? '#71248e' : node.children ? 'white' : '#26deb0'}
                      >
                        {node.data.name}
                      </text>
                    </Group>
                  );
                })}
              </Group>
            )}
          </Tree>
        </Group>
      </svg>
    </div>
  );
}

题外话

开源项目 openmatedata 血缘图使用的react-flow,节点,线都是使用div画的。大数据量时,可能堪忧

0 人点赞