Skip to content

G6

🕒 Published at:

官网

说明

适合画连线的图,不适合复杂操作

例如:

config,就是配置里的config

dataConfig,就是配置里的dataConfig

依次类推

下载与提示

npm install --save @antv/g6

vue2/3和react中,这个库用法相同

vue2中动态引入图片可使用require函数img: ${require(`@/assets/${cfg.name}.svg`)}

vue3中不支持require,需要先引入图片import 变量 from 图片路径,再使用img: ${变量}

react中同vue3

初始化

javascript
const container = document.getElementById('container');
const clientRect = container.getBoundingClientRect();
const width = clientRect.width || 1200;
const height = clientRect.height || 500;
const graph = new G6.Graph({
   container: 'container',
   width,height,
   ...config
})
//接收数据,并进行渲染,相当于graph.data(data)+graph.render()
graph.read(dataConfig)

配置

config

graph构造函数的配置项

配置说明
plugins: [tooltip],使用插件
layout:layoutConfig,布局
node/edge/comboStateStyles: {
active: {}, //高亮状态
nactive:{}, //非高亮
hover:{}, //悬浮
selected:{} //选中
},
设置节点/边/combo的不同状态样式
defaultEdge/Combo/Node:{
...elementConfig,
labelCfg
},
所有边(即连线)/Combo/Node的默认样式和行为
modes: {
//'drag-canvas/combo/node', //画布/combo/节点可拖拽
//'activate-relations', //高亮相同节点
//'click-select' //点击选中节点
//'create-edge' //创建边
default: ['drag-node'], //节点可拖拽
},
使用哪些模式,每个模式应用哪些行为
enabledStack:true启用栈操作
fitView: true,自动缩放以适应其容器的大小(填充整个可视区域,而不会超出容器的边界)
fitCenter: true,自动适应容器大小,并居中显示
fitViewPadding: 40,图形边界和容器边界之间的填充距离
renderer: 'svg',指定图形的渲染器类型
linkCenter: true,边的起点和终点会连接到节点的中心,而不是节点的边界

dataConfig

data中常见属性及解释

配置项描述
nodes: [elementConfig],节点配置项
edges: [elementConfig],边配置项
combos: [elementConfig],combo配置项,
combo也就是分组

elementConfig

元素的配置项,元素即节点&边&combo等所有元素

配置项描述类型示例
parentId父级ID,只能是combo的IdStringcombos: [
{ id: 'c1', parentId: 'c2' },
{ id: 'c2'},
],
offset折现的偏移量,就是从多长开始折Number
source/targetAnchor边的起始/终止起点连接的连接桩的下标NumbertargetAnchor:0
source/target连线的起点/终点对应的(节点/combo的id/{x,y})String
anchorPoints设置元素的锚点ArrayanchorPoints:[
[0, 0.5],
[1, 0.5],
]
x/y节点的坐标Number
r/圆的半径Number
id标识 ID,String
opacity透明度Numberopacity:0.3
sizeedge中对应边的粗细,size:1,Array
style元素 keyShape 的样式具体参见各图形样式属性Object见styleConfig
type元素的类型,不传则使用默认值,String
label元素的文本标签,有该字段时默认会渲染 labelString
labelCfg元素label标签的配置项Object
rx/ry水平垂直方向的锐角,类似border-radioNumber

rect

ellipse

text

image

label配置项

名称备注类型
position自定义节点不支持该属性
文本相对于元素的位置,目前支持的位置有:
'center',(node的默认值)
'top',(combo的默认值)
'left',
'right',
'bottom'。

在edge中为'start','middle'(默认),'end'
String
refX/refY(combo与边特有)label在x,y方向的偏移量Number
autoRotate(边特有)标签文字是否跟随边旋转,默认falseBoolean
offset文本的偏移,
position为'bottom'时,文本的上方偏移量;
position为'left'时,文本的右方偏移量;
以此类推在其他
Number
style标签的样式属性,具体配置项参见统一整理在
图形样式属性 - Text 图形
Object

styleConfig

labelCfg的style常用配置项

上表中的标签的样式属性  style 的常用配置项如下:

名称备注类型
fill文本颜色String
stroke文本描边颜色String
lineWidth文本描边粗细Number
opacity文本透明度Number
fontSize文本字体大小Number
fontFamily文字字体String
endArrow(边特有)是否在终点显示箭头Boolean
cursor同css cursorString
... Combo 标签与节点、边标签样式属性相同,
统一整理在Text 图形 API

layoutConfig

名称类型默认值描述
typeStringdendrogram布局类型,支持
dagre、 //层次布局
dendrogram、
compactBox、
mindmap、
indeted。
excludeInvisiblesBooleanfalsev4.8.8 起支持。
布局计算是否排除掉隐藏的节点,若配置为 true,则隐藏节点不参与布局计算。
directionStringLR布局方向,有  
LR,  //自左向右
RL,  //自右向左
TB,  //自上而下
BT,  //自下而上
H,  //垂直
V //水平
说明:
L:左;R:右;T:上;B:下;H:垂直;V:水平。
nodesepFunc: () => 10,fn节点间距
ranksepFunc: () => 10,fn层次间距

方法

graph实例方法

方法作用参数描述
graph.read(data)接收数据,并进行渲染,
r相当于 data 和 render 方法的结合
graph.changeData(data, stack)更新数据,重新渲染
graph.addItem(type, model, stack)新增元素1. type:元素的类型- type:元素的类型
2. model:元素配置项
3. stack:是否入栈
updateItem更新元素1. item:元素的id/实例
2. model:元素配置项
3. stack:是否入栈
graph.removeItem(item, stack)移除元素1. item:元素的id/实例
2. stack:是否入栈
graph.createCombo(combo, elements, stack)创建combo1. combo: combo ID 或 Combo 配置
2. elements:添加到combo中的元素id集合
3. stack:是否入栈
graph.collapseExpandCombo(combo)展开或收缩指定的 Combocombo:combo ID 或 combo 实例combo:
graph.downloadFullImage(name, type, imageConfig)将画布上的元素导出为图片1. name,图片的名称,不指定则为 'graph'
2. type
1. 当renderer配置项值为svg时,不生效,仅会导出svg
2. 可选值:'image/png' / 'image/jpeg' / 'image/webp' / 'image/bmp'
3. imageConfig:
graph.toFullDataURL(callback, type, imageConfig)将画布上元素生成为图片的 URL。1. callback,异步生成 dataUrl 完成后的回调函数,在这里处理生成的 dataUrl 字符串
2. type
1. 当renderer配置项值为svg时,不生效,仅会导出svg
2. 可选值:'image/png' / 'image/jpeg' / 'image/webp' / 'image/bmp'
3. imageConfig:{backgroundColor,padding}callback,

元素(即公共的)实例方法

方法作用
item.getModel/Type()获取元素的当前数据模型(对应配置项)/类型
item.updatePosition(cfg)更新元素的位置,避免整体重新绘制,
cfg中需要包含x,y,如果没有,则按配置更新元素
item.update(model)根据配置项更新元素
item.destroy()销毁元素

节点实例方法

方法作用
node.lock/unlock()锁定/解锁节点,锁定节点后,节点不再响应拖拽事件
node.getNeighbors(type)1. type有'source' / 'target'和不写,
2. 'source' 获取指向当前节点的节点,
3. 'target' 只获取当前节点指向的目标节点
node.getEdges/getIn/OutEdges()getEdges 获取当前节点有关联的所有边(即连线)
getInEdges 获取指向当前节点的边
getOutEdges 获取当前节点指出去的边
node.addEdge/remove(edge)将指定边添加到/移除当前节点

边(连线)的实例方法

方法作用
edge.setSource/Target(node)根据提供的node实例,设置边的开始/结束节点
node.getSource/Target()获取边的开始/结束节点

combo实例方法

方法作用
combo.getChildren/Nodes/Combos()getChildren,获取combo包含的combo和node
getNodes,获取combo包含的node
getCombos,获取combo包含的combo
combo.addChildren/Nodes/Combos(param)接收combo/node实例,添加combo的子combo或子node
combo.removeChildren/Nodes/Combos(param)接收combo/node实例,移除combo的子combo或子node

鼠标事件

需要注意的是,这里的 mousemove 事件和通常的鼠标移动事件有所区别,它需要在鼠标按下后移动鼠标才能触发。

除了 mouseenter 和 mouseleave 外,事件回调函数的参数都包含鼠标相对于画布的位置 x、y 和鼠标事件对象 e 等参数。

