D3 toggle按钮控制多边形显隐仅最后一个可隐藏问题排查
问题说明
开发数据可视化网页过程中,需读取标注文件坐标数据渲染多边形边界框,基础多边形渲染功能运行正常,但基于d3实现的显隐切换功能存在异常:
- 首次点击toggle按钮可正常展示所有多边形
- 再次点击触发隐藏逻辑时,仅1个多边形被隐藏
- 初步定位原因:for循环逐行渲染多边形时,最后一次迭代的
bb变量值会覆盖之前所有赋值,传入判断逻辑时仅能获取最后一个边界框实例,因此仅能隐藏最后一个多边形;尝试将所有bb值存入数组传入判断的方案未生效。
问题代码
页面按钮代码:
<button>toggle polygon </button>
功能逻辑代码:
var bb; d3.select('button').on('click', function() { if ( bb ) { bb.remove(); // Remove bounding box bb = null; } else { for (var line = 2; line < lines.length; line++) { //array to loop through each lines of the annotation file console.log(line + " --> " + lines[line]); var annotationline = lines[line].split(' '), x1 = annotationline[0], y1 = annotationline[1], x2 = annotationline[2], y2 = annotationline[3], x3 = annotationline[4], y3 = annotationline[5], x4 = annotationline[6], y4 = annotationline[7], bb= d3.select(template).append("svg") .attr("width", width) .attr("height", height), poly = [{ "x": x1, "y": y1 }, { "x": x2, "y": y2 }, { "x": x3, "y": y3 }, { "x": x4, "y": y4 } ]; bb.selectAll("polygon") .data([poly]) .enter().append("polygon") .attr("points", function(d) { return d.map(function(d) { return [d.x, d.y].join(","); }).join(" "); }) .attr("stroke","red") .attr("fill", "none"); } } }
根因分析
代码存在两处核心问题:
- 循环内每次迭代都会新建svg实例,并覆盖外层作用域声明的
bb变量,循环结束后bb仅保留最后一个svg实例的引用,触发删除逻辑时自然只能操作最后一个元素 - 之前尝试的数组存储方案未生效,通常是因为没有在循环内将每一次创建的实例正确推入数组,也没有在删除环节遍历数组逐个执行移除操作
- 额外性能缺陷:为每个多边形单独创建svg容器会产生大量冗余DOM节点,完全无必要
修复方案
推荐实现(性能最优)
不需要反复删除/重建DOM,提前创建唯一的svg容器,所有多边形渲染到同一个svg内,通过CSSdisplay属性控制显隐,仅首次点击时执行渲染,后续切换仅修改样式属性:
// 提前初始化svg容器,默认隐藏 const containerSvg = d3.select(template) .append("svg") .attr("width", width) .attr("height", height) .style("display", "none"); d3.select('button').on('click', function() { const isHidden = containerSvg.style("display") === "none"; if (isHidden) { // 仅首次展示时渲染多边形,避免重复渲染 if (containerSvg.selectAll("polygon.bbox").empty()) { const polygonData = []; // 批量组装所有多边形坐标数据 for (let line = 2; line < lines.length; line++) { const coords = lines[line].split(' '); polygonData.push([ {x: coords[0], y: coords[1]}, {x: coords[2], y: coords[3]}, {x: coords[4], y: coords[5]}, {x: coords[6], y: coords[7]} ]); } // 批量渲染多边形,添加统一类名方便管理 containerSvg.selectAll("polygon.bbox") .data(polygonData) .enter() .append("polygon") .attr("class", "bbox") .attr("points", d => d.map(p => `${p.x},${p.y}`).join(" ")) .attr("stroke", "red") .attr("fill", "none"); } containerSvg.style("display", null); } else { containerSvg.style("display", "none"); } })
兼容原删除逻辑的实现
如果必须保留删除/重建DOM的交互逻辑,将单实例变量改为数组存储所有创建的svg实例,删除时遍历数组逐个移除即可:
let bbInstances = []; d3.select('button').on('click', function() { if (bbInstances.length) { bbInstances.forEach(bb => bb.remove()); bbInstances = []; } else { for (var line = 2; line < lines.length; line++) { const annotationline = lines[line].split(' '), x1 = annotationline[0], y1 = annotationline[1], x2 = annotationline[2], y2 = annotationline[3], x3 = annotationline[4], y3 = annotationline[5], x4 = annotationline[6], y4 = annotationline[7], // 用块级变量声明当前实例,避免变量提升导致的覆盖问题 currentBb = d3.select(template).append("svg") .attr("width", width) .attr("height", height), poly = [ {"x": x1, "y": y1}, {"x": x2, "y": y2}, {"x": x3, "y": y3}, {"x": x4, "y": y4} ]; currentBb.selectAll("polygon") .data([poly]) .enter().append("polygon") .attr("points", function(d) { return d.map(function(d) { return [d.x, d.y].join(","); }).join(" "); }) .attr("stroke","red") .attr("fill", "none"); // 将当前实例存入数组 bbInstances.push(currentBb); } } }
注意:原代码循环内变量声明混用逗号运算符,很容易引发变量作用域、赋值逻辑错误,建议不同变量间使用分号分隔,块级作用域变量优先使用
let/const声明。
内容的提问来源于stack exchange,提问作者anonym
相关产品推荐
相关产品推荐

