Wednesday, 22nd June 2011
Pan and zoom control
Since SVGs are infinitely scalable, it's 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 targetted effect, you can wrap the elements you want to change in a group with a transform attribute.
In this post, I'll explain now to make a pan and zoom controller that can be easily added to any SVG, to make something like the map below.
The transform attribute
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 the first few linear algebra videos at the Khan Academy).
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 2 lines of a 3x3 matrix. The code treats every coordinate in the group as a vector [x, y, 1] and multiplies it by a matrix like this:

The result is that:
- the new x-coordinate of each element is ax + cy + e
- the new y-coordinate of each element is bx + dy + f
For example, transform="matrix(1 0 0 1 10 0) will make each x-coordinate become 1x + 0y + 10 and each y-coordinate become 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 and 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. all the elements remain the same.
<g id="map-matrix" transform="matrix(1 0 0 1 0 0)"> All the other elements ... </g>
Now we create a script element that sets up some variables for the transform matrix, the height and width 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/ecmascript">
<![CDATA[
var transMatrix = [1,0,0,1,0,0];
function init(evt)
{
if ( window.svgDocument == null )
{
svgDoc = evt.target.ownerDocument;
}
mapMatrix = svgDoc.getElementById("map-matrix");
width = evt.target.getAttributeNS(null, "width");
height = evt.target.getAttributeNS(null, "height");
}
]]>
</script>
At onload="init(evt)" to the svg element so the function is called when the SVG loads and the variables are set.
The pan function
The pan function takes two variables which determine the distance along the x- and y-coordinates to pan. 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)
{
transMatrix[4] += dx;
transMatrix[5] += dy;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}
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 as way as to keep the centre in the centre by adding a factor to the last two values in the transformation matrix.
function zoom(scale)
{
for (var i=0; i<transMatrix.length; i++)
{
transMatrix[i] *= scale;
}
transMatrix[4] += (1-scale)*width/2;
transMatrix[5] += (1-scale)*height/2;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}
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. The important features are the onclick="pan(50,0)", etc.
<style>
.compass{
fill: #fff;
stroke: #000;
stroke-width: 1.5;
}
.button{
fill: #225EA8;
stroke: #0C2C84;
stroke-miterlimit: 6;
stroke-linecap: round;
}
.button:hover{
stroke-width: 2;
}
.plus-minus{
fill: #fff;
pointer-events: none;
}
</style>
<circle cx="50" cy="50" r="42" fill="white" opacity="0.75"/>
<path class="button" onclick="pan(0,50)"
d="M50 10 l12 20 a40,70 0 0,0 -24,0z" />
<path class="button" onclick="pan(50,0)"
d="M10 50 l20 -12 a70,40 0 0,0 0,24z" />
<path class="button" onclick="pan(0,-50)"
d="M50 90 l12 -20 a40,70 0 0,1 -24,0z" />
<path class="button" onclick="pan(-50,0)"
d="M90 50 l-20 -12 a70,40 0 0,1 0,24z" />
<circle class="compass" cx="50" cy="50" r="20"/>
<circle class="button" cx="50" cy="41" r="8"
onclick="zoom(0.8)"/>
<circle class="button" cx="50" cy="59" r="8"
onclick="zoom(1.25)"/>
<rect class="plus-minus" x="46" y="39.5"
width="8" height="3"/>
<rect class="plus-minus" x="46" y="57.5"
width="8" height="3"/>
<rect class="plus-minus" x="48.5" y="55"
width="3" height="8"/>
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.
| Attachment | Size |
|---|---|
| Australia_pan_zoom.svg | 6.38 KB |
Comments
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
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
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.
Thanks, I'm glad you found it useful. Using it with Graphviz is a great idea, I'll have to try it myself.
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.
Post new comment