前言
- 再次稍微研究了下这玩意,这玩意难用的地方在于,第一、文档写的很混乱,有很多漏写的。 第二、报错经常是它内部报错,得看它里面到底咋回事才知道为啥出现错误。第三、卸载比较诡异,如果有路由相互切换的话,可能得踩点坑。
事件
- 首先事件方面,可以通过graph.on监听事件,可以监听这些:
// click?: string;
// mousedown?: string;
// mouseup?: string;
// dblclick?: string;
// contextmenu?: string;
// mouseenter?: string;
// mouseout?: string;
// mouseover?: string;
// mousemove?: string;
// mouseleave?: string;
// dragstart?: string;
// dragend?: string;
// drag?: string;
// dragenter?: string;
// dragleave?: string;
// dragover?: string;
// dragout?: string;
// drop?: string;
// keyup?: string;
// keydown?: string;
// wheel?: string;
// focus?: string;
// "node:click"?: string;
// "node:contextmenu"?: string;
// "node:dblclick"?: string;
// "node:dragstart"?: string;
// "node:drag"?: string;
// "node:dragend"?: string;
// "node:mouseenter"?: string;
// "node:mouseleave"?: string;
// "node:mousemove"?: string;
// "node:drop"?: string;
// "node:dragenter"?: string;
// "node:dragleave"?: string;
// "edge:click"?: string;
// "edge:contextmenu"?: string;
// "edge:dblclick"?: string;
// "edge:mouseenter"?: string;
// "edge:mouseleave"?: string;
// "edge:mousemove"?: string;
// "canvas:mousedown"?: string;
// "canvas:mousemove"?: string;
// "canvas:mouseup"?: string;
// "canvas:click"?: string;
// "canvas:mouseleave"?: string;
// "canvas:dragstart"?: string;
// "canvas:drag"?: string;
// "canvas:dragend"?: string;
// "combo:click"?: string;
// "combo:contextmenu"?: string;
// "combo:dblclick"?: string;
// "combo:dragstart"?: string;
// "combo:drag"?: string;
// "combo:dragend"?: string;
// "combo:mouseenter"?: string;
// "combo:mouseleave"?: string;
// "combo:mousemove"?: string;
// "combo:drop"?: string;
// "combo:dragover"?: string;
// "combo:dragleave"?: string;
// "combo:dragenter"?: string;
- 主要先研究下脑图,脑图有个特点,就是传入data可以不需要提供x,y,new了graph之后,自动生成了x,y,然后方向可以在node里进行配置:
let centerX = 0;
graph.node(function (node: NodeConfig) {
if (node.id === "root") {
centerX = node.x as number;
}
return {
label: node.id,
labelCfg: {
position://看有没有孩子和孩子数量配置左右
node.children && node.children.length > 0
? "right"
: (node.x as number) > centerX
? "right"
: "left",
offset: 5,
},
};
});
- 节点拖拽移动。主要监听node:drag事件,然后去取鼠标的xy,转换为graph的xy,最后赋给节点。
graph.on("node:dragstart", (e: any) => {
const item = e.item;
const model = item.getModel();
model.style.cursor = "grab";
graph.update(item, model);
graph.paint();
});
graph.on("node:drag", (e: any) => {
const { clientX, clientY } = e;
const point = graph.getPointByClient(clientX, clientY);
const item = e.item;
const model = item.getModel();
item.updatePosition(point);
graph.update(item, model);
graph.paint();
});
graph.on("node:dragend", (e: any) => {
const item = e.item;
const model = item.getModel();
model.style.cursor = "default";
graph.update(item, model);
graph.paint();
});
graph.on("canvas:drag", (e: any) => {
});
graph.on("dragstart", (e: any) => {
});
graph.on("mousedown", (e: any) => {
const item = e.item;
if (item) {
const model = item.getModel();
model.style.cursor = "grab";
graph.update(item, model);
graph.paint();
}
});
自定义节点
- 其实是先注册个节点,注册节点时,可以利用addshape做节点样子,同时可以绑定事件,给节点赋文本。
- 需要注意是这里用的是图形分组概念,g6里面不知道哪个人搞了那么多名字全是group要么是groups很难区分。
- 通过一个g6实例的group,可以找到其所属的item。
import React, { useEffect, useRef } from "react";
import G6 from "@antv/g6";
import { NodeConfig } from "@antv/g6/lib/types";
import GGroup from "@antv/g-canvas/lib/group";
import { IShape } from "@antv/g-canvas/lib/interfaces";
const data = {
nodes: [
{
id: "Model",
type: "model-node",
x: 100,
y: 100,
style: {
width: 160,
height: 100,
fill: "#f1b953",
stroke: "#f1b953",
},
openIcon: {
x: 180,
y: 45,
fontSize: 20,
style: {
fill: "#fc0",
},
},
hideIcon: {
x: 180,
y: 45,
fontSize: 20,
style: {
fill: "#666",
},
},
labels: [
{
x: 10,
y: 20,
label: "标题,最长10个字符~~",
labelCfg: {
fill: "#666",
fontSize: 14,
maxlength: 10,
},
},
{
x: 10,
y: 40,
label: "描述,最长12个字333符~~~",
labelCfg: {
fontSize: 12,
fill: "#999",
maxlength: 12,
},
},
],
},
{
id: "node1",
x: 100,
y: 200,
},
],
};
interface modelNodeType extends NodeConfig {
openIcon: {
x: number;
y: number;
fontSize: number;
style: Object;
};
hideIcon: {
x: number;
y: number;
fontSize: number;
style: Object;
};
labels: Array<any>;
}
G6.registerNode(
"model-node",
{
drawShape(cfg: modelNodeType, group) {
const opts = cfg;
const openIcon = opts.openIcon;
const hideIcon = opts.hideIcon;
const shape = group!.addShape("rect", {
name: "model-node",
draggable: true,
attrs: cfg.style,
});
const openSwitch = group!.addShape("circle", {
draggable: true,
attrs: {
r: 10,
...openIcon,
...openIcon.style,
},
className: "state-open",
});
const hideSwitch = group!.addShape("circle", {
draggable: true,
attrs: {
r: 10,
...hideIcon,
...hideIcon.style,
},
className: "state-hide",
});
for (let i = 0; i < cfg.labels.length; i++) {
const item = cfg.labels[i];
const {
label,
labelCfg: { maxlength },
} = item;
let text = maxlength ? label.substr(0, maxlength) : label || "";
if (label.length > maxlength) {
text = `${text}...`;
}
group!.addShape("text", {
attrs: {
text,
...item,
...item.labelCfg,
},
});
}
this.bindEvent(group, openSwitch);
this.bindEvent(group, hideSwitch);
return shape;
},
bindEvent(group: GGroup, btn: IShape) {
btn.on("click", () => {
const open = group
.get("children")
.find((child: any) => child.cfg.className === "state-open");
const close = group
.get("children")
.find((child: any) => child.cfg.className === "state-hide");
if (btn.cfg.className === "state-open") {
const item = group.get("item");
const model = item.getModel();
open.toBack();
close.toFront();
model.style.height = 100;
item.update(model);
} else if (btn.cfg.className === "state-hide") {
const item = group.get("item");
const model = item.getModel();
close.toBack();
open.toFront();
model.style.height = 50;
item.update(model);
}
});
},
},
"single-node"
);
function App() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const graph = new G6.Graph({
container: ref.current!,
width: 800,
height: 800,
fitCenter: true,
modes: {
default: ["drag-canvas", "zoom-canvas", "drag-node"],
},
});
graph.data(data);
graph.render();
}, []);
return <div ref={ref} id="container"></div>;
}
export default App;
G6.registerEdge("hvh", {
draw(cfg, group) {
const startPoint = cfg!.startPoint!;
const endPoint = cfg!.endPoint!;
const startArrow = (cfg!.style && cfg!.style.startArrow) || undefined;
const endArrow = (cfg!.style && cfg!.style.endArrow) || undefined;
const shape = group!.addShape("path", {
attrs: {
stroke: "#333",
path: [
["M", startPoint.x, startPoint.y],
[
"L",
endPoint.x / 3 + (1 / 3) * startPoint.x,
endPoint.y / 2 + (1 / 3) * startPoint.y,
],
[
"L",
endPoint.x * 1.1 + (2 / 3) * startPoint.x,
endPoint.y / 2 + (2 / 3) * startPoint.y,
],
["L", endPoint.x, endPoint.y],
],
startArrow,
endArrow,
},
name: "path-shape",
});
return shape;
},
setState(name, value, item) {
const group = item!.getContainer();
const shape = group.get("children")[0];
if (name === "active") {
if (value) {
shape.attr("stroke", "red");
shape.attr("lineWidth", 3);
} else {
shape.attr("stroke", "#333");
shape.attr("lineWidth", 1);
}
}
if (name === "selected") {
if (value) {
shape.attr("lineWidth", 3);
} else {
shape.attr("lineWidth", 1);
}
}
},
});
defaultEdge: {
type: "line-arrow",
style: {
stroke: "#F6BD16",
startArrow: {
path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
fill: "#F6BD16",
},
endArrow: {
path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
fill: "#F6BD16",
},
},
},
State
- g6里为了传递信息,设置了state,这个状态有全局设置或者单个设置。一般来说,统一用全局设置。这个主要用来判断点击了没有hover了没有之类。
- 有点诡异的是好像相同行为触发的不同状态之间会冲突,所以最好是1个状态对应多值而不是多个状态对应二值。
- 然后文档全篇在说怎么设置值,没说怎么获取值。。。。。。。后来发现item里写了个方法getState可以拿到state所有值,我也是服了。。state的概念里不写怎么获取,只写hasState是用来判断二值的。然后这个方法获取的值也很诡异,是个数组字符串,比如设置的值是active , ‘1’ ,那么数组里面值是:
active:1
的字符串。(这设计谁想出来的。。。不能做成key value?不能按key取值?实在不行按active:1字符串取值也行啊) - 比如上面那个例子就有冲突,监听那里改下:
graph.on("edge:click", (ev: IG6GraphEvent) => {
const edge = ev.item;
const value = edge!.getStates()[0];
if (value !== "active:0") {
if (value === "active:2") {
graph.setItemState(edge!, "active", "0");
} else {
graph.setItemState(edge!, "active", "2");
}
}
});
graph.on("edge:mouseenter", (ev: IG6GraphEvent) => {
const edge = ev.item;
const value = edge!.getStates()[0];
if (value !== "active:2") {
graph.setItemState(edge!, "active", "1");
}
});
graph.on("edge:mouseleave", (ev: IG6GraphEvent) => {
const edge = ev.item;
const value = edge!.getStates()[0];
if (value !== "active:2") {
graph.setItemState(edge!, "active", "0");
}
});
编辑器
- 编辑器功能基本上是大家最关心的功能,查阅资料发现他们以前做过编辑器且没开源,然后又不搞了,让自己封装。
- 对于编辑,我摸索了一番,实际就是要自己做个dom面板来进行编辑,我以右键弹出选框选择编辑label为例(实际应该点击后把input设置成显示,修改完毕搞个按钮然后点击保存。再关闭input)。我直接就input不操作显示与否了:
import React, { useEffect, useRef, useState } from "react";
import G6 from "@antv/g6";
import { NodeConfig, Item, IG6GraphEvent } from "@antv/g6/lib/types";
import GGroup from "@antv/g-canvas/lib/group";
import { IShape } from "@antv/g-canvas/lib/interfaces";
const data = {
nodes: [
{
id: "Model",
type: "model-node",
x: 200,
y: 100,
style: {
width: 160,
height: 100,
fill: "#f1b953",
stroke: "#f1b953",
},
openIcon: {
x: 180,
y: 45,
fontSize: 20,
style: {
fill: "#fc0",
},
},
hideIcon: {
x: 180,
y: 45,
fontSize: 20,
style: {
fill: "#666",
},
},
labels: [
{
x: 10,
y: 20,
label: "标题,最长10个字符~~",
labelCfg: {
fill: "#666",
fontSize: 14,
maxlength: 10,
},
},
{
x: 10,
y: 40,
label: "描述,最长12个字333符~~~",
labelCfg: {
fontSize: 12,
fill: "#999",
maxlength: 12,
},
},
],
anchorPoints: [
[0, 0.5],
[0, 1],
],
},
{
id: "node1",
label: "node1",
x: 10,
y: 200,
size: 50,
anchorPoints: [
[1, 0.5],
[1, 0.8],
],
},
{
id: "node2",
label: "node2",
size: 50,
x: 70,
y: 20,
anchorPoints: [
[1, 0.5],
[0, 0.5],
[0.5, 1],
],
},
],
edges: [
{
id: "edge1",
target: "Model",
source: "node1",
type: "hvh",
sourceAnchor: 1,
targetAnchor: 1,
},
{
id: "edge2",
target: "node2",
source: "node1",
type: "hvh",
},
{
id: "edge3",
target: "node2",
source: "Model",
type: "hvh",
targetAnchor: 2,
},
],
};
interface modelNodeType extends NodeConfig {
openIcon: {
x: number;
y: number;
fontSize: number;
style: Object;
};
hideIcon: {
x: number;
y: number;
fontSize: number;
style: Object;
};
labels: Array<any>;
}
function shapesAddAttr(shape: Array<any>, key: string, value: any) {
shape.forEach((v) => v.attr(key, value));
}
G6.registerEdge("hvh", {
draw(cfg, group) {
const startPoint = cfg!.startPoint!;
const endPoint = cfg!.endPoint!;
const startArrow = (cfg!.style && cfg!.style.startArrow) || undefined;
const endArrow = (cfg!.style && cfg!.style.endArrow) || undefined;
const shape = group!.addShape("path", {
attrs: {
stroke: "#333",
path: [
["M", startPoint.x, startPoint.y],
[
"L",
endPoint.x / 3 + (1 / 3) * startPoint.x,
endPoint.y / 2 + (1 / 3) * startPoint.y,
],
[
"L",
endPoint.x * 1.1 + (2 / 3) * startPoint.x,
endPoint.y / 2 + (2 / 3) * startPoint.y,
],
["L", endPoint.x, endPoint.y],
],
startArrow,
endArrow,
},
name: "path-shape",
});
return shape;
},
setState(name, value, item) {
const group = item!.getContainer();
const shape = group.get("children");
if (name === "active") {
switch (value) {
case "0":
shapesAddAttr(shape, "stroke", "#333");
shapesAddAttr(shape, "lineWidth", 1);
break;
case "1":
shapesAddAttr(shape, "stroke", "red");
shapesAddAttr(shape, "lineWidth", 3);
break;
case "2":
shapesAddAttr(shape, "stroke", "blue");
shapesAddAttr(shape, "lineWidth", 3);
break;
default:
return;
}
}
},
});
G6.registerNode(
"model-node",
{
drawShape(cfg: modelNodeType, group) {
const opts = cfg;
const openIcon = opts.openIcon;
const hideIcon = opts.hideIcon;
const shape = group!.addShape("rect", {
name: "model-node",
draggable: true,
attrs: cfg.style,
});
const openSwitch = group!.addShape("circle", {
draggable: true,
attrs: {
r: 10,
...openIcon,
...openIcon.style,
},
className: "state-open",
});
const hideSwitch = group!.addShape("circle", {
draggable: true,
attrs: {
r: 10,
...hideIcon,
...hideIcon.style,
},
className: "state-hide",
});
for (let i = 0; i < cfg.labels.length; i++) {
const item = cfg.labels[i];
const {
label,
labelCfg: { maxlength },
} = item;
let text = maxlength ? label.substr(0, maxlength) : label || "";
if (label.length > maxlength) {
text = `${text}...`;
}
group!.addShape("text", {
attrs: {
text,
...item,
...item.labelCfg,
},
});
}
this.bindEvent(group, openSwitch);
this.bindEvent(group, hideSwitch);
return shape;
},
bindEvent(group: GGroup, btn: IShape) {
btn.on("click", () => {
const open = group
.get("children")
.find((child: any) => child.cfg.className === "state-open");
const close = group
.get("children")
.find((child: any) => child.cfg.className === "state-hide");
if (btn.cfg.className === "state-open") {
const item = group.get("item");
const model = item.getModel();
open.toBack();
close.toFront();
model.style.height = 100;
item.update(model);
} else if (btn.cfg.className === "state-hide") {
const item = group.get("item");
const model = item.getModel();
close.toBack();
open.toFront();
model.style.height = 50;
item.update(model);
}
});
},
},
"single-node"
);
function App() {
const ref = useRef<HTMLDivElement>(null);
const [state, setState] = useState("");
const [changeItem, setChangeItem] = useState<Item>();
useEffect(() => {
const contextMenu = new G6.Menu({
getContent(graph) {
console.log("graph", graph);
return `<div>编辑lable</div>`;
},
handleMenuClick: (target, item) => {
const model = item.getModel();
const value = model.label;
if (typeof value === "string") {
setState(value);
setChangeItem(item);
}
},
});
const graph = new G6.Graph({
container: ref.current!,
width: 800,
height: 800,
fitCenter: true,
modes: {
default: ["drag-canvas", "zoom-canvas", "drag-node"],
},
defaultEdge: {
type: "line-arrow",
style: {
stroke: "#F6BD16",
startArrow: {
path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
fill: "#F6BD16",
},
endArrow: {
path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
fill: "#F6BD16",
},
},
},
plugins: [contextMenu],
});
graph.on("edge:click", (ev: IG6GraphEvent) => {
const edge = ev.item;
const value = edge!.getStates()[0];
if (value !== "active:0") {
if (value === "active:2") {
graph.setItemState(edge!, "active", "0");
} else {
graph.setItemState(edge!, "active", "2");
}
}
});
graph.on("edge:mouseenter", (ev: IG6GraphEvent) => {
const edge = ev.item;
const value = edge!.getStates()[0];
if (value !== "active:2") {
graph.setItemState(edge!, "active", "1");
}
});
graph.on("edge:mouseleave", (ev: IG6GraphEvent) => {
const edge = ev.item;
const value = edge!.getStates()[0];
if (value !== "active:2") {
graph.setItemState(edge!, "active", "0");
}
});
graph.data(data);
graph.render();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(e.target.value);
if (changeItem) {
const model = changeItem.getModel();
model.label = e.target.value;
changeItem.update(model);
}
};
return (
<div>
<div id="editor">
<span>修改label :</span>
<input value={state} onChange={handleChange}></input>
</div>
<div ref={ref} id="container"></div>
</div>
);
}
export default App;
- 最后需要注意下自定义节点的格式,如果像我这么写自定义节点,那么label很可能就不对或者没有,所以事先需要规划好到底哪些属性应该去配置,哪些属性可以配置。