AntV G6 5.0+实战:自定义树节点、数据重渲染、动态显隐控制与右键菜单全实现
antv/g6定义节点、边、combo
·
1. 画布只重新渲染数据
- graph.render = graph.draw+graph,fitview()+graph.fitCenter()
- setData塞入新的数据
const updateGraph = (data) => {
if (!graph) {
console.warn("Graph is not initialized");
return;
}
graph.clear();
graph.setData(data);
graph.render();
};
2. 画布修改大小
const resizeGraph = (width, height) => {
if (!graph) return;
graph.setSize(width, height);
};
3. 获取画布宽高
- 使用ref元素获取
panelRef.value.$el.clientWidth/cilentHeight
- 如果这个div块是一会儿显示一会儿隐藏,为了1获取宽高生效,在隐藏的时候,css:
display:none
不行,读出来的一直都是0,应该用display:hidden
4. 自定义combo、node、edge的样式
1. 节点
- 节点/边设置
node: {
style: (d) => {
return setNodeStyle(d);
},
},
edge: {
type: "cubic-vertical",
style: (d) => {
return setEdgeStyle(data, d);
},
},
- 节点大小、标签、标签背景、字体大小、边框虚实等样式都是定义在style里,style和type是并列元素
- 标签过长换行: labelMaxWidth是基于节点的宽度设置的百分比。
labelWordWrap: true,
labelMaxLines: 10,
labelMaxWidth: "500%"
- 完整配置
const setNodeStyle = (d) => {
//flag:true-真实节点;false-虚拟节点
let style = {
size: NodeColor.find((item) => +item.category === +d.category)?.size || 16, //节点大小
labelText: d.name,
labelPlacement: "bottom",
lineWidth: d.flag ? 0 : 1,
lineDash: [5, 1],
stroke: fillNodeColor(d), //节点边框颜色
// labelOffsetX: 8,
labelOffsetY: 4,
labelTextAlign: "center",
labelFontSize: 8,
labelWordWrap: true,
labelMaxLines: 10,
labelMaxWidth: "500%",
labelFontStyle: "italic",
labelBackground: true,
labelBackgroundFill:
"linear-gradient(rgba(230,100,101,0.12), rgba(145,152,229,0.12))",
labelBackgroundStroke: "rgba(230,100,101,0.4)",
labelBackgroundRadius: 2,
ports: [{ placement: "top" }, { placement: "bottom" }],
fill: fillNodeColor(d), //节点颜色
};
return style;
}
- 定义节点边框虚实:lineDash,边框颜色:stroke;边宽:lineWidth
- 定义节点填充色:fill
2. combo
- combo设置
combo: {
type: "rect",
style: (d) => {
return setComboStyle(d);
},
},
- combo样式
const setComboStyle = (d) => {
let style = {
padding: 15,
lineDash: [5, 5],
lineWidth: 1,
radius: 4,
fill: "rgba(120,99,255,0.5)",
zIndex: 10, //不让点击combo里的节点,而是点击combo弹框
labelText: JSON.stringify(d.count) || "0", //必须是string类型,number类型会报错
labelPlacement: "bottom",
lineWidth: 1,
lineDash: [5, 1],
stroke: "rgba(120,99,255,0.5)", //节点边框颜色
labelOffsetY: 10,
labelFontSize: 12,
labelFontStyle: "italic",
};
return style;
};
5. 动态控制树节点的显示和隐藏
1. 项目背景
当前树形结构包含多级层级关系,若一次性展示所有叶子节点会导致节点过小,难以看清详细信息。存在以下难点:
- Antv/G6有combo分组,可以展开收起,但是每次点击combo收起后无法重新渲染布局,显示出来的节点仍然很小。
- 如果设置combo默认收起,导致初始后combo节点位置为(0,0)并不对,点击展开后所有子节点也都重叠在了一起在(0,0)的位置,不会重新渲染到响应位置。
- Antv/g6有
Graph.setElementVisibility(id, visibility, animation)
设置节点隐藏,但是无法设置边的隐藏,其一边的数据结构只有source、target
,没有id,其二,假设加了edgeId,需要根据隐藏的节点找出对应的边,对于数据量大的图,这不是一个好性能的方法。
为此,我们采用以下交互设计: - 初始状态下,每个父节点默认仅显示两个叶子节点
- 双击父节点时,将展开显示其所有叶子节点
- 再次双击该父节点,则收起所有叶子节点
2. 实现方式
- 注意:初始默认节点隐藏,那么initGraph传入的data就应该是setElementHidden后的数据。
1. 初始化画布
const initGraph = (data) => {
//如果之前有数据,先清空再重新渲染
if (graph) {
graph?.clear();
}
let container = document.getElementById("container");
let width = container?.scrollWidth || 800;
let height = container?.scrollHeight || 500;
let graphData = setElementHidden(data);
graph = new Graph({
container: container,
autoFit: "view",
data: graphData,
behaviors: ["drag-canvas", "zoom-canvas"],
node: {
style: (d) => {
return setNodeStyle(d);
},
},
edge: {
type: "cubic-vertical",
style: (d) => {
return setEdgeStyle(data, d);
},
},
layout: {
type: "compact-box",
direction: "TB",
getVGap: function getVGap() {
return 72;
},
},
2. 设置元素隐藏:我的要求是隐藏category是3的节点及其所有子节点,只保留两个;优势就是用到了Set,简化了代码
const setElementHidden = (data) => {
let hiddenTypeNodes = new Set(
data.nodes
.filter((node) => node.index > 2 && +node.category === 3)
.map((node) => node.id)
);
//hiddenTypeNodes作为source的对应的target节点
let hiddenParamNodes = new Set(
data.edges
.filter((edge) => hiddenTypeNodes.has(edge.source))
.map((edge) => edge.target)
);
let allHiddenNodes = new Set([...hiddenTypeNodes, ...hiddenParamNodes]);
let filteredNodes = data.nodes.filter((node) => !allHiddenNodes.has(node.id));
let filteredEdges = data.edges.filter((edge) => {
// 默认过滤逻辑
return !allHiddenNodes.has(edge.source) && !allHiddenNodes.has(edge.target);
});
_data.nodes = filteredNodes;
_data.edges = filteredEdges;
_data.combos = data.combos || [];
return _data;
};
3. 设置部分元素可见:只双击category是2的节点能够展开/收起节点,再次双击展开这个节点对应的category=3的全部节点及其所有子节点
const setPartElementVisible = (data, taskId) => {
// 找出未被选中的任务节点
let _notSelectedTaskNodes = new Set(
data.nodes
.filter((node) => +node.category === 2 && node.id != taskId)
.map((node) => node.id)
);
let _notSelectedTypeNodes = new Set(
data.edges
.filter((edge) => _notSelectedTaskNodes.has(edge.source))
.map((edge) => edge.target)
);
let hiddenTypeNodes = new Set(
data.nodes
.filter(
(node) =>
_notSelectedTypeNodes.has(node.id) &&
node.index > 2 &&
+node.category === 3
)
.map((node) => node.id)
);
let hiddenParamNodes = new Set(
data.edges
.filter((edge) => hiddenTypeNodes.has(edge.source))
.map((edge) => edge.target)
);
let allHiddenNodes = new Set([...hiddenTypeNodes, ...hiddenParamNodes]);
let filteredNodes = data.nodes.filter((node) => !allHiddenNodes.has(node.id));
let filteredEdges = data.edges.filter((edge) => {
// 默认过滤逻辑
return !allHiddenNodes.has(edge.source) && !allHiddenNodes.has(edge.target);
});
_data.nodes = filteredNodes;
_data.edges = filteredEdges;
_data.combos = data.combos || []; // 保留 combos(如果存在)
return _data;
};
4. 双击事件重新渲染画布
- preNode就是记住上次点击的节点,如果和上次一样,是要收起;如果和上次不一样,是要展开。
graph.on(NodeEvent.DBLCLICK, async (e) => {
if (clickTimer.value) {
clearTimeout(clickTimer.value);
clickTimer.value = null;
}
let newData = {};
let nodeDetail = await getNodeDetail(e?.target?.id);
//任务节点:双击展开,再双击收起
if (+nodeDetail.category === 2) {
if (preNodeId.value == e?.target?.id) {
newData = setElementHidden(data, e?.target?.id);
preNodeId.value = "";
} else {
newData = setPartElementVisible(data, e?.target?.id);
preNodeId.value = e?.target?.id;
}
updateGraph(newData);
}
});
5. 画布节点单击/双击阻止冒泡事件,不要互相影响
let clickTimer = ref();
//点击节点,弹出详情页面
graph.on(NodeEvent.CLICK, async (e) => {
if (clickTimer.value) {
clearTimeout(clickTimer.value);
clickTimer.value = null;
}
clickTimer.value = setTimeout(async () => {
//TODO:具体业务逻辑
}, 200);
});
graph.on(NodeEvent.DBLCLICK, async (e) => {
if (clickTimer.value) {
clearTimeout(clickTimer.value);
clickTimer.value = null; //单击设置了clickTimer,如果有说明在单击事件,不做任何操作
}
//TODO:具体业务逻辑
}
});
6. contextMenu加图标
- 官网给出的右键菜单只能显示文本,但是加图标能更好看些。
- 图标是用deepseek生成的(目前就依赖这种了!)
- 也可以使用unicode编码,用html字体实现,但我从iconfont上找到Unicode编码填入后无法加载
plugins: [
{
type: "contextmenu",
trigger: "contextmenu",
enable: (e) => e.targetType === "node",
getItems: (e) => {
[
{ name: "➕ 新增子节点", value: "add" },
{ name: "❌ 删除节点", value: "delete" },
];
},
onClick: async (value, target, current) => {
//TODO:具体业务逻辑
},
},

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)