前言
大数据数据血缘是指数据产生的链路,直白点说,就是我们这个数据是怎么来的,经过了哪些过程和阶段。而我们要做的就是将这个过程通过可视化技术展现出来。
产品功能
这是一个企业级的大数据血缘可视化项目,目前支持功能如下。目前版本 V1.0.1。
- 支持解析 Hive sql 生成血缘图
- 支持字段级血缘与表级血缘切换展示
- 支持完整血缘与不完整血缘链路切换展示
- 支持血缘高亮显示
- 支持设置血缘高亮颜色
- 支持画布水印
- 支持画布拖拽、放大、缩小、自适应、视图居中显示
- 支持血缘图图片下载
- 支持小地图拖拽
- 编辑器支持编写 Sql,美化 Sql 功能
- 编辑器支持切换主题色
- 编辑器支持语法高亮
在线预览地址:openbytecode.com/openLineage…
踩坑记录
这里再分享下在项目开发的过程中遇到的一些问题,帮助大家在遇到类似的问题的时候该如何解决。
1. 渲染之后调用 fitView() 方法不生效
/**
* 渲染视图
*/
export const renderGraph = (graph: any, lineageData: any) => {
if (!graph || !lineageData) return;
graph.data(lineageData);
graph.render();
graph.fitView();
};
最后在 AntV G6 官网找到答案
g6.antv.antgroup.com/api/graph#g…
也就是说要在初始化 Graph 的时候设置 fitView 为 true 才生效
graphRef.current = new G6.Graph({
container: container || '',
width: width,
height: height,
plugins: [grid, minimap, toolbar],
fitView: true,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
},
});
2. 点击字段空白处没有触发事件
定义的事件如下,点击图中可触发事件区域可以触发事件,但是点击图中不可触发事件区域未能触发事件
// 监听节点点击事件
graph.off('node:click').on('node:click', (evt: any) => {
const { item, target } = evt;
const currentAnchor = target.get('name');
if (!currentAnchor) return;
if (fieldCheckedRef.current) {
handleNodeClick(graph, item, currentAnchor, 'highlight');
} else {
handleNodeClick(graph, item, currentAnchor, 'tableHighlight');
}
});
这个问题比较有意思,最后在也是通过对比官网案例最终找到了答案
官网案例:g6.antv.antgroup.com/examples/in…
经过对比发现:例子中的蓝色小圆是填充了蓝色,而我们的没有填充,所以猜测可能是我们的字段矩形没有填充东西,也就是说矩形是空的,所以监听不到事件。
填充之前的代码如下:
attrs.forEach((e: any, i: any) => {
const { key } = e;
// group部分图形控制
listContainer.addShape('rect', {
attrs: {
x: 0,
y: i * itemHeight + itemHeight,
width: width,
height: itemHeight,
cursor: 'pointer',
},
name: key,
draggable: true,
});
});
给矩形填充白色
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,
});
});
正如我们猜测的那样,填充颜色之后就能够监听到事件了。
3. 使用 dagre 布局有一些表局部有重叠和间距不一致问题
最终使用 G6 自定义布局解决了该问题,自定义布局代码如下:
class CustomDagreLayout extends Base {
/** 布局的起始(左上角)位置 */
public begin: number[] = [0, 0];
/** 节点水平间距(px) */
public nodesep: number = 50;
/** 每一层节点之间间距 */
public ranksep: number = 50;
constructor(options?: DagreLayoutOptions) {
super();
this.updateCfg(options);
}
public getDefaultCfg() {
return {
nodesep: 50, // 节点水平间距(px)
ranksep: 50, // 每一层节点之间间距
begin: [0, 0], // 布局的起点位置
};
}
/**
* 执行布局
*/
public execute() {
const self = this;
const { nodes, edges, ranksep, nodesep, begin } = self;
if (!nodes) return;
const layerMap: Map<number, Node[]> = new Map();
nodes.forEach((item: any, index, arr) => {
if (!layerMap.has(item.level)) {
layerMap.set(
item.level,
arr.filter((node: any) => node.level === item.level)
);
}
});
const startX = begin[0];
const startY = begin[1];
const size = layerMap.size;
const maxWidth = size * nodeWidth + (size - 1) * ranksep;
const hr = Array.from(layerMap.values()).map((list: any[]) => {
const sum = list.reduce((pre: any, curr: any) => {
return pre + curr.size[1];
}, 0);
return sum + (list.length - 1) * nodesep;
});
const maxHeight = Math.max(...hr);
const offsetX = startX + maxWidth;
const offsetY = startY + maxHeight;
const centerLine = offsetY - maxHeight / 2;
layerMap.forEach((value, key) => {
let d = key === maxLevel ? size - 1 : key;
const x = offsetX - d * (nodeWidth + ranksep);
const y = centerLine + hr[d] / 2;
const sortNodes = value.sort((x: any, y: any) => y.order - x.order);
let preY = y;
sortNodes.forEach((e: any, index) => {
const { size } = e;
const margin = index === 0 ? 0 : nodesep;
preY = preY - size[1] - margin;
e.x = x;
e.y = preY;
});
});
if (self.onLayoutEnd) self.onLayoutEnd();
}
public getType() {
return 'lineageLayout';
}
}
export default CustomDagreLayout;
自定义布局效果如下: