Search code examples
javascriptd3.jspointer-events

Pointermove event not returning element index on touch


I'm trying to let users change some dot colours on a d3.js SVG chart.

It seems current element index is not being updated continuously in the pointermove event function on touch while for example cursor coordinates are.

I've used touch-action on the chart as advised elsewhere. I've tried many different options but none seem to work and I've run out of ideas.

The only thing I'm uncertain of is how I'm updating the function — maybe there's some proper way to do it?

I'm testing on iOS Safari.

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");

// LEDs
function updateLEDs() {
  touchGroup.selectAll("path")
    .data(pie(LEDs))
    .enter()
    .append("path")
    .attr("d", arc)
    .each(function(d, i) {
      index.set(this, i);
    })
    .on("touchstart", function(event) {
      event.preventDefault();
    })
    .on("pointermove", touched);

  const circleNodes = 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);

  function touched(event, d) {
    const i = d3.select(this).datum().index;
    let j = index.get(this);
    const a = [event.clientX, event.clientY];
    const p = event.pointerType;
    if (event.buttons === 1) {
      d3.select("#info").text("LED " + i + "/" + j + ", " + p + " @" + a);
      LEDs[index.get(this)] = `#${Math.floor(Math.random()*16777215).toString(16)}`;
      circleNodes.data(LEDs);
      updateLEDs();
    }
  }
}

// Init
clock.append(svg.node());
updateLEDs();
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;
  }
}
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<div id="info">LED</div>
<div id="clock"></div>


Solution

  • As showed here and explained here, it looks like the reason is a performance compromise on mobile and the solution that works is using the following to update the element index:

    document.elementFromPoint(event.clientX, event.clientY)

    and in context of D3 an my example it would be:

    d3.select(document.elementFromPoint(event.clientX, event.clientY)).datum().index;

    let col = "#000000";
    const num = 60;
    let LEDs = Array(num).fill(col);
    
    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");
    
    // LEDs
    function updateLEDs() {
      touchGroup.selectAll("path")
        .data(pie(LEDs))
        .enter()
        .append("path")
        .attr("d", arc)
        .on("touchstart", function(event) {
          event.preventDefault();
        })
        .on("pointermove", touched);
    
      const circleNodes = 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);
    
      function touched(event, d) {
        const i = d3.select(document.elementFromPoint(event.clientX, event.clientY)).datum().index;
        const a = [event.clientX, event.clientY];
        const p = event.pointerType;
        if (event.buttons === 1) {
          d3.select("#info").text("LED " + i + ", " + p + " @" + a);
          LEDs[i] = `#${Math.floor(Math.random()*16777215).toString(16)}`;
          updateLEDs();
        }
      }
    }
    
    // Init
    clock.append(svg.node());
    updateLEDs();
    body {
      background-color: #ccc;
    }
    
    #info {
      border-bottom: 1px solid black;
      font-size: 0.6em;
      padding: 0.4em;
      user-select: none;
      -webkit-user-select: none;
    }
    
    #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;
      }
    }
    <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
    <div id="info">LED</div>
    <div id="clock"></div>