iOS Safari中d3.js的pointermove事件触摸时无法更新元素索引
解决iOS Safari下D3.js触摸拖动时元素索引不更新问题
问题分析
你遇到的核心问题是:在iOS Safari中使用触摸操作拖动时,pointermove事件里无法正确获取当前触摸位置对应的元素索引,但光标坐标能正常更新。这主要由以下几个原因导致:
- 重复创建DOM元素:每次调用
updateLEDs都会新增一批path元素,旧元素未被移除,导致事件绑定重复,d3.local存储的索引对应关系混乱。 - 触摸事件目标锁定:iOS Safari触摸开始后,
pointermove的event.target会锁定为初始触摸元素,不会随拖动位置变化更新,导致始终获取第一个触摸元素的索引。 - 不必要的全量更新:每次修改LED颜色后调用
updateLEDs,重新渲染所有元素,破坏了触摸交互的上下文。
修复后的代码
JavaScript部分
let col = "#000000"; const num = 60; let LEDs = Array(num).fill(col); const index = d3.local(); const width = 600; const height = 600; const r = Math.min(width, height) / 2.5; const cr = (r * Math.PI) / num - 4; const pie = d3.pie().value(1); const arc = d3.arc() .innerRadius(0) .outerRadius(r * 2); const svg = d3.create("svg") .attr("width", "100%") .attr("height", "100%") .attr("viewBox", [0, 0, width, height]) .attr('preserveAspectRatio', 'xMidYMid meet'); const touchGroup = svg.append("g") .attr("transform", `translate(${width / 2}, ${height / 2})`) .attr("id", "touch-group"); const circleGroup = svg.append("g") .attr("transform", `translate(${width / 2}, ${height / 2})`) .attr("id", "circle-group"); // 触摸事件处理函数,提取到外部避免重复创建 function touched(event) { event.preventDefault(); // 获取触摸点在SVG坐标系中的位置 const [x, y] = d3.pointer(event, touchGroup.node()); // 遍历所有path,找到包含当前触摸点的元素 let activeIndex = -1; touchGroup.selectAll("path").each(function(d, i) { if (this.isPointInClientSpace(x, y)) { activeIndex = index.get(this); } }); if (activeIndex !== -1 && event.buttons === 1) { // 更新LED颜色 LEDs[activeIndex] = `#${Math.floor(Math.random()*16777215).toString(16)}`; // 仅更新对应circle的fill,无需全量渲染 circleGroup.selectAll("circle") .data(LEDs) .attr("fill", d => d); // 更新信息显示 d3.select("#info").text(`LED ${activeIndex}, ${event.pointerType} @[${event.clientX}, ${event.clientY}]`); } } // 初始化和更新函数,使用join处理元素的enter/update function initAndUpdateLEDs() { // 处理touchGroup的path元素,确保只创建一次,后续仅更新数据 touchGroup.selectAll("path") .data(pie(LEDs)) .join("path") .attr("d", arc) .each(function(d, i) { // 仅在元素创建时设置local索引 if (index.get(this) === undefined) { index.set(this, i); } }) .on("touchstart", touched) .on("pointermove", touched); // 处理circleGroup的circle元素 circleGroup.selectAll("circle") .data(LEDs) .join("circle") .attr("fill", d => d) .attr("cx", (d, i) => r * Math.cos((2 * Math.PI * i) / num - Math.PI / 2)) .attr("cy", (d, i) => r * Math.sin((2 * Math.PI * i) / num - Math.PI / 2)) .attr("r", cr); } // 初始化 document.querySelector("#clock").append(svg.node()); initAndUpdateLEDs();
CSS部分
body { background-color: #ccc; } #info { background: #222; color: #bbb; font-size: 0.6em; padding: 0.4em; } #clock { position: relative; width: 100%; max-width: 480px; margin: 0 auto; overflow: hidden; background-color: #aaa; cursor: pointer; touch-action: none; } #touch-group { fill: none; pointer-events: all; } #circle-group { pointer-events: none; }
HTML部分
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script> <div id="info">LED</div> <div id="clock"></div>
关键修复点
- 使用
join管理DOM元素:替换enter().append为join,确保元素只在初始化时创建一次,后续仅更新数据,避免索引混乱。 - 动态检测触摸元素:放弃依赖
event.target,用d3.pointer获取触摸坐标,通过isPointInClientSpace判断触摸点所在path,解决iOS触摸目标锁定问题。 - 局部更新DOM:修改LED颜色后仅更新对应circle的
fill属性,不重新全量渲染,保持触摸交互上下文稳定。 - 统一事件处理:将
touched函数提取到外部,避免重复创建函数实例,减少事件绑定混乱。
内容的提问来源于stack exchange,提问作者ALx
相关产品推荐
相关产品推荐

