Zoom in d3 version 4: understanding SVG transforms
Zooming in d3 version 4 isn’t straightforward to pick up. Far from it.
Just like force graphs, zoom in d3 has been separated into its own module. Understanding the API documents can be a little…confusing.
Things start to make more sense once you realise that zoom is intricately linked to SVG transforms, and that zooming and panning can just be thought of as changing the transforms on SVG elements.
So step one to understand zooming is to understand how the SVG transform works. Hence this article.
For basic applications of d3-zoom we don’t have to know everything about SVG transforms, but it’s worth going through some simple examples. I’ll do that in this post.
Note: this is not meant to be a complete overview of SVG transformation. There are far more knowledgable people than me who have written fantastic things on the subject. Rather, it’s to get us to the point where we can start implementing d3’s zoom capabilities and know what’s actually going on.
Let’s begin.
What is a SVG transform? Link to heading
SVG elements can be rotated, skewed, translated and scaled. All of these things can be done through modifying the transform attribute of the SVG element. Hence the name.
From a d3-zoom context, the two most important transformations to understand are translating and scaling.
Translating Link to heading
The translate()
function is used to change the location of the SVG element horizontally and vertically.
Usually you’d give the translate function two parameters. The first of these is how far you’d like to move the element along its x-axis, and the second is how far you’d like to move the element along its y-axis.
Say we have a circle with radius 10 at the point (400,400). We want to move it 200 points right and 300 points down.
We would write the SVG element like this:
<circle cx="400" cy="400" r="10" transform="translate(200 300)" />
Likewise, if we wanted to move the same circle left 100 points and up 50 points, we’d transform the circle like so:
<circle cx="400" cy="400" r="10" transform="translate(-100 -50)" />
Here’s another example.
(Click on the picture above to see the code.)
In this example we create circles in a for loop and translate each circle by an amount based on the iterator. Each circle is created at the same (x,y) point, but we transform each one by translating it a different amount. The end result is a diagonal line of circles.
Here’s the bit that creates the circles:
var radius = 20;
var point = {x: 50, y: 50 } ;
for(var i = 1; i <= 10; i++){
svg.append("circle")
.attr("cx", point.x)
.attr("cy", point.y)
.attr("r", radius)
.attr("fill", "blue")
.attr("transform", "translate(" + (i*30) + "," + (i*30) + ")");
}
Scaling Link to heading
Scaling changes the size of a SVG element.
The scale()
function is what works the magic.
The scale
function takes two parameters. The first one is the scaling multiplier for the x-axis and the second one the scaling multiplier for the y-axis.
The scale
function can also take only one parameter. In this case the scale
function assumes that the scaling factor is the same in both the x and y directions.
Let’s look at our circle with radius 10 from the previous example.
<circle cx="400" cy="400" r="10"/>
We can easily turn it into a circle with radius 20 by adding a scale multiplier of 2.
<circle cx="400" cy="400" r="10" transform="scale(2)" />
Here’s another example.
Just like the previous example, we create circles in a for loop and translate them so they form a diagonal line down the page.
The difference here is that we also change the scale factor of each circle so that the circles get bigger.
for(var i = 1; i <= 10; i++){
svg.append("circle")
.attr("cx", point.x)
.attr("cy", point.y)
.attr("r", radius)
.attr("fill", "blue")
.attr("transform", "translate(" + (i*40) + "," + (i*40) + ")"+
" scale(" + (1 + i/5) + ")";
}
Now our circles have two transformations applied to them.
Does the order of these transformations matter?
Yes. Yes it does.
Order matters Link to heading
In the last example we first added a translate function that moved the circle diagonally down the page. Then we added a scale factor that made the circle bigger.
One very important point to make is that the order of transformation matters. If we first scaled the circles and then translated them, we’d get very different results.
In fact, let’s try that.
Why did we get different results?
Here’s the secret: when we transform a SVG element, we transform its whole coordinate system.
So when we translate a circle, we translate the entire coordinate system of the circle. When we scale a rectangle, we scale the entire coordinate system of the rectangle.
When we first translate the circle and then scale it, we first translate the coordinate system and then scale the circle inside that coordinate system. This gives the results we would intuitively expect.
Remember, when we scale the circle, we also scale its coordinate system. Hence, in the case where we scale the circle before we translate it, we’re actually translating the circle inside the scaled coordinate system, not the original coordinate system.
Since we’re scaling the circle upwards, we also scale its translations upwards as well. When you try to move the circle ten units to the right, and the circle has a scale factor of 2, you’ll find that the circle moves 20 units.
I’m going to link you to this resource that explains this concept brilliantly. I give it extra kudos for the cute drawings!
As for our example, notice how the circles are getting further apart from each other as we go down the page. This is a consequence of the increasing scale for each circle.
Summary Link to heading
To summarise: the zoom system in d3 is based around transforming SVG elements.
The two main transforms that the zoom system utilises are translating and scaling. There may be rotation added in a later update, but we’re not quite there yet.
In the next post I’ll go through a simple zoom example, applying the stuff we learnt here on SVG transformations.
Stay tuned!
This post is part of a series of articles on how zoom works in version 4 of d3. Hope you enjoyed it!