Panning and zooming


22 Jun 2011 Code on Github

Introduction

Since SVGs are infinitely scalable, it can be useful to add controls to pan and zoom, particularly to maps. Panning and zooming can be achieved by manipulating the viewBox attribute, but that will affect the entire SVG including the controller. For a more targeted effect, you can wrap the elements you want to change in a group with a transform attribute.

In this tutorial, I'll explain now to make a pan and zoom controller that can be easily added to any SVG, to make something like this map.

All the code can be found by clicking the Github link at the top of the page.

The transform element

Panning and zooming can be easily achieved using the transform attributes translate and scale respectively.

To pan left by 10 units, you translate everything 10 units right by wrapping it in a group like this.

<g transform="translate(10 0)">
  All the other elements...
</g>

To zoom in by 25%, you scale everything by 1.25 unit by wrapping it in a group like this:

<g transform="scale(1.25)">
  All the other elements...
</g>

However, this will zoom, centred on the top, left corner. Normally we want to zoom centred on the centre of the screen, which means we need to combine scaling with a translation. And when you start combining scaling and translation transformation, things start getting a bit trickier (because you also need to scale previous translations).

Matrix transformations

Using matrices makes transformations a lot simpler in the long run, because they allow us to easily update our transformation. However, if you're not familiar with matrices, then they can be a bit tricky (and I'd recommend watching 3Blue1Brown's Essence of Linear Algebra playlist. Or you can just skip to the code and trust I know what I'm doing.

The SVG transform matrix is applied like this.

<g transform="matrix(a b c d e f)">
  All the other elements...
</g>

Where a, b, c, d, e and f form the top two lines of a 3 × 3 matrix. The code treats every coordinate in the group as a vector $[x, y, 1]$ and multiplies it by a matrix like this:

$\begin{bmatrix}a & c & e\\b & d & f \\0 & 0 & 1 \end{bmatrix} \begin{bmatrix}x \\y \\ 1 \end{bmatrix}$

The result is that:

  • the new x-coordinate of each element is $a(x) + c(y) + e$
  • the new y-coordinate of each element is $b(x) + d(y) + f$

For example, the result of transform="matrix(1 0 0 1 10 0)" will be:

  • each x-coordinate becomes $1x + 0y + 10$
  • each y-coordinate becomes $0x + 1y + 0$

In other words, we translate the matrix 10 units to the right.

With transform="matrix(1.25 0 0 1.25 0 0)":

  • each x-coordinate becomes $1.25x + 0y + 0$
  • each y-coordinate becomes $0x + 1.25y + 0$

In other words, we scale the matrix by 25%.

Initialising the SVG

Now we can create some functions to pan (translate) and zoom (scale) our map. First wrap all the graphical in a group with and id so we can select it and a transform matrix set as (1 0 0 1 0 0), which is the identity matrix, i.e. it doesn't move any of the elements.

<g id="matrix-group" transform="matrix(1 0 0 1 0 0)">
  All the other elements ...
</g>

For my example, I create a script element inside the SVG so I have a single image file. You can also put the javascript in a separate file.

The code sets up some variables for the group containing the transform matrix, the coordinates for the center of the SVG, and an array of values for the transformation matrix. We could get the transformation matrix values by parsing the map-matrix element, but it's quicker and easier to just set up an array.

<script type="text/javascript"><![CDATA[
    var transformMatrix = [1, 0, 0, 1, 0, 0];
    var svg = document.getElementById('map-svg');
    var viewbox = svg.getAttributeNS(null, "viewBox").split(" ");
    var centerX = parseFloat(viewbox[2]) / 2;
    var centerY = parseFloat(viewbox[3]) / 2;
    var matrixGroup = svg.getElementById("matrix-group");
]]></script>

I've assumed that the SVG has a viewBox element, which I'm parsing to get the center coordinate. If instead your SVG has width and height coordinates then you can get those directly.

The pan function

The pan function takes two variables which determine the distance to pan along the x- and y-coordinates. For example, pan(10,0) pans 10 units right and pan(-20,10) pans 20 units left and 10 units down. The function works by adding the translation factor to the last two values in the transformation matrix.

function pan(dx, dy) {     	
  transformMatrix[4] += dx;
  transformMatrix[5] += dy;
            
  var newMatrix = "matrix(" +  transformMatrix.join(' ') + ")";
  matrixGroup.setAttributeNS(null, "transform", newMatrix);
}

It's a little be hacky to create a new transform by converting an array of values into a string, but it's fine since there should be any other transforms on group wrapping the map. To see the "proper" way to create SVG transforms, see my SVG dragging tutorial.

