注意:写文章的时候,我用的xflow版本是1.x的
像上面这种布局,
xflow或者x6是没有的,最接近的也只有deg,但是不满足。
上面的图有几个特点
- 布局是左右的,相当于横过来的树形
- 箭头的方向不是固定的,负责人那里箭头是指向回来的
- 注意负责人指向国资委的那根线,箭头不是连接在国资委这个节点上,而是连接在中间的线段上的
1. 思路
思路来自x6文档的思维导图,这里面关于layout、connector的使用很有帮助。
主要思路就是:
- 使用第三方布局算法,把节点的位置计算出来
- 配置
connector连接器,来自定义箭头方向,以及线段的走向,比如直角走多远。 - 将位置信息写入
graphData,渲染出来
2. 处理布局
有两种方式:
- 使用
xflow文档上的自定义布局
<XFlow
graphLayout={{
customLayout: async (graphData: NsGraph.IGraphData) => {
/** 自定义布局算法, 为每一个节点node赋予渲染所需的x,y属性 */
return graphData
}
}}
>
</XFlow>
2. 自己处理了graphData后赋值
我用的这个方法,因为要处理后端来的数据顺便就做了个布局。
2.1 布局库
先介绍一个布局算法库hierarchy,这个是antv的东西,里面有个树形布局compact-box,恰好满足我们的需求,这是g6自带的,但是xflow好像不支持。x6也有示例,也是用的这个库。
hierarchy没几个api,可以直接参考g6的文档。
// 使用方法很简单
import Hierarchy from '@antv/hierarchy';
// 这里的treeData就是一个树形的数据
// {id:xx,children: [{id: xx, children: [{id: xxx}]}, {id: xx}]}
const compactBoxLayout = Hierarchy.compactBox(treeData, {、
// 方向为水平
direction: 'H',
// 宽高为固定值
getHeight(d: any) {
return NODE_SIZE.height;
},
getWidth(d: any) {
return NODE_SIZE.width;
},
// 水平间距
getHGap() {
return 80;
},
// 垂直间距
getVGap() {
return 5;
},
// 节点位置
getSide: (node: any) => {
return node.data.side;
},
});
返回的就是一个包含节点x、y的对象
{
id: xxx,
x: x,
y: x
children: [
{
id: xxx,
x: x,
y: x
}
]
}
2.2 转化为图数据
有了节点位置信息,就可以把图的数据组合出来。
const graphData: NsGraph.IGraphData = { nodes: [], edges: [] };
// 递归遍历树,组合数据
const traverse = (parentItem: any) => {
if (parentItem) {
// 父节点
graphData.nodes.push({
id: parentItem.data.id,
renderKey: 'NODE',
x: parentItem.x,
y: parentItem.y,
width: NODE_SIZE.width,
height: NODE_SIZE.height,
label: parentItem.data.label,
nodeType: parentItem.data.nodeType,
side: parentItem.data.side,
children: parentItem.data.children,
// 插桩配置
ports: {
// 定义插桩位置配置
groups: {
left: {
position: 'left',
},
right: {
position: 'right',
},
},
//
items: [
{
id: 'left' + parentItem.data.id,
group: 'left',
attrs: {
// 隐藏插桩圆圈
circle: {
r: 0,
},
},
},
{
id: 'right' + parentItem.data.id,
group: 'right',
attrs: {
circle: {
r: 0,
},
},
},
],
},
});
// 如果有子节点,遍历子节点
if (parentItem.children) {
parentItem.children.forEach((child: any) => {
let source = parentItem.id;
let target = child.id;
let sourcePortId = child.data.side + parentItem?.data.id;
let targetPortId = (child.data.side === 'left' ? 'right' : 'left') + child.data.id;
// 配置边
graphData.edges.push({
id: nanoid(),
renderKey: 'EDGE',
// 节点源和目标
source,
target,
// 插桩源和目标
sourcePortId,
targetPortId,
attrs: {
line: {
// 翻转连线的时候,正常连线的箭头应该去掉
targetMarker: {
name: child.data.filpEdge ? '' : 'block',
},
},
},
// 自定义连接器,这个后面解释
connector: {
name: 'mindmap',
},
});
// 可以看到图中负责人那里是反过来的箭头
// 反转连线的情况加一根线,反向连接
// 为什么要翻转在连一根?
// 因为我这个箭头是在线段中间,直接设置offset不好控制
// 还不如再连一条线设置线段位置箭头就自然对了
if (child.data.filpEdge) {
// 翻转嘛,交换源和目标插桩
[sourcePortId, targetPortId] = [targetPortId, sourcePortId];
graphData.edges.push({
id: nanoid(),
source: child.id,
/**
* 本来目标节点就是父节点,但是负责人连接的时候不是直接连接在国资委节点上
* 而是连接在线段上的
* 所以这里需要判断一下
*
* 判断的思路就是,如果连接目标是根节点,就连接线段而不是插桩
*
* 当然这个思路是基于我的业务的,因为负责人这一个分支,只有负责人->国资委这个线段是这样连的
*/
target: parentItem.data.nodeType === 'primary' ? graphData.edges[graphData.edges.length - 1].id : parentItem.id,
sourcePortId,
targetPortId,
// 由于是连接在线段上,所以连接器也要切换成另外一种
// 连接器后面解释
connector: {
name: parentItem.data.nodeType === 'primary' ? 'mindmap2' : 'mindmap',
},
});
}
traverse(child);
});
}
}
};
traverse(compactBoxLayout);
// 最后的结果就是graphData保存的数据
这里面主要做了下面几件事:
- 遍历布局算法生成的布局树
- 根据节点关系,配置连线,并处理反向连接的情况
2.3. 自定义连接器connector
连接器是x6里面的概念,和路由有点混。
他们都是控制边渲染方式的配置。
对于路由来说,它是控制路径点 vertices来改变边的渲染,也就是这条边经过哪些点,是平滑过度还是垂直变化。
而连接器,是控制path元素的d属性,也就是怎么画出这条线。
我选择连接器实现的原因有下面几点:
- 自带的路由不能实现功能,最多这样
// getGraphConfig
//...
connecting: {
router: {
name: 'er',
args: {
min: 16,
direction: 'H',
offset: 'center',
},
},
},
//...
这个箭头有点问题
- 自定义路由对于我来说不直观
当然你也可以参考文档自定义路由。
2.3.1. 第一种连接器
第一种就是最普通的那种,父节点指向子节点。只不过弯折的时候是直角。
import { Graph } from '@antv/x6';
// 在createGraphConfig之前注册连接器
Graph.registerConnector(
'mindmap',
function (sourcePoint, targetPoint, routerPoints, options) {
// sourcePoint和this.sourceBBox有一点点区别
// 前者是加上了插桩的尺寸,后者只有节点尺寸
// 大部分情况下是一样的,只是看你自己需求
const { x: targetX } = this.targetBBox;
let { x: sourceX } = this.sourceBBox;
const targetData = this.targetView?.cell.getData();
// 间距就取的中间
const gap = (targetX - sourceX) / 2;
// d属性的值
// 不懂的可以简单复习一下svg的语法
const pathData = `
M ${sourceX} ${sourcePoint.y}
L ${sourceX + gap} ${sourcePoint.y}
L ${sourceX + gap} ${targetPoint.y}
L ${targetX} ${targetPoint.y}
`;
return pathData;
},
true,
);
这个路径大概的意思就是
2.3.2. 第二种连接器
第二种和第一种大同小异,第二种是反向连接的时候用的
Graph.registerConnector(
'mindmap2',
function (sourcePoint, targetPoint, routerPoints, options) {
const targetCell = this.targetView?.cell as X6Edge;
const gap = ((targetCell.getSourcePoint().x ?? 0) - (targetCell.getTargetPoint().x ?? 0)) / 2;
const pathData = `
M ${sourcePoint.x} ${sourcePoint.y}
L ${sourcePoint.x + gap} ${sourcePoint.y}
`;
return pathData;
},
true,
);