自定义节点高度不一致的时候, 节点无法居中对齐, 导致直线变成了折线 #5231
oo10 posted onGitHub
问题描述
minmap 里自定义节点高度不一致的时候, 直线变成了折线相连, 节点无法居中对齐。 请问应该如何解决?
重现链接
*
重现步骤
const TREELIST = {
id: '1',
name: '分析',
children: [
{
id: '1-1指标ID',
name: '收入',
compareText: '环比',
contributionText: '贡献度',
nodeType: 'IND',
nowValue: 19,
hisValue: 10,
changeRate: 0.1,
contributionDegree: 1.2,
children: [
{
id: '1-1-1',
name: '利息收入',
children: [
{
id: '1-1-1-1',
name: '利息收入111'
},
{
id: '1-1-1-2',
name: '罚息收入222'
}
]
},
{
id: '1-1-2',
name: '罚息收入'
}
]
},
{
id: '1-2',
name: '支出',
compareText: '环比下降',
contributionText: '贡献度',
nodeType: 'IND',
nowValue: 17888,
hisValue: 10,
changeRate: -0.9,
contributionDegree: -1.9,
children: [
{
id: '1-5-1',
name: '第三层1',
nodeType: 'IND',
children: [
{
id: '1-5-1-1',
name: '第四层1第四层1第四层1第四层1第四层1',
children: [
{
id: '1-5-1-1-1',
name: '支出111',
nodeType: 'IND'
}
]
}
]
}
]
}
]
// }
// ]
};
// 文本超出隐藏 (字段, 最大长度, 字体大小)
const fittingString = (str, maxWidth, fontSize) => {
str = String(str);
const ellipsis = '...';
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
// Chinese charactors
currentWidth += fontSize;
} else {
// get the width of single letter according to the fontSize
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = ${str.substr(0, i)}${ellipsis}
;
}
});
return res;
};
// 获取文本的长度
const getTextSize = (str, maxWidth, fontSize) => {
let width = G6.Util.getTextSize(str, fontSize)[0];
return width > maxWidth ? maxWidth : width;
};
// 自定义节点
G6.registerNode('tree-node', {
draw(cfg, group) {
let rect;
// 指标节点
if (cfg.nodeType === 'IND') {
rect = group.addShape('rect', {
attrs: {
x: 0, // x 轴移动距离
y: 0, // y 轴移动距离
width: 180, // 宽
height: 120, // 高
fill: '#EBF0FA',
stroke: '#EBF0FA', // 边框色
// fontSize: 16,
fontWeight: 600,
radius: 4
},
name: 'big-rect-shape-IND'
});
// 指标名称
group.addShape('text', {
attrs: {
text: fittingString(cfg.name, 180, 14),
x: 12,
// y + padding
y: 8 + 4,
fontSize: 14,
textAlign: 'left',
textBaseline: 'top',
fill: '#22365E',
lineHeight: 22
},
name: 'text-shape'
});
// 指标数据
group.addShape('text', {
attrs: {
text: fittingString(cfg.nowValue, 180, 20),
x: 12,
// y + padding
y: 28 + 4,
fontSize: 20,
textAlign: 'left',
textBaseline: 'top',
fill: '#000000',
lineHeight: 28
},
name: 'text-shape'
});
// 分隔直线
group.addShape('path', {
attrs: {
path: [
['M', 78 + 12, 60],
['L', 78 + 12, 61]
],
stroke: '#d8e1f5',
lineWidth: 156,
lineAppendWidth: 1
},
// 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
name: 'path-shape'
});
// 对比数据
group.addShape('text', {
attrs: {
text: fittingString(cfg.compareText, 180, 14),
x: 12,
// y + padding
y: 65 + 4,
fontSize: 14,
textAlign: 'left',
textBaseline: 'top',
fill: '#000000',
lineHeight: 22
},
name: 'text-shape'
});
group.addShape('text', {
attrs: {
text: fittingString(cfg.changeRate * 100 + '%', 180, 14),
x: 180 - 12,
// y + padding
y: 65 + 4,
fontSize: 14,
textAlign: 'right',
textBaseline: 'top',
fill: cfg.changeRate > 0 ? '#E53232' : '#4DB378',
lineHeight: 22
},
name: 'text-shape'
});
// 贡献度数据
group.addShape('text', {
attrs: {
text: fittingString(cfg.contributionText, 180, 14),
x: 12,
// y + padding
y: 89 + 4,
fontSize: 14,
textAlign: 'left',
textBaseline: 'top',
fill: '#000000',
lineHeight: 22
},
name: 'text-shape'
});
if (cfg.children && cfg.children.length > 0) {
group.addShape('marker', {
attrs: {
x: 180 + 10,
y: 60,
r: 6,
symbol: cfg.collapsed ? G6.Marker.expand : G6.Marker.collapse,
stroke: '#666',
fill: '#fff',
lineWidth: 1,
cursor: 'pointer' // 鼠标变手
},
name: 'collapse-icon'
});
}
} else {
rect = group.addShape('rect', {
attrs: {
x: 0, // x 轴移动距离
y: 0, // y 轴移动距离
width: getTextSize(cfg.name, 180, 16) + 20, // 宽
height: 40, // 高
fill: '#EBF0FA',
stroke: '#EBF0FA', // 边框色
fontSize: 16,
fontWeight: 600,
radius: 4
},
name: 'big-rect-shape'
});
group.addShape('text', {
attrs: {
text: fittingString(cfg.name, 180, 16),
x: 10,
y: 20,
fontSize: 16,
textAlign: 'left',
textBaseline: 'middle',
fill: '#000'
},
name: 'text-shape'
});
}
if (cfg.depth === 0) {
rect = group.addShape('rect', {
attrs: {
x: 0, // x 轴移动距离
y: 0, // y 轴移动距离
width: getTextSize(cfg.name, 180, 20) + 48, // 宽
height: 60, // 高
fill: '#466EC0',
stroke: '#466EC0', // 边框色
fontSize: 16,
fontWeight: 600,
radius: 4
},
name: 'big-rect-shape'
});
group.addShape('text', {
attrs: {
text: fittingString(cfg.name, 180, 20),
x: 24,
y: 30,
fontSize: 20,
textAlign: 'left',
textBaseline: 'middle',
fill: '#fff'
},
name: 'text-shape'
});
}
return rect;
},
// afterDraw: function (cfg, group) {
// const children = group.get('children');
// console.log(children.length);
// console.log(cfg, group);
// // 将所有图形平移
// children.forEach((child) => {
// const { x, y } = child.attr();
// const size = [cfg.width, cfg.height]
// console.log(99999, height)
// console.log('cfg.size', cfg.size);
// child.attr({
// x: x + size.width / 2,
// y: y + size.height / 2
// });
// });
// },
setState: (name, value, item) => {
console.log('自定义节点--->setState:', name, value, item);
if (name === 'collapsed') {
console.log('item-group', item.get('group'));
const marker = item.get('group').findAll((ele) => ele.get('name') === 'collapse-icon');
console.log('marker--->', marker);
marker[0] && marker[0].attr('symbol', value ? G6.Marker.collapse : G6.Marker.expand);
// 如果是根节点需要处理两个marker
if (item._cfg.model.depth === 0) {
marker[1] && marker[1].attr('symbol', value ? G6.Marker.collapse : G6.Marker.expand);
}
}
}
});
// 自定义边
G6.registerEdge('kaimo-line', {
/**
* 绘制边,包含文本
* @param {Object} cfg 边的配置项
* @param {G.Group} group 图形分组,边中的图形对象的容器
* @return {G.Shape} 绘制的图形,通过 node.get('keyShape') 可以获取到
*/
draw(cfg, group) {
console.log(cfg);
const startPoint = cfg.startPoint;
const endPoint = cfg.endPoint;
let shape = group.addShape('path', {
// 线条
attrs: {
stroke: '#dfe2ec',
path: [
['M', startPoint.x, startPoint.y],
['L', endPoint.x / 3 + (2 / 3) * startPoint.x, startPoint.y],
['L', endPoint.x / 3 + (2 / 3) * startPoint.x, endPoint.y],
['L', endPoint.x, endPoint.y]
],
endArrow: {
path: G6.Arrow.triangle(5, 5, 0), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
d: 0,
fill: '#dfe2ec',
opacity: 0.5,
lineWidth: 1
}
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: 'path-shape'
});
return shape;
}
});
// 自定义事件
G6.registerBehavior('kaimo-behavior', {
getEvents() {
return {
'node:click': 'onNodeClick'
};
},
// 自定义单击事件
onNodeClick: (evt) => {
// 单击展开/收起
console.log('自定义单击事件--->', evt, graph);
evt.item._cfg.model.collapsed = !evt.item._cfg.model.collapsed;
// 更改 item 的状态,触发自定义节点:setState
graph.setItemState(evt.item, 'collapsed', !evt.item.getModel().collapsed);
graph.layout();
}
});
// 定义画布的宽高
const width = document.getElementById('g6-container').scrollWidth || 1600;
const height = document.getElementById('g6-container').scrollHeight || 960;
// 实例化G6
// 因为我们用的是树图,所以这里是G6.TreeGraph(),还有其他,像是普通图的配置G6.Graph(),一般y用的比较多的就像是树图这种,还有组织架构图一类的。
const graph = new G6.TreeGraph({
// 图的 DOM 容器,对应上面我们定义的id
container: 'g6-container',
width,
height,
// 设置画布的交互模式
modes: {
default: [
// 自定义事件
'kaimo-behavior',
// 拖拽画布
'drag-canvas',
// 缩放画布
'zoom-canvas'
]
},
// 配置节点的属性
defaultNode: {
// 节点类型,cicle:圆形,rect:矩形,ellipse:椭圆,diamond:菱形,triangle:三角形,star:五角星,image:图片,modelRect:卡片
type: 'tree-node',
// 节点样式
style: {
// 鼠标经过是的形状,跟css是一样的。
cursor: 'pointer',
// 圆角
radius: 4
},
anchorPoints: [
[1, 0.5],
[0, 0.5]
]
},
// 配置边的属性
defaultEdge: {
// 指定边的类型,可以是内置边的类型名称,也可以是自定义边的名称。
// line:直线,polyline:折线,arc:圆弧线,quadratic:二阶贝塞尔曲线,cubic:三阶贝塞尔曲线,cubic-vertica:垂直方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点。cubic-horizontal:水平方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点。loop:自环
type: 'kaimo-line'
},
// 布局配置项
layout: {
// 布局名称,这个可就太多了,这个只是树图结构中的一种。比如还有compactBox:紧凑树布局,dendrogram:生态树布局,indented:缩紧树布局。
type: 'mindmap', // 脑图树布局
direction: 'H', // H / V / LR / RL / TB / BT 这些是控制节点分布位置,从左往右、从右往左、从中间往上下延伸、从中间往左右延伸...具体可以看官网
workerEnabled: true, // 开启 Web-Worker
// 节点 id 的回调函数
getId: function getId(d) {
return d.id;
},
// 下面都是一些控制节点与节点间距离的回调函数,具体可以试着修改一下值。
// 节点高度的回调函数
getHeight: function getHeight(cfg) {
console.log(cfg);
return 120;
},
// 节点宽度的回调函数
getWidth: function getWidth(cfg) {
return 180;
},
// 节点纵向间距的回调函数
getVGap: function getVGap() {
return 10;
},
// 节点横向间距的回调函数
getHGap: function getHGap(val) {
return 30;
},
// 这个getSide就是控制节点位置的属性了,通过数据结构中定义的值做判断,来控制左右,
// 注意的是这个官方写的只能return 'left'和'right',当我们的树结构是竖着的呢?难道是用top和bottom?这里我也试过了,用top和bottom是不好使的,因为人家官方确确实实的只有left和right,通过尝试,其实left就对应top,right对应bottom,所以要控制节点在上面就写left,在下面就写right。
getSide: (node) => {
if (node.data.state === 'left') {
return 'left';
}
return 'right';
}
},
// 动画属性
animate: false
});
// 默认全部展开
G6.Util.traverseTree(data, function (item) {
item.collapsed = false;
});
// 初始化的图数据
graph.data(data);
// 根据提供的数据渲染视图。
graph.render();
// 让画布内容适应视口
graph.fitView();
预期行为
只有单个节点的时候,应该垂直居中用直线相连。
平台
- 操作系统: [macOS, Windows, Linux, React Native ...]
- 网页浏览器: [Google Chrome, Safari, Firefox]
- G6 版本: [4.5.1 ... ]
屏幕截图或视频(可选)
<img width="1112" alt="image" src="https://github.com/antvis/G6/assets/15247637/acd5c670-e709-4551-bf71-8c681076cd8c">
补充说明(可选)
No response