The zoom function

The zoom function takes a single parameter, which determines the zoom factor. For example zoom(1.25), makes everything 25% larger, so zooms in. zoom(0.8), makes everything 80% of the original size, so zooms out. The function works by multiplying every value in the matrix by the scaling factor, which not only scales all the elements, but scales any previous translations. It then translates the matrix in such a way as to keep the centre in the centre by adding a correction value to the last two values in the transformation matrix.

function zoom(scale) {
  for (var i = 0; i < 4; i++) {
    transformMatrix[i] *= scale;
  }

  transformMatrix[4] += (1 - scale) * centerX;
  transformMatrix[5] += (1 - scale) * centerY;
		        
  var newMatrix = "matrix(" +  transformMatrix.join(' ') + ")";
  matrixGroup.setAttributeNS(null, "transform", newMatrix);
}

Notice that lines 30-31 are the same as lines 17-18, so it's probably worth moving them into a separate function.

The controller

Now we've defined the functions we can create a controller that calls them. Below is a very simple example, but being SVG, you could make something a lot more intricate. Or you can use HTML element outside of the SVG. The important features are the onclick="pan(25,0)", etc. event handlers.

You can choose how large to make the transformation by changing the arguments. I like to scale by 1.25 and 0.8 as they are nice fractions and perfectly cancel each other out.

<path class="button" onclick="pan(0, 25)" d="M25 5 l6 10 a20 35 0 0 0 -12 0z" />
<path class="button" onclick="pan(25, 0)" d="M5 25 l10 -6 a35 20 0 0 0 0 12z" />
<path class="button" onclick="pan(0,-25)" d="M25 45 l6 -10 a20, 35 0 0,1 -12,0z" />
<path class="button" onclick="pan(-25, 0)" d="M45 25 l-10 -6 a35 20 0 0 1 0 12z" />
  
<circle class="button" cx="25" cy="20.5" r="4" onclick="zoom(0.8)"/>
<circle class="button" cx="25" cy="29.5" r="4" onclick="zoom(1.25)"/>

This can then slot into the end of an existing SVG. Make sure it is at the end of the SVG so it floats above the other elements and make sure it's not within the group with the transform attribute, so it remains in place.

Comments (25)

Morten on 7 Sep 2011, 11:03 a.m.

It looks nice - I would like to be able to show maps produced in Qgis as svg with the zoom and pan included.

Your examples does however not seem to work in IE8 (with Adobe SVG-wiever) - or is it just on my PC (running Win 7)?

Opening Australia_pan_zoom.svg gives me errors like: SvgDoc is undefined, width is undefined etc.

It works nice in Google Chrome and Mozilla.

Best regards Morten

Peter on 10 Sep 2011, 10:43 p.m.

Hi Morten.

It sounds like the onload="init(evt)" isn't being called. I don't have any experience with Adobe SVG-viewer so I'm not sure if there's a way around this, but I assume there must be. It maybe be possible to call init() using Javascript in the main HTML once the SVG has been loaded, but again I don't have much experience with doing that sort of thing.

Peter

kirill on 6 Dec 2011, 12:18 p.m.

Hi!

Just wanted to say thank you for this tutorial! I'm generating huge svg files via Graphviz and it was real pain to view them with only browser zoom.

Peter on 7 Dec 2011, 3:29 p.m.

Thanks, I'm glad you found it useful. Using it with Graphviz is a great idea, I'll have to try it myself.

Alex on 14 Dec 2011, 11:59 a.m.

Hi Peter!

There's always something useful to find on your site. This example is the shortest code for zoom and pan as far as I have seen.

Thank you, again.

Shreeram kushwaha on 11 Nov 2012, 10:45 p.m.

Is there any way to apply this to an SVG whose code is written inside an html doc???

Peter on 13 Nov 2012, 11:53 a.m.

Hi Shreeram,

There is a way to do this with an SVG written directly into the HTML, but it's tricky because you need to work out where the SVG is in relation to the page so you can convert the mouse coordinates (which are relative to the HTML page) into coordinates relative the the SVG.

I've not done it myself, but I think this SO answer explains how it can be done: http://stackoverflow.com/questions/10298658/mouse-position-inside-autoscaled-svg

Darkcrow on 6 Dec 2012, 3:57 p.m.

Hi Peter,
Thank you so much for this useful tutorial.
Cheers

Shreeram kushwaha on 8 Dec 2012, 10:59 p.m.

Dear Peter,

Still I am not able to solve my problem.

pl help me out.

