D3和X6
D3
版本
d3已经更新到v7版本,中文文档只更新到v4版本,存在部分api不适用和过时问题
使用d3-darge
插件布局,插件适配d3版本为v5,近年未更新
API
使用darge中setNode
和setEdge
绘制node和edge
使用d3中selection
和zoom
函数实现元素选择和缩放拖拽功能
使用darge的render
方法绘制svg,selection的on
事件对元素进行监听和方法执行
UI交互
使用element-ui的dropmenu
实现下拉菜单功能,需功能和样式订制,代码修改
提示框使用css和d3animation
实现
所有的弹出层样式(坐标)需要手动计算
增删改查
每次做出修改需要手动重绘页面,新添加node和edge需要携带完整配置,否则使用默认样式等
重绘后新添加元素手动监听各种事件,无法使用事件委托机制,或使用复杂
其他
拖拽元素、箭头等功能需要d3原生api进行大量算法编程,无现成api提供使用,写法繁琐复杂
流程分组、定制等逻辑需要自行开发,无法避开
X6
与G6的对比
使用场景 | 性能 | 定制化和学习 | |
---|---|---|---|
X6 | 偏重于图编辑,节点拖拽、锚点拖拽、编辑形状等 | 基于svg创建dom元素,节点或编辑元素过多会影响性能 | 基于svg(html),定制组件便利 |
G6 | 偏重于图可视化,分析数据(GIS)等 | 基于canvas,大量数据依然流畅渲染 | 深入了解canvas,不便于调试 |
具体功能实现
创建node和edge
- d3
createNode(id, node) {
this.g.setNode(id, {
...node,
rx: 5,
ry: 5
});
},
createEdge(source, target, edge) {
this.g.setEdge(source, target, {
...edge,
arrowhead: "vee",
curve: d3.curveBasis
});
},
// style
g {
&.edgeLabel {
user-select: none;
cursor: pointer;
.high-light {
fill: #c4e3b6;
}
}
&.edgePath {
.path {
stroke: #333;
stroke-width: 2px;
}
.high-light {
stroke: #c4e3b6;
}
marker path {
fill: #a3a3a3;
stroke: #5d5d5d;
&.high-light {
fill: #c4e3b6;
}
}
}
&.node {
user-select: none;
cursor: pointer;
fill: #fff;
stroke: #ddd;
&.high-light {
stroke: #c4e3b6;
stroke-width: 2px;
}
text {
fill: #4a4a4a;
stroke: none;
font-size: 16px;
font-weight: 600;
}
}
}
- x6
formatNode(id: string, node: Node.Metadata) {
return {
id,
...node,
attrs: {
body: {
fill: "#5f95ff",
stroke: "transparent",
rx: 5,
ry: 5
},
label: {
fill: "#ffffff"
}
}
};
},
formatEdge(source: string, target: string, edge: Edge.Metadata) {
return {
source,
target,
attrs: {
line: {
stroke: "#a2b1c3",
strokeWidth: 2
}
},
labels: [
{
markup: [
{
tagName: "rect",
selector: "body"
},
{
tagName: "text",
selector: "label"
}
],
attrs: {
label: {
cursor: "pointer",
text: edge.label,
textAnchor: "middle",
textVerticalAnchor: "middle",
fontSize: 14,
fill: "#8d939b"
},
body: {
cursor: "pointer",
ref: "label",
refX: "-20%",
refY: "-20%",
refWidth: "140%",
refHeight: "140%",
fill: "#d3d3d3",
stroke: "#a2b1c3",
strokeWidth: 1
}
}
}
]
};
},
修改node和edge
- d3的增删改查需要自己去写逻辑(使用form表单提交),再去重绘svg,事件监听需要重新加在每一个node和edge上
- x6可以使用封装好的
tools
,事件监听也是使用委托代理到graph上,自动更新视图
// 鼠标移入节点显示
this.g.on("node:mouseenter", ({ e, node, view }) => {
node.addTools({
name: "button-remove",
args: {
x: 0,
y: 0,
offset: { x: 10, y: 10 }
}
});
});
// 鼠标移出节点显示
this.g.on("node:mouseleave", ({ node }) => {
node.removeTools();
});
// 双击节点修改label
this.g.on("node:dblclick", ({ node, e }) => {
node.addTools({
name: "node-editor",
args: {
event: e
}
});
});
}
x6在使用editor-tool时,setText有天坑,源码也会有些bug,需优化
// node
setText: (args: any) => {
(
CellEditor.NodeEditor.getDefaults() as {
setText: (args: any) => void;
}
).setText(args);
}
// edge
// 首先明确edge只存在一个label,有label时事件只作用于label上
if (edge.labels.length && e.target.nodeName === "path") {
return;
}
// 再优化重写setText逻辑
setText: ({
cell,
value,
index
}: {
cell: Edge;
value: string;
index?: number;
}) => {
if (index === -1) {
if (value) {
cell.appendLabel({
position: {
distance: 0.5
},
...this.edgeLabelMarkUp(value)
});
}
} else {
if (value) {
cell.prop(`labels/${index}/attrs/label/text`, value);
} else if (typeof index === "number") {
cell.removeLabelAt(index);
}
}
}
使用element UI插件
x6对react的ant-design框架提供了非常友好的支持,可以安装@antv/x6-react-components
开箱即用,无需写适配逻辑和样式。在vue的elementUI中,使用了d3一样的做法,引入UI组件后进行样式、功能二次开发
<el-dropdown ref="menuDown" trigger="click" placement="bottom">
<div></div>
<el-dropdown-menu class="x6-down-menu" slot="dropdown">
<el-dropdown-item
v-for="(item, index) in dropdown"
:key="index"
:icon="item.icon"
:command="item.command"
>{{ item.label }}</el-dropdown-item
>
</el-dropdown-menu>
</el-dropdown>
使用ref定位到组件后,手动调用show()
和hide()
方法,再加上改变横竖坐标定位
// d3中计算坐标,鼠标坐标通过d3.mouse获取
const svgGroup = this.svgGroup.node();
const mouse = d3.mouse(svgGroup);
const transform = d3.zoomTransform(svgGroup);
tooltip
.html(content)
.style("left", mouse[0] * transform.k + transform.x + 15 + "px")
.style("top", mouse[1] * transform.k + transform.y + 15 + "px");
// x6中计算坐标,鼠标坐标通过监听事件参数获取
// 鼠标右键点击画布
this.g.on("blank:contextmenu", ({ e, x, y }) => {
this.showMenu(x, y);
});
showMenu(x: number, y: number) {
console.log(x, y);
console.log(this.g.scale());
console.log(this.g.translate());
const { tx, ty } = this.g.translate();
const { sx, sy } = this.g.scale();
(this.$refs.menuDown as any).show();
const menu = (this.$refs.menuDown as any).popperElm;
setTimeout(() => {
menu.setAttribute("x-placement", "bottom-start");
menu.style.left = x * sx + tx + 30 + "px";
menu.style.top = y * sy + ty + 20 + "px";
}, 0);
},
小地图插件
// new Graph添加配置
const miniMap = this.$refs.miniMap as HTMLElement;
minimap: {
enabled: true,
container: miniMap,
height: 150,
width: 250
}
// 添加dom
<div class="mini-map" ref="miniMap"></div>
// style
.mini-map {
position: absolute;
bottom: 20px;
right: 36px;
.x6-widget-minimap {
background-color: #d8e7cc;
.x6-graph {
box-shadow: 0 0 4px 0 #7ca190;
}
}
}
使用拖拽插件
addon/stencil
// stencil配置
// 一定要配置stencilGraphWidth和stencilGraphHeight,否则初次渲染不出来
// getDragNode配置拖拽中节点,getDropNode配置放下后节点
const stencil = new Addon.Stencil({
title: "新建节点",
target: this.g,
search: (cell, key) => {
return (
cell.shape.indexOf(key) !== -1 ||
(cell.attr("text/text") as String).indexOf(key) !== -1
);
},
placeholder: "搜索节点",
notFoundText: "404",
stencilGraphWidth: 300,
stencilGraphHeight: 100,
groups: [
{
name: "node",
collapsable: false
}
],
// getDragNode: (node) => {
// return node.clone();
// },
getDropNode: (node) => {
const shape = node.shape;
const nodeInfo = { label: node.attrs!.text.text };
if (shape === "rect") {
Object.assign(nodeInfo, {
width: 80,
height: 48,
shape: "rect"
});
} else if (shape === "circle") {
Object.assign(nodeInfo, {
width: 80,
height: 80,
shape: "circle"
});
}
return this.g.createNode(this.formatNode(nodeInfo));
}
});
(this.$refs.stencil as HTMLElement).appendChild(stencil.container);
const rect = new Shape.Rect({
width: 70,
height: 40,
label: "rect",
attrs: {
body: {
fill: "#5f95ff",
stroke: "transparent",
rx: 5,
ry: 5
},
label: {
fill: "#ffffff"
}
}
});
const circle = new Shape.Circle({
width: 80,
height: 80,
label: "circle",
attrs: {
body: {
fill: "#5f95ff",
stroke: "transparent"
},
label: {
fill: "#ffffff"
}
}
});
stencil.load([rect, circle], "node");
// 需要添加dom和样式
<div class="stencil" ref="stencil"></div>
// style
.stencil {
flex: 1;
height: 100%;
position: relative;
margin-right: 20px;
}
监听node:added
事件,在新添加节点上添加链接桩、可移动等属性
this.g.on("node:added", ({ node }) => {
node.setData({ disableMove: true });
this.setNodePort(node);
});
链接桩
要自定义链接桩位置(top\bottom\left\right等),只能使用group方式来创建制定position,attrs无法通过groups复用。属于可以优化但没做处理的功能,其实用到地方还很多
const ports = {
groups: {
top: {
position: "top",
attrs: {
circle: {
r: 5,
magnet: true,
stroke: "#31d0c6",
strokeWidth: 2,
fill: "#fff"
}
}
},
left: {
position: "left",
attrs: {
circle: {
r: 5,
magnet: true,
stroke: "#31d0c6",
strokeWidth: 2,
fill: "#fff"
}
}
},
right: {
position: "right",
attrs: {
circle: {
r: 5,
magnet: true,
stroke: "#31d0c6",
strokeWidth: 2,
fill: "#fff"
}
}
},
bottom: {
position: "bottom",
attrs: {
circle: {
r: 5,
magnet: true,
stroke: "#31d0c6",
strokeWidth: 2,
fill: "#fff"
}
}
}
},
items: [
{
id: node.id + "port-top",
group: "top"
},
{
id: node.id + "port-bottom",
group: "bottom"
},
{
id: node.id + "port-left",
group: "left"
},
{
id: node.id + "port-right",
group: "right"
}
]
};
node.prop("ports", ports);
链接桩配置
connecting: {
snap: {
radius: 30
},
allowBlank: false,
allowMulti: false,
allowLoop: false,
allowEdge: false,
highlight: true
},
// 对应connecting中highlight配置
highlighting: {
magnetAvailable: {
name: "stroke",
args: {
padding: 4,
attrs: {
"stroke-width": 2,
stroke: "#FE854F"
}
}
}
},