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.
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%.
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:
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 }
}
},
Actually this example solved all my questions: https://observablehq.com/@d3/programmatic-zoom