事件cell 节点/node 节点/port 连接桩/edge 边/边blank 画布空白区域
单击cell/node/node:port/edge/blank:click
双击cell/node/node:port/edge/blank:dblclick
右键cell/node/node:port/edge/blank:contextmenu
鼠标按下cell/node/node:port/edge/blank:mousedown
移动鼠标cell/node/node:port/edge/blank:mousemove
鼠标抬起cell/node/node:port/edge/blank:mouseup
鼠标滚轮cell/node/-/edge/blank:mousewheel
鼠标进入cell/node/node:port/edge/graph:mouseenter
鼠标离开cell/node/node:port/edge/graph:mouseleave
javascript
graph.on('cell:click', ({ e, x, y, cell, view }) => {})
graph.on('node:click', ({ e, x, y, node, view }) => {})
graph.on('edge:click', ({ e, x, y, edge, view }) => {})
graph.on('blank:click', ({ e, x, y }) => {})
graph.on('cell:mouseenter', ({ e, cell, view }) => {})
graph.on('node:mouseenter', ({ e, node, view }) => {})
graph.on('edge:mouseenter', ({ e, edge, view }) => {})
graph.on('graph:mouseenter', ({ e }) => {})

自定义

自定义箭头

javascript
defaultEdge: {
  type: 'polyline',
  // opacity: 0.3,
  style: {
    stroke: '#000',
    lineWidth: 1,
    endArrow: {
      //M命令是“moveto”的缩写,设置绘制起点
      //L命令是“lineto”的缩写,绘制边到某个坐标
      //Z命令表示返回到路径的起点并闭合路径
      path: `M 0,0 L 0,-6 L 10,0 L 0,6 Z`
      d //箭头偏移量
    }
  }
},

自定义节点类型

动态添加元素

javascript
//注册自定义节点,并取名为dom-node
G6.registerNode('dom-node', {
  //draw 函数定义了如何绘制这种自定义节点。
      //cfg为节点配置项
      //group为图形组,用于添加和管理图形元素
  draw: (cfg, group) => {
    const stroke = cfg.style ? cfg.style.stroke || '#5B8FF9' : '#5B8FF9';
    //group.addShape,在图形组中添加一个名为dom的形状
    const shape=group.addShape('rect', {
      attrs: {
        width: cfg.size[0],
        height: cfg.size[1],
      },
      draggable: true,
    });
    //水平居中
    group.addShape('text', {
      attrs: {
        text: cfg.label || '',
        textAlign: 'center',
        x: cfg.size[0] / 2,
        y: 40,
        fill: '#000'
      }
    });
    return shape;
  },
});

dom(使用vue组件)

注意自定义节点只能渲染静态页面,不能动态变化

javascript
import Vue from 'vue';
import MyComponent from './myComponent';
//注册自定义节点,并取名为dom-node
G6.registerNode('dom-node', {
  //draw 函数定义了如何绘制这种自定义节点。
      //cfg为节点配置项
      //group为图形组,用于添加和管理图形元素
  draw: (cfg, group) => {
    const stroke = cfg.style ? cfg.style.stroke || '#5B8FF9' : '#5B8FF9';
    
    const outDiv = document.createElement('div');
    // 准备传递给 Vue 组件的 props
    const props = cfg;
    // 创建 Vue 实例并挂载到容器
    const app = new Vue({
      render: h => h(MyComponent, { props })
    }).$mount(outDiv);
    //group.addShape,在图形组中添加一个名为dom的形状
    const shape = group.addShape('dom', {
      attrs: {
        width: cfg.size[0],
        height: cfg.size[1],
        html: app.$el.outerHTML, //只支持纯字符串,不支持dom,因此只能写纯静态页面,不能交互
        //html:`<div></div>`,
      },
      draggable: true,
    });
    return shape;
  },
});

jsx

javascript
//注册自定义节点,并取名为rect-jsx
//由于只能用内置节点,用dom报错(不知道为啥),建议用draw函数,见dom
G6.registerNode(
  'rect-jsx',
  //使用jsx,cfg为节点的配置项
  //这里渲染了一个可以拖拽的图片,并设置label水平居中
  (cfg) => `<rect>
              <image style={{
                  img: ${require(`@/assets/${cfg.name}.svg`)},
                  width: ${cfg.size[0]},
                  height:  ${cfg.size[1]}
                }}
                draggable="true"
              />
              <text style={{
                        textAlign:'center',
                        marginLeft:${cfg.size / 2}
                      }}
                >${cfg.label}</text>
          </rect>`);