Paulo Bueno on 11 Dec 2012, 6:08 p.m.

Hi Peter,

Thank you for this enourmous help. Very clear (and cleaver) post.

I've made an reference to this page here.

Best regards,

webreac on 16 Jan 2013, 11:42 p.m.

Hello, thank you for your example, but I would like to remove the controller. I would like to draw a map with the zoom controlled by the scroll wheel and the pan controlled by a mouse drag.

Martin on 7 Mar 2013, 9:58 a.m.

Hi,

is there a way to optimize these control for gesture on iPad?

Greets
Martin

Peter on 7 Mar 2013, 1:40 p.m.

Sorry, I don't know anything about how the iPad works with SVG.

Akash Deep Kanojia on 18 Jun 2013, 9:36 p.m.

Hello Peter,

Just wanna say a big THANK YOU. Your tutorial is a lifesaver.

I just wanna give you a little more trouble. Can you please tell me if we want to zoom in at a particular point not at the center of the SVG.
Actually the scenario is like I have to zoom the SVG at the element or group which is clicked.

Thanks in advance...

Morten on 24 Feb 2014, 1:37 p.m.

Hi Peter

Very nice page! - I've been playing around with it and produced this: Link

It is based on a svg file exported from QGIS (www.QGIS.org) and I have simply replaced all the svg drawing code from your Australia_pan_zoom.svg example with a link to the exported svg file like this:

<svg version="1.2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">

<image height="100%" width="100%" x="0" y="0" xlink:href="QGIS_printcomposer.svg"/>

</svg>

Morten on 24 Feb 2014, 11:23 p.m.

Hello Peter

Cool stuff!

I played around with the code in your Australia_pan_zoom.svg example and found out that you can replace all the "drawing svg code" in the file with a link to anothet svg file. A file which in my example is an export of a map from QGIS.

See more here: QGIS posting

VishnuVardhan V on 24 Jun 2014, 8:15 a.m.

Hi Peter

Thanks for the great tutorial.

I need a help, is there a way to block the content going behind the pan and zoom control ?

Thanks

Simon Drew on 7 Aug 2014, 10:38 a.m.

Thank you for sharing this info, Peter. I am also looking at Graphviz for data visualisation in SVG, and I imagine this could be a useful way to get around the problem of browsers with differing zoom capabilities. I have a couple of questions, however:

1. Graphviz creates its own transform attribute for the graph element when it generates the SVG. Presumably this would need to be overridden somehow, but I have not yet succeeded in getting the amended image to appear when I try to do so. What is it that I am failing to understand -does the transform always have to apply to the root element?

2. Graphviz parameters can also be used to affect the SVG height, width and viewbox dimensions (which is necessary if you only have browser zoom to work with). Are there recommended values for using your technique, or does the matrix operate entirely independently of these?

Anonymous on 9 Feb 2015, 2:32 a.m.

Isn't it also possible to use CSS zoom and position to acheive simple panning and zooming?

Anonymous on 24 Mar 2015, 4:38 p.m.

Hello Peter.

Very cool stuff.

I have an svg file that I would like to integrate with your pan/zoom code. What is the best way to get the source of your code and do it?

I do not see anywhere I can download the source of your svg file.

Thanks in advance,

Tony on 26 Dec 2015, 4:45 p.m.

Thanks for this very helpful tutorial. I've just added pan and zoom to our atlas-style mapping, as well as an ability to enlarge the whole map, Seems to work well.

odahcam on 27 Nov 2018, 7 p.m.

Hey, nice post, I just think there's an error in the matrix shown, where the item on coordinates (2, 3), the second "e", should be a "f".

Peter on 29 Nov 2018, 9:08 p.m.

You're right, I've fixed it, thanks!

Marek on 2 Apr 2019, 12:10 p.m.

It's a fantastic article, thank you very much Peter.

I'm wondering how to add zoom on 'onwheel' event, then it would be a perfect solution. Have you considered adding this to your tutorial?

Niall on 27 Aug 2020, 9:42 a.m.

Great tutorial and incredibly helpful. Thank you Peter!

I did notice one thing, in the section on the zoom function, the code is:

function zoom(scale) {
for (var i = 0; i < 4; i++) {
transformMatrix[i] *= scale;
}
...
}

In github (https://github.com/petercollingridge/code-for-blog/blob/master/svg-interaction/pan_and_zoom/pan_and_zoom.svg) the equivalent code (which works better for me) is::

function zoom(scale) {
for (var i = 0; i < 6; i++) {
transformMatrix[i] *= scale;
}
...
}

Thanks again!