You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 21:07:02