自定义功能

边的起点终点偏移通过自定义边

javascript
//edges: [{
//    source: '1', // String,必须,起始点 id
//    target: '2', // String,必须,目标点 id
//    startOffsetX: -5,
//    startOffsetY: -15,
//    endOffsetX: 5,
//    endOffsetY: -15
//  }]

G6.registerEdge('edge-flow', {
  draw(cfg, group) {
    let start = cfg.startPoint;
    let end = cfg.endPoint;
    start = {
      x: start.x + (cfg.startOffsetX || 0),
      y: start.y + (cfg.startOffsetY || 0)
    };
    end = {
      x: end.x + (cfg.endOffsetX || 0),
      y: end.y + (cfg.endOffsetY || 0)
    };
    let path = [
      ['M', start.x, start.y],
      ['L', end.x, end.y]
    ];

    let d = 4;
    return group.addShape('path', {
      attrs: {
        stroke: '#F6BD16',
        fill: '#F6BD16',
        path,
        cursor: 'pointer',
        endArrow: {
          path: `M ${d},0 L -${d},-${d + 2} L -${d},${d + 2} Z`,
          d
        },
        lineWidth: 1
      }
    });
  }
});

点击新增边(即连线)

javascript
//如果不需要复杂的添加逻辑,直接写modes:{default:['create-edge']}
G6.registerBehavior('click-add-edge', {
    //设置事件
  getEvents() {
    return {
      'node:click': 'onClick',
      mousemove: 'onMousemove',
      'edge:click': 'onEdgeClick',
    };
  },
  onClick(ev) {
    const self = this;
    const node = ev.item;
    const graph = self.graph;
    const point = { x: ev.x, y: ev.y };
    const model = node.getModel();
    //如果已经新增了边,更新边的目标节点
    if (self.edge) {
      graph.updateItem(self.edge, {
        target: model.id,
      });
      self.edge = null;
    } else {
      self.edge = graph.addItem('edge', {
        source: model.id,
        target: model.id,
      });
    }
  },
  //更新边的目标坐标
  onMousemove(ev) {
    const self = this;
    const point = { x: ev.x, y: ev.y };
    if (self.addingEdge && self.edge) {
      self.graph.updateItem(self.edge, {
        target: point,
      });
    }
  },
  //点击边的时候删除边
  onEdgeClick(ev) {
    const self = this;
    const currentEdge = ev.item;
    if (self.addingEdge && self.edge === currentEdge) {
      self.graph.removeItem(self.edge);
      self.edge = null;
      self.addingEdge = false;
    }
  },
});

点击新增节点

javascript
G6.registerBehavior('click-add-node', {
  //设置事件
  getEvents() {
    return {
      'canvas:click': 'onClick',
    };
  },
  // Click event
  onClick(ev) {
    const self = this;
    const graph = self.graph;
    // Add a new node
    graph.addItem('node', {
      x: ev.canvasX,
      y: ev.canvasY,
      id: `node-${addedCount}`, // Generate the unique id
    });
    addedCount++;
  },
});

通过切换模式实现什么时候执行什么模式

javascript
const graph = new G6.Graph({
  container: 'container',
  width,
  height,
  modes: {
    // Defualt mode
    default: ['drag-node', 'click-select'],
    // Adding node mode
    addNode: ['click-add-node', 'click-select'],
    // Adding edge mode
    addEdge: ['click-add-edge', 'click-select'],
  },
});
graph.data(data);
graph.render();

const mode = graph.getCurrentMode();
//mode对应初始化实例时,mode中的那些key
graph.setMode(mode);

监测窗口大小变化重新渲染

javascript
const container = document.getElementById('container');
//graph为new G6.Graph后得到的实例
if (typeof window !== 'undefined') {
  window.top.addEventListener('resize', () => {
    console.log('resize');
    if (!graph || graph.get('destroyed')) return;
    const clientRect = container.getBoundingClientRect();
    const width = clientRect.width || 1200;
    const height = clientRect.height || 500;
    graph.changeSize(width, height);
  });
}

保存

//获取图数据,把这个数据保存下来,再通过graph.data(保存的数据),
//就能实现保存
graph.save()

工具包

https://g6.antv.antgroup.com/api/plugins

tooltip(使用vue组件)

