d3 / d3-zoom

Pan and zoom SVG, HTML or Canvas using mouse or touch input.
https://d3js.org/d3-zoom
ISC License
507 stars 143 forks source link

Brush & zoom #222

Closed Fil closed 2 years ago

Fil commented 4 years ago

From @amannn (copied from https://github.com/d3/d3-zoom/issues/198#issuecomment-685592624 ):

I'm having issues with the `mouseup` event not being handled correctly. ![chart](https://user-images.githubusercontent.com/4038316/91970354-d0b86e80-ed17-11ea-9e60-ab25ec3c888d.gif) There's a zoom applied to the chart and when I'm dragging in the context area below, the `mouseup` doesn't seem to end the pan gesture. The `mousemove` events that happen after `mouseup` cause the chart to be panned again. Once I pan the regular chart above, the panning stops correctly. This behaviour happened after an upgrade to `d3-zoom@2.0.0`. Here's a reproduction: https://codepen.io/amann/pen/QWNqdqw?editors=1010. Sorry that the example is a bit convoluted. I'm new to d3 and wasn't sure which parts I could safely remove.
Fil commented 4 years ago

So, I just tried porting to D3 6 the “Brush and Zoom” example that this codepen is based on (gist).

It appears that there is a bug in the sense that, to avoid looping between the brush and zoom events, we test the event.sourceEvent. With the global d3.event this did not matter since it was "always there". However, now that the event is local, when we call brush.move and zoom.transform directly we do not pass it, and the test always fails.

I see two possibilities:

1) Use a different method to avoid looping.

For exemple here I create a global zoomevent to track the double calls (note that it could alternatively be a property of a DOM element if we are sure that zoom and brush are applied to the same element).

+ let zoombrush;

- function brushed() {
-   const event = d3.event;
-   if (event.sourceEvent && event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
+ function brushed(event) {
+  if (zoombrush) return; // ignore brush-by-zoom
+  zoombrush = 1;
  var s = event.selection || x2.range();
  x.domain(s.map(x2.invert, x2));
  focus.select(".area").attr("d", area);
  focus.select(".axis--x").call(xAxis);
  svg.select(".zoom").call(zoom.transform, d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0));
+  zoombrush = 0;
}

- function zoomed() {
-   const event = d3.event;
-   if (event.sourceEvent && event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
+ function zoomed(event) {
+  if (zoombrush) return; // ignore zoom-by-brush
+  zoombrush = 1;
  var t = event.transform;
  x.domain(t.rescaleX(x2).domain());
  focus.select(".area").attr("d", area);
  focus.select(".axis--x").call(xAxis);
  context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
+  zoombrush = 0;
}

This method can be applied today "in user land". See the new example in https://blockbuilder.org/Fil/076415313a08e76ae60d468e2cb25909


2) pass the event to brush.move and to zoom.transform.

(This seems more complex and needs to patch brush.move)

- svg.select(".zoom").call(zoom.transform, transform);
+ svg.select(".zoom").call(zoom.transform, transform, null, event);
- context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
+ context.select(".brush").call(brush.move, x.range().map(t.invertX, t), event);

This leaves the user code closer to the original, but it can not work as of today, because brush.move does not currently accept an event. Maybe we should allow it and patch it like so:

diff --git a/src/brush.js b/src/brush.js
index d920536..9a10103 100644
--- a/src/brush.js
+++ b/src/brush.js
@@ -211,7 +211,7 @@ function brush(dim) {
         .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
   }

-  brush.move = function(group, selection) {
+  brush.move = function(group, selection, event) {
     if (group.tween) {
       group
           .on("start.brush", function(event) { emitter(this, arguments).beforestart().start(event); })
@@ -227,7 +227,7 @@ function brush(dim) {
             function tween(t) {
               state.selection = t === 1 && selection1 === null ? null : i(t);
               redraw.call(that);
-              emit.brush();
+              emit.brush(event);
             }

             return selection0 !== null && selection1 !== null ? tween : tween(1);
@@ -244,7 +244,7 @@ function brush(dim) {
             interrupt(that);
             state.selection = selection1 === null ? null : selection1;
             redraw.call(that);
-            emit.start().brush().end();
+            emit.start(event).brush(event).end(event);
           });
     }
   };
amannn commented 4 years ago

@Fil Thanks for your help here!

Your example seems to work flawlessly. Also I strongly guess my code was based on that example initially (I didn't write it).

However I tried applying your user land fix in the app and I'm still seeing the issue. My CodePen reproduction doesn't even use the brushed handler, so I'm not sure how this fix can be applied there?

On the other hand my real code has the brushed handler, but even there I couldn't fix the issue with the zoombrush variable fix. I think for the reproduction I removed that handler since I noticed the bug also occurs without it.

I'm still investigating what is different in your example so that it works …

Fil commented 4 years ago

Ah yes—when I was exploring your codepen I changed

svg.call(chartZoom);

to

focus.call(chartZoom);

so that the zoom behavior would not fire on the context part. That's when I saw that there was no brushed handler, and decided to upgrade the original example instead, in order to start from a working reference.

amannn commented 4 years ago

Oh right, that avoids the issue. Thank you very much for your help!

Fil commented 4 years ago

Yes. We need to research the issue in itself, which might be a regression—but I'm not sure yet if it's linked to #198.

buharov-alexander commented 2 years ago

Seems zoom and brush still steal each other's events (version 7.4.4). I have found different solutions how to use zoom and brush together. But all of them looks like workarounds or hacks.

Here a small example: https://codepen.io/buharov-alexander/pen/mdXwvdY Parent element has zoom, child element has brush as result: "zoom.end" event will be missed and zoom pan will be sticked. zoom_sticking

Fil commented 2 years ago

This particular issue has been resolved. @buharov-alexander please can you open a new discussion instead, with a precise use-case? Note that you should be able to avoid the brush or zoom behavior "stealing" events by using brush.filter or zoom.filter.