说明
适合画连线的图,不适合复杂操作
例如:
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的Id | String | combos: [ { id: 'c1', parentId: 'c2' }, { id: 'c2'}, ], |
offset | 折现的偏移量,就是从多长开始折 | Number | |
source/targetAnchor | 边的起始/终止起点连接的连接桩的下标 | Number | targetAnchor:0 |
source/target | 连线的起点/终点对应的(节点/combo的id/{x,y}) | String | |
anchorPoints | 设置元素的锚点 | Array | anchorPoints:[ [0, 0.5], [1, 0.5], ] |
x/y | 节点的坐标 | Number | |
r | /圆的半径 | Number | |
id | 标识 ID, | String | |
opacity | 透明度 | Number | opacity:0.3 |
size | edge中对应边的粗细,size:1, | Array | |
style | 元素 keyShape 的样式具体参见各图形样式属性 | Object | 见styleConfig |
type | 元素的类型,不传则使用默认值, | String | |
label | 元素的文本标签,有该字段时默认会渲染 label | String | |
labelCfg | 元素label标签的配置项 | Object | |
rx/ry | 水平垂直方向的锐角,类似border-radio | Number |
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(边特有) | 标签文字是否跟随边旋转,默认false | Boolean |
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 cursor | String |
... Combo 标签与节点、边标签样式属性相同, 统一整理在Text 图形 API |
layoutConfig
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
type | String | dendrogram | 布局类型,支持 dagre、 //层次布局 dendrogram、 compactBox、 mindmap、 indeted。 |
excludeInvisibles | Boolean | false | v4.8.8 起支持。 布局计算是否排除掉隐藏的节点,若配置为 true,则隐藏节点不参与布局计算。 |
direction | String | LR | 布局方向,有 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) | 创建combo | 1. combo: combo ID 或 Combo 配置 2. elements:添加到combo中的元素id集合 3. stack:是否入栈 |
graph.collapseExpandCombo(combo) | 展开或收缩指定的 Combo | combo: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和nodegetNodes ,获取combo包含的nodegetCombos ,获取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;
},