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:

Matrix multiplication

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.

AttachmentSize
Australia_pan_zoom.svg6.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.

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

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

Hi Peter,

 

Thank you so much for this useful tutorial.

 

Cheers

Dear Peter,

Still I am not able to solve my problem.

pl help me out.

Hi Peter,

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

I've made an reference to this page here.

Best regards,

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.

Hi,

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

Greets
Martin

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

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...

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>

 

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

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 

Post new comment

The content of this field is kept private and will not be shown publicly.