前言
AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。
G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。
它是一款国产可视化插件,中文官方文档方便阅读和学习。G6可以实现很多d3才能实现的可视化图表,d3作为一款国外很强大的可视化插件,它的官方文档是非汉语文档,社区虽然很活跃,但几乎是英文文档,阅读和学习起来并不是那么轻松,尤其是英语不太好的同学,阅读和学习d3更吃力。这时候G6就是不错的选择。因为G6包含丰富的图表类型,还可以实现节点,边等自定义。
使用步骤
安装&引入
npm install --save @antv/g6 //安装
import G6 from '@antv/g6'; //在需要的js文件引入
使用
创建容器
<div id="container"></div>
<style>
#container{
width: 100%;
height: 800px;
}
</style>
数据格式
const data = {
// 节点
nodes: [
{
id: 'node1',
x: 100,
y: 200,
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: 'node1',
target: 'node2',
},
],
};
节点样式
主要包括nodes(节点)和edges(边),节点是画布上显示的小矩形,边是两个节点的连线,节点样式可以设置为
- circle:圆;
- rect:矩形;
- ellipse:椭圆;
- polygon:多边形;
- fan:扇形;
- image:图片;
- marker:标记;
- path:路径;
- text:文本;
- dom(svg):DOM(图渲染方式
renderer
为'svg'
时可用)。
常用方法
- draw()画布的绘制方法。新增shape或group后,调用此方法将最新的内容渲染到画布上。
- changeSize(width, height)改变画布的大小
- getClientByPoint(x, y)将窗口坐标转换为canvas坐标。
- getPointByClient(x, y)将canvas坐标转换为窗口坐标。
- on(eventType, callback)绑定事件。
- off(eventType, callback)事件解绑。
- addShape(shape, attrs)添加单个图形到画布。
- addGroup(attrs)添加单个组到画布。
- attr()设置或获取实例的绘图属性,无参数获取,有参数更新
- set(name, value)设置实例的属性,如visible, zIndex, id等。
- get(name)获取实例的属性值
- show()显示某实例对应的图形。
- hide()隐藏某实例对应的图形
- remove()删除实例本身
- destroy()销毁实例
- getBBox()获取实例的包围盒
graph method
- graph.save()
- graph.read(data) 读数据渲染
- graph.find(id) 寻找数据模型
- graph.add(type, model)
- graph.remove(item)
- graph.update(item, model) item为id或 项对象
- graph.getItems();获取图内所有项
- graph.getNodes()
- graph.getEdges()
- graph.getGroups()
- graph.preventAnimate(callback) 阻止动画
- getShape(x,y)返回该坐标点最上层的元素。
- findById(id)根据元素ID返回对应的实例
案例
- 脑图有个特点,就是传入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;//把第一个节点的x作为center
}
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。。。
model.style.cursor = "default";
graph.update(item, model);
graph.paint();
});
graph.on("canvas:drag", (e: any) => {
// console.log(e);
});
graph.on("dragstart", (e: any) => {
//比node:dragstart先
});
graph.on("mousedown", (e: any) => {
//比dragstart先
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", // String,该节点存在则必须,节点的唯一标识
x: 100, // Number,可选,节点位置的 x 值
y: 200, // Number,可选,节点位置的 y 值
},
],
};
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) {
//ggroup就是graphics group缩写
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"); //在这个图形分组下的item
const model = item.getModel();
open.toBack();
close.toFront(); //这个是让2个圆z轴位置变化 item上的方法
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(); //Item 上的方法
open.toFront(); //item上的方法
model.style.height = 50;
item.update(model); //item上的方法
}
});
},
},
"single-node"
); // 继承自内置节点
function App() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const graph = new G6.Graph({
container: ref.current!,
width: 800,
height: 800,
// renderer: 'svg',
fitCenter: true,
modes: {
default: ["drag-canvas", "zoom-canvas", "drag-node"],
},
});
// 传入数据
graph.data(data);
// 执行渲染
graph.render();
// graph.fitView();
}, []);
return <div ref={ref} id="container"></div>;
}
export default App;
- g6的自定义边跟自定义节点是类似操作。
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,
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: "path-shape",
});
return shape;
},
setState(name, value, item) { //这个方法在3.3后可以不用,改为直接设置全局node/edge StateStyles
const group = item!.getContainer();
const shape = group.get("children")[0]; // 顺序根据 draw 时确定
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",
},
},
},
说明
-
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", // String,该节点存在则必须,节点的唯一标识
label: "node1",
x: 10, // Number,可选,节点位置的 x 值
y: 200, // Number,可选,节点位置的 y 值
size: 50,
anchorPoints: [
//这属性用来设定边的连接中心
[1, 0.5],
[1, 0.8],
],
},
{
id: "node2", // String,该节点存在则必须,节点的唯一标识
label: "node2",
size: 50,
x: 70, // Number,可选,节点位置的 x 值
y: 20, // Number,可选,节点位置的 y 值
anchorPoints: [
//这属性用来设定边的连接中心
[1, 0.5],
[0, 0.5],
[0.5, 1],
],
},
],
edges: [
{
id: "edge1",
target: "Model",
source: "node1",
type: "hvh",
// 该边连入 source 点的第 0 个 anchorPoint,
sourceAnchor: 1,
// 该边连入 target 点的第1个 anchorPoint,
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,
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: "path-shape",
});
return shape;
},
setState(name, value, item) {
//这个方法在3.3后可以不用,改为直接设置全局node/edge StateStyles
const group = item!.getContainer();
const shape = group.get("children"); // 顺序根据 draw 时确定
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) {
//ggroup就是graphics group缩写
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"); //在这个图形分组下的item
const model = item.getModel();
open.toBack();
close.toFront(); //这个是让2个圆z轴位置变化 item上的方法
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(); //Item 上的方法
open.toFront(); //item上的方法
model.style.height = 50;
item.update(model); //item上的方法
}
});
},
},
"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) => {
//target是dom item 是Item
//只有click了才知道是哪个节点触发的
const model = item.getModel();
const value = model.label;
if (typeof value === "string") {
//将节点的值赋给input并绑上onchange给它
setState(value);
setChangeItem(item); //要调用更新,最小是item item才有update
}
},
});
const graph = new G6.Graph({
container: ref.current!,
width: 800,
height: 800,
// renderer: 'svg',
fitCenter: true,
modes: {
default: ["drag-canvas", "zoom-canvas", "drag-node"],
//edit: ["click-select", "click-add-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("node:click", (ev: any) => {
// console.log(ev);
// graph.setMode("edit");
// });
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();
// graph.fitView();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(e.target.value);
//修改state后改变节点Label
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很可能就不对或者没有,所以事先需要规划好到底哪些属性应该去配置,哪些属性可以配置。