1. g6有combo,x6有 parent.addChild(child),实现combo效果,即node外面的框子;
2. g6有tooltip插件,x6用 删除 node,removeNode(node.id),新增 node graph.addNode(node),实现
共同的:er图布局,必须经过计算(暂未实现)
下面是在 X6 官网调试的demo 代码
import { Graph } from ‘@antv/x6’
import dagre from ‘dagre’
import { DagreLayout } from ‘@antv/layout’
const LINE_HEIGHT = 24
const NODE_WIDTH = 150
let nodes = [
{
id: 1,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 1
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #211
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 2,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 2
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #422
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 3,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 3
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #633
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 4,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 4
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #844
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 5,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 5
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #1055
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 6,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 6
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #1266
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 7,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 7
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #1477
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 8,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 8
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #1688
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
},
{
id: 9,
shape: “er-rect”,
width: 60,
height: 40,
label: Child - 9
,
zIndex: 1,
attrs: {
body: {
stroke: ‘none’,
fill: #1899
,
cursor: “auto”,
},
label: {
fill: ‘#fff’,
fontSize: 12,
cursor: “auto”,
},
},
}
]
let edges = [
{
“id”: “edge1”,
“shape”: “edge”,
“source”: {
“cell”: “1”,
},
“target”: {
“cell”: “2”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
},
{
“id”: “edge2”,
“shape”: “edge”,
“source”: {
“cell”: “3”,
},
“target”: {
“cell”: “4”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
},
{
“id”: “edge3”,
“shape”: “edge”,
“source”: {
“cell”: “5”,
},
“target”: {
“cell”: “4”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
},
{
“id”: “edge4”,
“shape”: “edge”,
“source”: {
“cell”: “4”,
},
“target”: {
“cell”: “6”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
},
{
“id”: “edge5”,
“shape”: “edge”,
“source”: {
“cell”: “6”,
},
“target”: {
“cell”: “7”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
},
{
“id”: “edge6”,
“shape”: “edge”,
“source”: {
“cell”: “7”,
},
“target”: {
“cell”: “8”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
},
{
“id”: “edge7”,
“shape”: “edge”,
“source”: {
“cell”: “7”,
},
“target”: {
“cell”: “9”,
},
“attrs”: {
“line”: {
“stroke”: “#A2B1C3”,
“strokeWidth”: 2
}
},
“zIndex”: 1
}
]
let parentNode = {
id: ‘parent’,
x: 10,
y: 10,
width: 20,
height: 20,
zIndex: 0,
label: ‘Parent’,
attrs: {
body: {
fill: ‘#fffbe6’,
stroke: ‘#ffe7ba’,
cursor: “pointer”,
},
label: {
fontSize: 12,
cursor: “pointer”,
},
text: {
refX: 25,
refY: 10,
},
},
}
Graph.registerPortLayout(
‘erPortPosition’,
(portsPositionArgs) => {
return portsPositionArgs.map((_, index) => {
return {
position: {
x: 0,
y: (index + 1) * LINE_HEIGHT,
},
angle: 0,
}
})
},
true,
)
Graph.registerNode(
‘er-rect’,
{
inherit: ‘rect’,
markup: [
{
tagName: ‘rect’,
selector: ‘body’,
},
{
tagName: ‘text’,
selector: ‘label’,
},
],
attrs: {
rect: {
strokeWidth: 1,
stroke: ‘#5F95FF’,
fill: ‘#5F95FF’,
},
label: {
fontWeight: ‘bold’,
fill: ‘#ffffff’,
fontSize: 12,
},
},
ports: {
groups: {
list: {
markup: [
{
tagName: ‘rect’,
selector: ‘portBody’,
},
{
tagName: ‘text’,
selector: ‘portNameLabel’,
},
{
tagName: ‘text’,
selector: ‘portTypeLabel’,
},
],
attrs: {
portBody: {
width: NODE_WIDTH,
height: LINE_HEIGHT,
strokeWidth: 1,
stroke: ‘#5F95FF’,
fill: ‘#EFF4FF’,
magnet: true,
},
portNameLabel: {
ref: ‘portBody’,
refX: 6,
refY: 6,
fontSize: 10,
},
portTypeLabel: {
ref: ‘portBody’,
refX: 95,
refY: 6,
fontSize: 10,
},
},
position: ‘erPortPosition’,
},
},
},
},
true,
)
Graph.registerNode(
“tooltip-node”,
{
width: 600,
height: 450,
x: 200,
y: 50,
attrs: {
body: {
stroke: “#5F95FF”,
strokeWidth: 1,
fill: “rgba(204,204,204,0.2)”,
refWidth: 1,
refHeight: 1,
},
title: {
text: “Node”,
refX: 40,
refY: 14,
fill: “rgba(0,0,0,0.85)”,
fontSize: 12,
“text-anchor”: “start”,
},
text: {
text: “this is content text”,
refX: 40,
refY: 38,
fontSize: 12,
fill: “rgba(0,0,0,0.6)”,
“text-anchor”: “start”,
},
},
markup: [
{
tagName: “rect”,
selector: “body”,
},
{
tagName: “text”,
selector: “title”,
},
{
tagName: “text”,
selector: “text”,
},
],
},
true
);
const graph = new Graph({
container: document.getElementById(‘container’),
// embedding: {
// enabled: true,
// },
highlighting: {
embedding: {
name: ‘stroke’,
args: {
padding: -1,
attrs: {
stroke: ‘#73d13d’,
},
},
},
},
})
function getNodes(nodes) {
const parent = graph.addNode(parentNode)
nodes.forEach(item => {
if (item.shape !== ‘edge’) {
parent.addChild(graph.addNode(item))
}
})
}
function getEdges(edges) {
edges.forEach(item => {
if (item.shape == ‘edge’) {
if (item.source && item.target) {
graph.addEdge({
…item,
// source: item.source,
// target: item.target,
// attrs: item.attrs,
// normal 默认路由,原样返回路径点。
// orth 正交路由,由水平或垂直的正交线段组成。
// oneSide 受限正交路由,由受限的三段水平或垂直的正交线段组成。
// manhattan 智能正交路由,由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。
// metro 智能地铁线路由,由水平或垂直的正交线段和斜角线段组成,类似地铁轨道图,并自动避开路径上的其他节点(障碍)。
// er 实体关系路由,由 Z 字形的斜角线段组成。
// router: “er”,
connector: {
// normal 简单连接器,用直线连接起点、路由点和终点。
// smooth 平滑连接器,用三次贝塞尔曲线线连接起点、路由点和终点。
// rounded 圆角连接器,用直线连接起点、路由点和终点,并在线段连接处用圆弧链接(倒圆角)。
// jumpover 跳线连接器,用直线连接起点、路由点和终点,并在边与边的交叉处用跳线符号链接。
name: "normal",
zIndex: 10000,
},
});
}
}
})
}
// 布局方向
let dir = “LR”; // LR RL TB BT 竖排
// dir = ‘RL’ // LR RL TB BT 竖排
// dir = ‘TB’ // LR RL TB BT 横排
// dir = ‘BT’ // LR RL TB BT 横排
// 自动布局
function layout() {
const nodes = graph.getNodes();
const edges = graph.getEdges();
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: dir, nodesep: 16, ranksep: 16 });
g.setDefaultEdgeLabel(() => ({}));
let width = 0;
let height = 0;
nodes.forEach((node, i) => {
if (node.id !== ‘parent’) {
width = 80;
height = 60;
g.setNode(node.id, { width, height });
}
});
edges.forEach((edge) => {
const source = edge.getSource();
const target = edge.getTarget();
g.setEdge(source.cell, target.cell);
});
dagre.layout(g);
graph.freeze();
g.nodes().forEach((id) => {
const node = graph.getCell(id);
if (node) {
const pos = g.node(id);
node.position(pos.x, pos.y);
}
});
edges.forEach((edge) => {
const source = edge.getSourceNode();
const target = edge.getTargetNode();
const sourceBBox = source.getBBox();
const targetBBox = target.getBBox();
if ((dir === "LR" || dir === "RL") && sourceBBox.y !== targetBBox.y) {
const gap =
dir === "LR"
? targetBBox.x - sourceBBox.x - sourceBBox.width
: -sourceBBox.x + targetBBox.x + targetBBox.width;
const fix = dir === "LR" ? sourceBBox.width : 0;
const x = sourceBBox.x + fix + gap / 2;
edge.setVertices([
{ x, y: sourceBBox.center.y },
{ x, y: targetBBox.center.y },
]);
} else if ((dir === "TB" || dir === "BT") && sourceBBox.x !== targetBBox.x) {
const gap =
dir === "TB"
? targetBBox.y - sourceBBox.y - sourceBBox.height
: -sourceBBox.y + targetBBox.y + targetBBox.height;
const fix = dir === "TB" ? sourceBBox.height : 0;
const y = sourceBBox.y + fix + gap / 2;
edge.setVertices([
{ x: sourceBBox.center.x, y },
{ x: targetBBox.center.x, y },
]);
} else {
edge.setVertices([]);
}
});
graph.unfreeze();
}
getNodes(nodes);
getEdges(edges)
layout();
let ctrlPressed = false
const embedPadding = 20
let nodeId = [];
// 计算 parent node 的 width height
let flag = true;
if (flag) {
const nodes = graph.getNodes();
let x = 0;
let y = 0;
let parent = undefined;
nodes.forEach((node) => {
const element = node.getPosition();
if (element.x > x) {
x = element.x;
}
if (element.y > y) {
y = element.y;
}
if (node.id === “parent”) {
parent = node;
}
});
parent.prop(
{
size: { width: x + 100, height: y + 60 },
},
{ skipParentHandler: true }
);
flag = false;
}
// 递归往前找
function getPreEdges(id) {
edges.forEach(item => {
if (item.shape == ‘edge’) {
if (item.target.cell == id) {
item.attrs.line.stroke = ‘#0f0’;
getPreEdges(item.source.cell)
nodeId.push(item.source.cell)
}
}
})
}
// 递归往后找
function getNextEdges(id) {
edges.forEach(item => {
if (item.shape == ‘edge’) {
if (item.source.cell == id) {
item.attrs.line.stroke = ‘#0f0’;
getNextEdges(item.target.cell)
nodeId.push(item.target.cell)
}
}
})
}
graph.on(“edge:click”, ({ e, x, y, edge, view }) => {
// console.log(“edge:click”, edge.store.data);
const nodes = graph.getNodes();
nodes.forEach((node) => {
if (node.store.data.shape == “tooltip-node”) {
console.log(“node”, node.id, node.store.data.shape);
graph.removeNode(node.id);
}
});
graph.addNode({
x,
y: y > 330 ? 330 : y,
shape: “tooltip-node”,
});
});
graph.on(“node:click”, ({ e, x, y, node, view }) => {
if (node.store.data.id == “parent”) {
console.log(“node:click”, node.store.data);
} else {
node.store.data.attrs.body.fill = ‘#f00’;
// 从画布上删除点击的 node
// graph.removeNode(node.id)
// 递归改数据
nodeId.push(${node.id}
)
getPreEdges(node.id);
getNextEdges(node.id)
graph.clearCells(); // 清空画布所有的 node、edge
nodeId.forEach(ele => {
nodes.forEach(item => {
if (item.id == ele) {
item.attrs.body.fill = ‘#0f0’;
}
})
})
getNodes(nodes)
edges.forEach(item => {
graph.addEdge({
…item,
connector: {
name: “normal”,
},
});
})
// 重新布局
layout();
}
});
graph.on(‘node:change:size’, ({ node, options }) => {
if (options.skipParentHandler) {
return
}
const children = node.getChildren()
if (children && children.length) {
node.prop(‘originSize’, node.getSize())
}
})
graph.on(‘node:change:position’, ({ node, options }) => {
if (options.skipParentHandler || ctrlPressed) {
return
}
const children = node.getChildren()
if (children && children.length) {
node.prop(‘originPosition’, node.getPosition())
}
const parent = node.getParent()
if (parent && parent.isNode()) {
let originSize = parent.prop(‘originSize’)
if (originSize == null) {
originSize = parent.getSize()
parent.prop(‘originSize’, originSize)
}
let originPosition = parent.prop('originPosition')
if (originPosition == null) {
originPosition = parent.getPosition()
parent.prop('originPosition', originPosition)
}
let x = originPosition.x
let y = originPosition.y
let cornerX = originPosition.x + originSize.width
let cornerY = originPosition.y + originSize.height
let hasChange = false
const children = parent.getChildren()
if (children) {
children.forEach((child) => {
const bbox = child.getBBox().inflate(embedPadding)
const corner = bbox.getCorner()
if (bbox.x < x) {
x = bbox.x
hasChange = true
}
if (bbox.y < y) {
y = bbox.y
hasChange = true
}
if (corner.x > cornerX) {
cornerX = corner.x
hasChange = true
}
if (corner.y > cornerY) {
cornerY = corner.y
hasChange = true
}
})
}
if (hasChange) {
parent.prop(
{
position: { x, y },
size: { width: cornerX - x, height: cornerY - y },
},
{ skipParentHandler: true },
)
}
}
})