Combining drag and zoom should be an easy task, and it is. But only if you do it right.

In this article I’ll present two ways to apply drag and zoom to your d3 visualisations. One way is easy and simple, and one way is complicated and difficult, yet both examples hold key insights.

The easy way

Mike Bostock has written a simple example where he combines drag and zoom. Click on the above picture to see his code.

Mike’s code is fairly self explanatory, so I’ll paraphrase from it. Roughly speaking, it goes like this:

  1. Create a SVG element to house the visualisation. Create some points and have them in a phyllotaxis pattern.
  2. Append a “g” element to the SVG element. This “g” element will contain all the circles.
  3. Add circles to the “g” element defined in Step 2. Allow these circles to be dragged by defining a drag handler in the selection.call function. Also define a function called “dragged”, which contains the code that runs whenever the “drag” event is detected.
  4. Add a zoom handler to the SVG element and define a maximum level that we can zoom in.  Also define a function called “zoomed” that triggers a transform whenever a zoom event is detected. The transform is applied to the g element defined in Step 2. This is important.

And we’re done. Quick and painless.

When we zoom, we apply a transform on the “g” element that contains the circles. Every circle inherits this transform from its parent element.

This means that when we drag a circle, the mouse event takes into account the transformation of the SVG element.

We don’t run into problems when we drag circles around. Life is good.

Now the second way.

The hard way

This code follows much of the same logic as above.

We create a SVG element and append a “g” element to it. We create a bunch of random circles that are appended to the “g” element. We even create a stylish black background that we also append to the SVG element.

Then we create a zoom handler and apply it to the SVG element.

And things start to go wrong.

Instead of defining the zoom transform on the “g” element containing the circles, like we did in the above example, we define the zoom transform on the circles themselves.

Every circle has the same transform applied to it. The “g” element has no transform applied onto itself.

A subtle difference, but a crucial one. Because now mouse events won’t do what we think they will, since they reference the original “transform” of the encompassing SVG element.

This means that when we add drag capabilities into the mix, things go haywire.

Try zooming in or out, and then dragging the circles around. They won’t drag to the cursor! Lucky there’s a workaround.

We need to change our drag function to take into account the current transform.

First step is to find out where the circle that we’re dragging is first located. We can log this against the “start” event in the drag handler.

The drag function is a bit complicated. In the below code, we

  • check to see if we’ve zoomed in or out at all. If we haven’t, we won’t be able to read the transform attribute because it won’t exist.
    • If we haven’t zoomed in or out, it means that we’re operating at a scale factor of 1 – the default.
    • If we have zoomed in or out, we need to get the current scale factor. We can extract it from the ‘transform’ attribute of whatever we’re dragging by using some string manipulation techniques.
  • adjust the location of the circle to follow the mouse pointer. Because d3.event.x and d3.event.y follow the original non-transformed coordinate system of the SVG background, it means that the circle will drag too far when we’re zoomed in, and not far enough when we’re zoomed out. We fix this by taking the starting point and then adjusting for the current scale factor.

Last step is to apply the drag handler to the circles and we’re done.

It works – but it’s complicated and ugly.

Takeaways

The first takeaway is if you want to add zoom functionality to a whole visualisation, it looks like it’s important to apply the zoom handler on the encompassing elements wherever possible. So if you have a “g” element holding lots of circles, apply the zoom behaviour to the “g” element, not the individual circles. If you don’t do this then the mouse events won’t adjust to the current transform.

The second takeaway is that if something seems hard and convoluted that should be easy, odds are that you’ve missed something. Take a step back and figure out what’s the root cause of the problem.

Thanks for reading!


This post is part of a series of articles on how zoom works in version 4 of d3. Hope you enjoyed it!

Leave a Reply

Your email address will not be published. Required fields are marked *