做大数据的项目,必不可少的是要接触到数据血缘图,它在大数据项目中有着很重要的作用。
之前在公司也做过一些案例,也看过很多友商的产品,阿里的DataWork,领英的Datahub,
datawork的血缘图使用的是 G6
,自家的产品
Datahub使用的是 爱彼邻的 可视化库 visx
本篇文章就来谈谈datahub中的血缘图。
点击此处链接你将看到 datahub中的血缘图,
由于是demo环境,数据有可能会被删掉,读者可以自行寻找。
该血缘图的特性如下
F12 节点的源码,发现使用的是SVG 实现的
标签的类前缀都是vx
,但直接搜没有搜到,于是去项目的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 构建的。
提前关键词,该库具有的特征
低级元素是说它不直接提供一个个完整的图表,而且要使用多个元素组装实现,这也意味着 要使用它,还是有一点门槛的,但人家的审美确实在线。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
因为这个库并不是一个专业的Graph库,所有在图的布局算法,自定义接的,自定义线,或者图的交互 都不如g6做的丰富。
选型还需慎重,依赖大量svg的api,标签。
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>
);
}