Search code examples
javascriptsvgd3.jszooming

How to programmatically zoom d3 and get translate values right


I use d3 zoom (d3 v7.8) to zoom and pan around in an SVG. This works all nicely, but now I want to add a zoom-in and a zoom-out button.

Now I could manually change the transform attribute of the SVG group I translate and zoom in on.

  1. First problem with this is, that the internal zoom object does not know its new zoom and translation values. So when going back to a pan, the layout jumps to a previous state.
  2. When zooming I realized, as soon as one is not zoom into the center, I also have to change the translate values. (When using the mouse-wheel one can see that not only the scale value but also the translate values change). How could I find the correct values for this?

To overcome the first problem I thought I could use the d3 method scaleTo but to call that I need to pass in a position. I tried to read out the current position with a regex looking at the translate values of my panGroup. But still it would jump around wildly.

    import { select } from 'd3-selection'
    import { zoom } from 'd3-zoom'
    
      //...

    this.svgZoom = zoom()
      .on('zoom', this.handleZoomOrPan)
      .scaleExtent([1, this.maxZoom])

    function zoomIn() {
      const { k, x, y } = this.getTransformParameters(this.panGroup)
      const newScale = k + 1
      this.svgZoom.scaleTo(
        select(this.panGroup).transition().duration(750),
        newScale,
        position
      )
    }

I tried what happens if I don't pass a position, as this param is optional. Using the button would still let my panGroup jump around if it was panned with the mouse before. Even when using the zoom buttons first and the scaleTo works (because it would just zoom into the center) I get weird behaviour whenever I start to pan. Somehow the scaleTo does not save the new scale values...

I created a video to show that behaviour: https://streamable.com/vnse3f

You can see that I zoom first and then start to pan. Immediately when panning the zoom level jumps back to 100%.

Edit

As mentioned in my first comment I needed to use the selection on which I used .call(this.svgZoom) on.

This is now okayish but not perfect.

When calling

this.svg
  .transition()
  .duration(750)
  .call(
    this.svgZoom.transform,
    zoomIdentity.translate(x, y).scale(newScale)
  )

where x and y are the values I got from reading out the current translate values, I do get a zoom but it would always translate to the left upper corner.

How could I get the correct x and y values to zoom in on the current part of the screen I can see now?

I created another video to showcase this:

https://streamable.com/zd857g

As you can see the zoom is always wandering to the upper left corner.

And here's the code where I get the current translate values.

getTransformParameters(element) {
      const transform = element.getAttribute('transform')
      if (transform?.length) {
        // https://regex101.com/r/mnl3az/3
        const regexTranslate = /translate\((\-?\d+\.?\d*)\,\s?(\-?\d+\.?\d*)/g // eslint-disable-line
        const matchesTranslate = [...transform.matchAll(regexTranslate)]

        // https://regex101.com/r/HZolRp/1
        const regexScale = /scale\((\-?\d+\.?\d*)\)/g // eslint-disable-line
        const matchesScale = [...transform.matchAll(regexScale)]

        const x = parseFloat(matchesTranslate[0][1]) || 0
        const y = parseFloat(matchesTranslate[0][2]) || 0
        const k = parseFloat(matchesScale[0][1]) || 1

        return { k, x, y }
      } else {
        return { k: 1, x: 0, y: 0 }
      }
    },

Solution

  • Actually this example solved all my questions: https://observablehq.com/@d3/programmatic-zoom