javascript
import Tooltip from './Tooltip';
import Vue from 'vue';
const tooltip = new G6.Tooltip({
  offsetX: 10,
  offsetY: 10,
  fixToNode: [1, 0.5], //相对于节点的定位
  // 允许出现 tooltip 的 item 类型
  itemTypes: ['node', 'edge'],
  // custom the tooltip's content
  // 自定义 tooltip 内容
  getContent: (e) => {
    const outDiv = document.createElement('div');
    outDiv.id = 'outDiv';
    outDiv.style.width = 'fit-content';
    // 准备传递给 Vue 组件的 props
    const tooltipProps = {model: e.item.getModel()};

    // 创建 Vue 实例并挂载到容器
    const app = new Vue({
      render: h => h(Tooltip, { props: tooltipProps })
    }).$mount(outDiv);
    // 返回html会作为tooltip的内容
    // tooltip会将这个html节点appendChild到tooltip容器中,因此只返回app.$el,减少一个div渲染
    return app.$el;
  }
});

工具栏ToolBar**(使用vue组件)**

可以通过getContent自定义,

不写getContent的时候,为默认工具栏具有[栈撤销,栈回退,放大,缩小,1:1适应,画布适应] 功能

https://g6.antv.antgroup.com/zh/examples/tool/toolbar/#toolbar

