一、效果图
二、源码
html
/**
*
* Author: me
* CreatDate: 2024-08-22
*
* Description: 复杂G6案例
*
*/
<template>
<div class="moreG6-wapper">
<div id="graphContainer" ref="graphRef" class="graph-content"></div>
</div>
</template>
<script>
import G6 from "@antv/g6";
export default {
name: "moreG6",
components: {},
props: {},
data() {
return {
graph: null, // G6 实例
graphData: {
nodes: [],
edges: [],
},
dataList: [
{
name: "第一阶段",
aimData: [
{
title: "张三",
aim: "战胜李四",
type: 1,
},
{
title: "李四",
aim: "战胜张三",
type: 2,
},
],
eventData: [
{
time: "1948年11月6日",
isLeft: true, // 在左边还是在右边
dataList: {
title: "发现王五正在收缩",
type: 1,
domain: "综合",
describe: "发现王五正在收缩,当即转入追击",
children: [
{
title: "张三攻占a区,没发现李四",
type: 1,
domain: "战斗",
describe: "",
},
{
title: "李四调动李白",
type: 1,
domain: "战斗",
describe: "",
},
],
},
},
{
time: "1948年11月7日",
isLeft: false,
dataList: {
title: "张三团守a州,同时向b州东进",
type: 2,
domain: "综合",
describe: "张三团守a州,令小红、小兰两人回b州东进",
children: [
{
title: "李四在b官邸",
type: 2,
domain: "战斗",
describe: "",
},
],
},
},
],
},
{
name: "第二阶段",
aimData: [
{
title: "小红",
aim: "战胜王五",
type: 1,
},
{
title: "小兰",
aim: "战胜王五",
type: 1,
},
{
title: "王五",
aim: "战胜张三",
type: 2,
},
],
eventData: [
{
time: "1948年11月6日",
isLeft: true,
dataList: {
title: "发现王五正在收缩",
type: 1,
domain: "综合",
describe: "发现王五正在收缩,当即转入追击",
children: [
{
title: "张三攻占a区,没发现李四",
type: 1,
domain: "战斗",
describe: "",
},
{
title: "李四调动李白",
type: 1,
domain: "战斗",
describe: "",
},
{
title: "李四造谣张三",
type: 1,
domain: "舆论",
describe: "",
},
],
},
},
{
time: "1948年11月7日",
isLeft: false,
dataList: {
title: "张三团守a州",
type: 2,
domain: "综合",
describe: "张三团守a州,令小红、小兰两人回徐州东进",
children: [
{
title: "李四在b官邸",
type: 2,
domain: "前进",
describe: "",
},
{
title: "小兰垄断李四的口粮",
type: 2,
domain: "商战",
describe: "",
},
],
},
},
],
},
{
name: "第三阶段",
},
{
name: "第四阶段",
},
],
bgColor: "#0B1E4C",
stageSize: [110, 40],
aimSize: [150, 30],
eventSize: [176, 46],
eventContentSize: [182, 90],
eventDataSize: [120, 40],
stageSpace: 100,
openSpace: 40,
aimSpace: 150,
eventSpace: 220,
domainW: 28,
};
},
mounted() {
this.$nextTick(() => {
this.initData();
this.initGraph();
this.drawGraph();
});
},
methods: {
/**
* 根据数据生成节点和边数据
*/
initData() {
const that = this;
const dom = this.$refs.graphRef;
const centerX = dom.offsetWidth / 2;
this.dataList.forEach((stage, index) => {
const pl = ((stage.partieList?.length || 0) / 2) * that.aimSize[1];
let eventLen = 0;
if (index === 0) {
stage.y = 40 + pl;
} else {
eventLen = this.dataList[index - 1].eventData?.length || 0;
stage.y =
this.dataList[index - 1].y +
eventLen * that.eventSpace +
that.stageSpace * 2 +
pl;
}
// 1.阶段总线
const stageNde = {
id: `stageNode${index}`, // 元素的 id
label: stage.name, // 标签文字
x: centerX,
y: stage.y,
type: "rect", // 元素的图形
size: that.stageSize, // 元素的大小
// 标签配置属性
labelCfg: {
positions: "center", // 标签在元素中的位置
// 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
style: {
fill: "#fff", // 填充色,这里是文字颜色
fontSize: 16,
// fontWeight: "bold",
fontFamily: "黑体",
},
},
// 包裹样式属性的字段 style 与其他属性在数据结构上并行
style: {
lineWidth: 2,
fill: "#0F3664", // 元素的填充色
stroke: "#fff", // 元素的描边色
radius: 20, // 圆角
},
// 锚点
anchorPoints: [
[1, 0.5], // 右中
[0.5, 1], // 下中
],
};
this.graphData.nodes.push(stageNde);
if (index !== 0) {
const stageLine = {
id: `stageLine${index}`,
source: `stageNode${index - 1}`, // 和上面的保持一致
target: `stageNode${index}`,
type: "line", // 边形状
style: {
lineWidth: 2,
color: "#fff",
opacity: 0.7, // 边透明度
},
};
this.graphData.edges.push(stageLine);
}
// 2.阶段-目标数据
if (stage.aimData && stage.aimData.length) {
const adl = stage.aimData.length;
// 目标数据
stage.aimData.forEach((aim, aimIndex) => {
// aimNodeY 先大概吧,后面再重新计算优化一下
const aimNodeY =
stage.y + (that.aimSize[1] + 10) * aimIndex - pl - adl * 12;
// 目标标题
const aimNode = {
id: `aimNode${index}${aimIndex}`,
label: aim.title + "目标",
x: centerX + that.stageSize[0] + that.openSpace * 2.5,
y: aimNodeY,
type: "rect",
size: that.aimSize,
labelCfg: {
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 0,
fill: aim.type === 1 ? "#E16E76" : "#1685F3",
radius: [0, 15, 15, 0],
},
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
};
this.graphData.nodes.push(aimNode);
// 目标内容
const aimContentNode = {
id: `aimContentNode${index}${aimIndex}`,
label: aim.aim,
x: centerX + that.stageSize[0] + that.openSpace * 6.5,
y: aimNodeY,
type: "rect",
size: [0, 40],
labelCfg: {
positions: "left",
style: {
textAlign: "left",
fill: "#fff",
fontSize: 14,
opacity: 0.7,
fontFamily: "黑体",
},
},
style: {
lineWidth: 0,
fill: that.bgColor,
},
anchorPoints: [[0, 0.5]],
};
this.graphData.nodes.push(aimContentNode);
// 连线
const aimLine = {
id: `aimLine${index}${aimIndex}`,
source: `stageNode${index}`,
target: `aimNode${index}${aimIndex}`,
type: "cubic-horizontal", // 边形状
style: {
lineWidth: 1,
fill: "transparent", // 设置透明背景色
opacity: 0.7,
},
};
const aimContentLine = {
id: `aimContentLine${index}${aimIndex}`,
source: `aimNode${index}${aimIndex}`,
target: `aimContentNode${index}${aimIndex}`,
type: "line", // 边形状
style: {
lineWidth: 1,
fill: "#ffffff",
opacity: 0.7,
},
};
this.graphData.edges.push(aimLine);
this.graphData.edges.push(aimContentLine);
});
}
// 3.时间事件数据
if (stage.eventData && stage.eventData.length) {
stage.eventData.forEach((event, eventIndex) => {
event.x = event.isLeft
? -centerX + that.stageSize[0] + that.openSpace * 4
: centerX + that.stageSize[0] + that.openSpace * 4;
event.y =
stage.y +
eventIndex * that.eventSpace +
that.aimSize[1] +
that.openSpace * 2.5; // 先大概吧,后面再重新计算优化一下
// 时间
const timeNode = {
id: `timeNode${index}${eventIndex}`,
label: event.time,
x: event.isLeft ? centerX - 10 : centerX + 10,
y: event.y - 16,
type: "rect",
size: [0, 40],
labelCfg: {
positions: event.isLeft ? "right" : "left",
style: {
textAlign: event.isLeft ? "right" : "left",
fill: "#fff",
fontSize: 14,
opacity: 0.8,
fontFamily: "黑体",
},
},
style: {
lineWidth: 0,
fill: that.bgColor,
},
};
this.graphData.nodes.push(timeNode);
// 事件标题
const eventNode = {
id: `eventNode${index}${eventIndex}`,
label: that.fittingString(event.dataList.title, 9),
labelInit: event.dataList.title,
x: event.isLeft
? -event.x - that.domainW + 12
: event.x + that.domainW - 12,
y: event.y,
type: "rect",
size: that.eventSize,
labelCfg: {
positions: event.isLeft ? "right" : "left",
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 0,
fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
radius: event.isLeft ? [24, 0, 0, 24] : [0, 24, 24, 0],
},
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
};
this.graphData.nodes.push(eventNode);
// 领域
const realmNode = {
id: `realmNode${index}${eventIndex}`,
label: [...event.dataList.domain].join("\n"),
x: event.isLeft
? -event.x + that.openSpace * 2 + 6
: event.x - that.openSpace * 2 - 6,
y: event.y,
type: "rect",
size: [that.domainW, that.eventSize[1]],
labelCfg: {
positions: "center",
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 1,
fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
stroke: event.dataList.type === 1 ? "#EDA9AE" : "#76B7F8", // 元素的描边色
},
};
this.graphData.nodes.push(realmNode);
// 内容
const eventContentNode = {
id: `eventContentNode${index}${eventIndex}`,
label: event.dataList.describe
? that.divideString(event.dataList.describe, 11, 4)
: "暂无描述",
labelInit: event.dataList.describe,
x: event.isLeft ? -event.x + 9 : event.x - 9,
y: event.y + that.eventSize[1] + 22,
type: "rect",
size: that.eventContentSize,
labelCfg: {
positions: "center",
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 1,
fill: "#3C4B70",
stroke: "#5C6988", // 元素的描边色
opacity: 0.9,
},
};
this.graphData.nodes.push(eventContentNode);
// 连线
const eventLine = {
id: `eventLine${index}${eventIndex}`,
source: `stageNode${index}`,
target: `eventNode${index}${eventIndex}`,
type: "polyline", // 边形状
style: {
lineWidth: 1,
fill: "transparent",
opacity: 0.7,
},
};
this.graphData.edges.push(eventLine);
// 4.时间事件后面的列表
if (event.dataList.children && event.dataList.children.length) {
event.dataList.children.forEach((eventData, eventDataIndex) => {
const x =
event.x +
that.eventSize[0] +
that.openSpace * 3 +
eventDataIndex *
(that.eventDataSize[0] + that.openSpace * 1.4);
const y = event.y + that.openSpace;
// 标题
const eventDataNode = {
id: `eventDataNode${index}${eventIndex}${eventDataIndex}`,
label: that.fittingString(eventData.title, 6),
labelInit: eventData.title,
x: event.isLeft
? -x - that.domainW + 14
: x + that.domainW - 14,
y: y,
type: "rect",
size: that.eventDataSize,
labelCfg: {
positions: event.isLeft ? "right" : "left",
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 0,
fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
},
anchorPoints: [[0.5, 0]],
};
this.graphData.nodes.push(eventDataNode);
// 领域
const realmNode02 = {
id: `realmNode${index}${eventIndex}${eventDataIndex}02`,
label: [...eventData.domain].join("\n"),
x: event.isLeft ? -x + 60 : x - 60,
y: y,
type: "rect",
size: [that.domainW, that.eventDataSize[1]],
labelCfg: {
positions: "center",
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 1,
fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
stroke: event.dataList.type === 1 ? "#EDA9AE" : "#76B7F8", // 元素的描边色
},
};
this.graphData.nodes.push(realmNode02);
// 内容
const eventContentNode02 = {
id: `eventContentNode${index}${eventIndex}${eventDataIndex}02`,
label: eventData.describe
? that.divideString(eventData.describe, 9, 3)
: "暂无描述",
labelInit: eventData.describe,
x: event.isLeft ? -x : x,
y: y + that.eventDataSize[1] + 18,
type: "rect",
size: [
that.eventDataSize[0] + that.domainW,
that.eventContentSize[1] * 0.85,
],
labelCfg: {
positions: "center",
style: {
textAlign: "center",
fill: "#fff",
fontSize: 14,
fontFamily: "黑体",
},
},
style: {
lineWidth: 1,
fill: "#3C4B70",
stroke: "#5C6988",
opacity: 0.9,
},
};
this.graphData.nodes.push(eventContentNode02);
// 连线
const eventLine = {
id: `eventLine${index}${eventIndex}${eventDataIndex}`,
source: `eventNode${index}${eventIndex}`,
target: `eventDataNode${index}${eventIndex}${eventDataIndex}`,
type: "polyline", // 边形状
style: {
lineWidth: 1,
fill: "transparent", // 透明色
opacity: 0.7,
},
};
this.graphData.edges.push(eventLine);
});
}
});
}
});
},
/**
* 初始化graph实例
*/
initGraph() {
// 缩略图
const minimap = new G6.Minimap({
size: [window.innerWidth / 6, window.innerHeight / 4],
});
// 提示框
const tooltip = new G6.Tooltip({
offsetX: 10,
offsetY: 10,
size: [100, 100],
itemTypes: ["node"], // 允许出现 tooltip 的 item 类型
// 自定义 tooltip 内容
getContent: (e) => {
const outDiv = document.createElement("div");
outDiv.style.width = "fit-content";
outDiv.innerHTML = `<div style="max-width:200px;">
${e.item.getModel().labelInit || e.item.getModel().label}
</div>`;
return outDiv;
},
// 哪些要设置提示框
shouldBegin: (e) => {
const id = e.item.getModel().id;
const a = id.includes("eventNode");
const b = id.includes("eventContentNode");
const c = id.includes("eventDataNode");
const is = a || b || c;
return is;
},
});
// 容器节点
const dom = this.$refs.graphRef;
// 配置参数
const options = {
container: "graphContainer", // 画布的容器id
with: dom.offsetWidth, // 画布宽度
height: dom.offsetHeight, // 画布高度
minZoom: 0.2,
maxZoom: 5,
// 图交互模式
modes: {
default: ["drag-canvas", "zoom-canvas"], // 允许拖拽画布、缩放画布
},
plugins: [tooltip, minimap], // 插件
};
// 实例化
this.graph = new G6.Graph(options);
// 自适应
this.resizeView();
// 监听窗口大小变化
window.addEventListener("resize", this.resizeView);
},
/**
* 画图
*/
drawGraph() {
// 清除画布元素
this.graph.clear();
// 数据源
this.graph.data(this.graphData);
// 渲染
this.graph.render();
},
/**
* 自适应
*/
resizeView() {
const dom = this.$refs.graphRef;
if (dom && this.graph) {
this.graph.changeSize(dom.offsetWidth, dom.offsetHeight); // 修改画布大小
// this.graph.fitCenter(); // 移至中心
}
},
/**
* 字符串等长划分-入口事件
* @param str 字符串
* @param len 一行的长度
* @param row 行数
*/
divideString(str, len, row = 3) {
str = this.fittingString(str, len * row - 2);
const result = this.divideHandle(str, len);
return result.join("\n");
},
/**
* 字符串等长划分
* @param str 字符串
* @param len 划分的长度
*/
divideHandle(str, len) {
if (str.length <= len) {
return [str];
}
const chunk = str.substring(0, len);
const arr = [chunk, ...this.divideHandle(str.substring(len), len)];
return arr;
},
/**
* 截取字符串,超出多少len省略
* @param str 字符串
* @param len 长度
*/
fittingString(str, len) {
if (str.length <= len) {
return str;
}
const result = str.substring(0, len) + "...";
return result;
},
},
beforeDestroy() {
this.graph && this.graph.destroy();
},
watch: {},
};
</script>
<style lang='scss' scoped>
.moreG6-wapper {
height: 100%;
background: #0b1e4c;
.graph-content {
width: 100%;
height: 96%;
::v-deep .g6-tooltip {
border: 1px solid #e2e2e2;
border-radius: 4px;
font-size: 12px;
color: #545454;
background-color: rgba(255, 255, 255, 0.9);
padding: 10px 8px;
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
}
::v-deep .g6-minimap {
position: absolute;
right: 0;
top: 61px;
background-color: #0b1e4c;
}
}
}
</style>
三、记录说明
-
当前使用版本:
"@antv/g6": "^4.8.21"
-
官方文档节点形状为shape,但是在不记得哪个版本后,
shape
改为type
,当时整了好久,形状就是不生效,原来是字段改变了,这个坑要注意 -
初步接触就有这么个界面需求,官方文档有点乱,且不全面,新手小白上路属实有点头皮发麻,后来也琢磨出来一些:
- 官方案例实现不了的,自己画
- 无非就是两个要点:节点、边
- 像这案例一样的不能使用居中api的,得自己计算节点的位置
- 实现不了的不影响使用的小功能,就干掉它,不要了...
-
期间遇到还没解决的问题:
- 使用
image
内置节点,自己的图片显示有问题 polyline
样式的连线拐点问题,本来定在边角的锚点,但是拐点太多了不齐整,后来就直接锚点在中间了...- 本来有个节点收缩/展开的功能,后来没做了,这个后面再研究研究...
- 使用