D3 Brush & Zoom多次缩放异常:变换基于初始参数而非更新值
D3.js 交互式地图刷选缩放二次失效问题修复
问题现象
基于D3开发的交互式地图支持拖拽矩形框刷选缩放,首次刷选、重置后刷选均正常,但处于缩放状态时二次刷选,brush会基于原始SVG范围检测选区,无法匹配当前缩放后的视图,导致无法正确缩放至刷选区域。
问题根源
brush直接绑定在SVG根元素上,而缩放仅通过修改地图和点元素的transform属性实现,brush的坐标系统始终未同步当前缩放变换,导致二次刷选时获取的选区坐标是原始画布位置,而非当前视图对应的实际区域。
修复方案
- 统一缩放容器:将陆地路径和点元素放入同一个
<g>容器,所有缩放变换直接作用于该容器,避免重复设置transform。 - 同步brush与缩放变换:在brush事件中,通过
d3.zoomTransform获取当前缩放状态,将brush选区坐标转换为匹配当前视图的坐标。 - 修正点坐标的刷选判断:将点的投影坐标逆变换为原始坐标,再与转换后的brush选区范围对比,确保刷选高亮逻辑正确。
修改后的完整代码
let width = 1200; let height = 800; const svg = d3 .select("#chart1") .append("svg") .attr("width", width) .attr("height", height) const projection = d3.geoMercator() .scale(width / 2.5 / Math.PI) .rotate([0, 0]) .center([0, 0]) .translate([width / 2, height / 2]); const path = d3.geoPath().projection(projection); const colorScale = d3.scaleOrdinal().domain(["class1","class2","class3","class4","class5"]) .range(["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99"]); let brush = d3.brush() .on('brush', brushMove) .on("end", brushEnd); let brushExtent; let brushedSufficient = true; const zoom = d3.zoom() .scaleExtent([1, Infinity]) .translateExtent([[0, 0], [width, height]]) .extent([[0, 0], [width, height]]) .on("zoom", zoomed); // 创建统一的缩放容器 const mapGroup = svg.append("g").attr("class", "map-group"); function brushMove(e) { brushExtent = e.selection; if (!brushExtent) return; // 获取当前缩放变换,转换brush选区坐标 const transform = d3.zoomTransform(mapGroup.node()); const invertedExtent = brushExtent.map(transform.invert); svg.selectAll("circle.point") .attr("fill", d => isInBrushExtent(d, invertedExtent) ? 'white' : colorScale(d.cat)) .raise(); brushedSufficient = isSufficient(); } function isInBrushExtent(d, extent) { const [x, y] = projection([d.lon, d.lat]); return x >= extent[0][0] && x <= extent[1][0] && y >= extent[0][1] && y <= extent[1][1]; } function isSufficient() { return brushExtent[1][0] - brushExtent[0][0] > 10 && brushExtent[1][1] - brushExtent[0][1] > 10; } function brushEnd() { if (!brushExtent || !brushedSufficient) return; const transform = d3.zoomTransform(mapGroup.node()); const invertedExtent = brushExtent.map(transform.invert); const x0 = invertedExtent[0][0]; const x1 = invertedExtent[1][0]; const y0 = invertedExtent[0][1]; const y1 = invertedExtent[1][1]; const x = -(x0 + x1) / 2; const y = -(y0 + y1) / 2; const k = Math.min(8, 0.9 / Math.max((x1 - x0) / width, (y1 - y0) / height)); svg.transition().duration(750).call( zoom.transform, d3.zoomIdentity .translate(width / 2, height / 2) .scale(k) .translate(x, y), ) } function zoomed(e) { const transform = e.transform; // 仅对统一容器应用变换 mapGroup.attr('transform', transform) .selectAll('path.land') .attr("stroke-width", 1 / transform.k); mapGroup.selectAll("circle.point") .attr('r', 5 / transform.k); } function resetZoom() { svg.transition() .duration(750) .call(zoom.transform, d3.zoomIdentity) } d3.select("#resetZoom").on("click", resetZoom); // 将陆地路径放入缩放容器 mapGroup.selectAll("path.land") .data(land.features) .join("path") .attr("class", "land") .attr("fill", "white") .attr("stroke", "#808080") .attr("d", path); // brush绑定在SVG根元素,确保刷选区域覆盖整个画布 svg.append("g") .attr("class", "brush") .call(brush); // 将点放入缩放容器 mapGroup.selectAll("circle.point") .data(data) .join("circle") .attr("class", "point") .attr("id", d => "id" + d.id) .attr("cx", d => projection([d.lon, d.lat])[0]) .attr("cy", d => projection([d.lon, d.lat])[1]) .attr("stroke-width", 1) .attr("r", 5) .attr("fill", d => colorScale(d.cat)) .attr("cursor", "pointer") .on("mouseover", (e, d) => console.log(d.name))
内容的提问来源于stack exchange,提问作者jhjanicki
相关产品推荐
相关产品推荐