javascript
import ToolBar from './ToolBar';
import Vue from 'vue';
//实例化时,根配置enabledStack:true时,可使用toolbar.undo()进行撤销操作
const toolbar = new G6.ToolBar({
  // container: tc,
  className: 'g6-toolbar-ul',
  getContent: () => {
    // 在哪些类型的元素上响应
  itemTypes: ['node', 'edge', 'canvas'],
  getContent(e) {
    const outDiv = document.createElement('div');
    outDiv.id = 'outDiv';
    outDiv.style.width = 'fit-content';
    const props = {model: e.item.getModel()};
    const app = new Vue({
      render: h => h(ToolBar, { props })
    }).$mount(outDiv);
    // 只支持纯字符串,不支持dom,因此只能使用静态页面,不能交互,交互交给handleMenuClick
    return app.$el.outerHTML;
  },
  //code是点击的工具栏中dom元素身上code属性的值
  handleClick: (code, graph) => {
    if (code === 'undo') {
      toolbar.undo();
    } else if (code === 'redo') {
      toolbar.redo();
    }
  },
});

右键菜单Menu(使用vue组件)

javascript
import Menu from './Menu';
import Vue from 'vue';
const contextMenu = new G6.Menu({
  // 在哪些类型的元素上响应
  itemTypes: ['node', 'edge', 'canvas'],
  getContent(e) {
    const outDiv = document.createElement('div');
    outDiv.id = 'outDiv';
    outDiv.style.width = 'fit-content';
    // 准备传递给 Vue 组件的 props
    const props = {model: e.item.getModel()};

    // 创建 Vue 实例并挂载到容器
    const app = new Vue({
      render: h => h(Menu, { props })
    }).$mount(outDiv);
    // 只支持纯字符串,不支持dom,因此只能使用静态页面,不能交互,交互交给handleMenuClick
    return app.$el.outerHTML;
  },
  //target是点击的右键菜单的dom元素,item是触发上下文的元素
  handleMenuClick: (target, item) => {},
  offsetX: 16 + 10, // 水平偏移量,需要加上父级容器的 padding-left 与自身偏移量 10
  offsetY: 0,       // 垂直偏移量
});

缩略图

javascript
const minimap = new G6.Minimap({
    size: [150, 100],
    //container,      //容器,若不指定会生成一个
    //className,      //生成dom元素的className
    type: 'delegate' //'default':渲染图上所有图形;'keyShape':只渲染图上元素的 keyShape;'delegate':只渲染图上元素的大致图形
    //hideEdge: false //是否隐藏边
});

对其线

javascript
const snapLine = new G6.SnapLine();

网格图

javascript
const grid = new G6.Grid({
    img,    //base64 格式字符串
});

一套写好的计算坐标的方法

javascript
//偶数计算矩形
//center为从每一层从中间向两侧排列的三角形
//剩下一个是每一层从左到右排列的三角形
    fn(nodeCount){
      function calculateTrianglePositions(nodes, nodeWidth, nodeHeight, containerWidth,center) {
        const positions = [];
        const centerX = containerWidth / 2; // 容器中心x坐标
        //偶数逻辑
        if(nodes%2===0){
          let cols=Math.ceil(Math.sqrt(nodes)); //每行个数
          let level = 1; // 当前层数,从1开始
          let xOffset =centerX - Math.floor(cols/2)*nodeWidth; // 当前层的x坐标偏移量
          let yOffset = 0; // 当前层的y坐标偏移量
          let currentIndex=0; //当前层的第几个元素
          for (let i = 0; i < nodes; i++) {
            // 如果当前节点数超过了当前层的节点数,则进入下一层
            if (currentIndex === cols) {
              // eslint-disable-next-line no-unused-vars
              level++; // 层数递增
              yOffset += 10;
              currentIndex=0;
            }
            // 计算当前节点的x坐标(基于偏移量和节点在当前层的位置)
            const x = xOffset + currentIndex * nodeWidth;
            // 计算当前节点的y坐标(基于层数和节点高度)
            const y = yOffset; // 等边三角形排列的y坐标
            currentIndex++;
            positions.push({ x, y });
          }
        }
        else if (center){
          //奇数且居中逻辑
          let level = 1; // 当前层数,从1开始
          let xOffset = 0; // 当前层开始排列的第一个元素相对于中间的偏移量
          let yOffset = 0; // 当前层的y坐标偏移量
          let currentIndex=0; //当前层的第几个元素
          let leftCurrentIndex=0; //当前层左边添加的几个元素
          let rigthCurrentIndex=0; //当前层右边添加的第几个元素
          let isOdd=true; //是否是奇数行且为第一个元素
          let isRight=false; //当前是否是在添加右边
          for (let i = 0; i < nodes; i++) {
            // 如果当前节点数超过了当前层的节点数,则进入下一层
            if (currentIndex === level) {
              currentIndex=0;
              level++; // 层数递增
              isOdd=level%2===1;
              console.log('奇数',level%2)
              xOffset =isOdd?10: 5; // 更新x坐标偏移量
              yOffset += 10;
              leftCurrentIndex=0;
              rigthCurrentIndex=0;
              // if(overCount<level){
              //
              // }
            }
            let x;
            //天衍四十九,遁去其一,这便是那遁去的一,道爷,你悟了吗
            if(isOdd){
            // 计算当前节点的x坐标(基于偏移量和节点在当前层的位置)
              x = centerX;
              isOdd=false;
              console.log('isOdd',x)
            }else{
              if(isRight){
                x = centerX+ xOffset + rigthCurrentIndex*nodeWidth;
                rigthCurrentIndex++;
                console.log('isRight',x)
              }else{
                x = centerX-xOffset - leftCurrentIndex*nodeWidth;
                leftCurrentIndex++;
                console.log('isLeft',x,xOffset,leftCurrentIndex)
              }
              isRight=!isRight;
            }
            currentIndex++;
            // 计算当前节点的y坐标(基于层数和节点高度)
            const y = yOffset; // 三角形排列的y坐标
            positions.push({ x, y });
          }
        }
        else{
          //奇数逻辑
          let level = 1; // 当前层数,从1开始
          let xOffset = 0; // 当前层的x坐标偏移量
          let yOffset = 0; // 当前层的y坐标偏移量
          let currentIndex=0; //当前层的第几个元素
          for (let i = 0; i < nodes; i++) {
            // 如果当前节点数超过了当前层的节点数,则进入下一层
            if (currentIndex === level) {
              level++; // 层数递增
              xOffset -= 5; // 更新x坐标偏移量
              yOffset += 10;
              currentIndex=0;
              // if(overCount<level){
              //
              // }
            }
            currentIndex++;
            // 计算当前节点的x坐标(基于偏移量和节点在当前层的位置)
            const x = centerX+ xOffset + currentIndex*nodeWidth;
            // 计算当前节点的y坐标(基于层数和节点高度)
            const y = yOffset; // 等边三角形排列的y坐标
            positions.push({ x, y });
          }
        }

        return positions;
      }

// 示例
//   const nodeCount = 7; // 节点个数
      const nodeWidth = 10; // 节点宽度
      const nodeHeight = 10; // 节点高度
      const containerWidth = 100; // 容器宽度
      const positions = calculateTrianglePositions(nodeCount, nodeWidth, nodeHeight, containerWidth,true);
// 打印结果
      positions.forEach((pos, index) => {
        console.log(`Node ${index + 1}: (${pos.x}, ${pos.y})`);
      });
      return positions;
    },