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