codinginthevoid codinginthevoid - 14 days ago 8
iOS Question

D3 zoom failing on two simultaneous touches

I'm having a problem with D3 (version 4) on mobile devices, the zoom functionality seems to not work when touching two elements at exactly the same time. The only device I've tested on is an iPhone 7 (iOS 10.1.1), and the problem exists both in the mobile Safari and Chrome browser apps.

I've created a simple example (below) that I can reproduce this issue with, it just has two rectangles on a yellow background. On my iPhone:


  • If I touch them at the exact same time with two fingers, the zoom will not change as I move my fingers apart and together.

  • But if the touches are not exactly at the same time (even ever so slightly apart), the zooming works fine.



Am I missing something that's causing this behaviour? I tried searching for others with the same problem and couldn't find anything... but given the popularity of D3, this must be something I'm doing wrong.

This is causing an issue for my users. Many times it works as the touch events aren't exactly at the same time... but quite often the touches will be at the same time, and it looks to the user like the zoom is broken or takes multiple attempts to succeed. Very frustrating.



var svg = d3.select('#svgLocation').append('svg')
.attr('style', 'width:300px;height:300px;background-color:yellow;');

var grp = svg.append("g");
grp.append("path").attr('d','M25,25 275,25 275,125 25,125Z');
grp.append("path").attr('d','M25,175 275,175 275,275 25,275Z');

var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", function() { grp.attr("transform", d3.event.transform) });
svg.call(zoom);

#svgLocation {
width:100%;
height:100%;
}

<!DOCTYPE html>
<html>

<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="http://d3js.org/d3.v4.min.js"></script>
</head>

<body>
<div id="svgLocation"></div>
</body>

</html>




Answer

So, this does appear to be a bug in D3v4, that affects at least one device (iPhone 7 devices running iOS 10, both the Safari and Chrome browsers). It does not affect all devices though: I couldn't reproduce the problem when I tried it on a Windows 7 all-in-one touch computer.

On the iPhone, if two touches happen at the same time (or very nearly so), there will be two touchstarted events fired as expected. However, on both events the "touches" property will contain an array with both touches. (The "changedTouches" property will contain just the corresponding touch only.)

(Note that on the Windows touch device, the "touches" property for the first of the two events only had a single touch value.)

In the D3 zoom touchstarted function, the "start" of the gesture is only triggered if the count of touches matches the count of changedTouches... this is enforced by the "if" statement (Commit #17) in the following:

var g = gesture(this, arguments),
  touches = event.changedTouches,
  n = touches.length, i, t, p;

[...]

if (event.touches.length === n) {
  touchstarting = setTimeout(function() { touchstarting = null; }, touchDelay);
  interrupt(this);
  g.start();
}

I tried commenting out the "if" statement so g.start() always got called (even if there are two touches and one touchChanged)... and it immediately started working on the iPhone. The double-tap to zoom also continued working, which is the commit on which this check was added.

However, I'm hesitant to leave this in my project; obviously this if statement check was put in there for some reason. I'm worried that by disabling it, I will have opened some other edge case I'm not aware of. I'll post this issue to the Github project, hopefully a fix will be found for this.