一、效果图
二、源码
/**
*
* 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}`, // 元素的 idlabel: 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", // 画布的容器idwith: 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
样式的连线拐点问题,本来定在边角的锚点,但是拐点太多了不齐整,后来就直接锚点在中间了…- 本来有个节点收缩/展开的功能,后来没做了,这个后面再研究研究…
- 使用