By Andy Fitch •

Tutorial - Creating an Interactive SVG map

Over the past few years there’s been an increasing demand from our clients for interactive maps, whether it be for shopping centres, housing developments or just geographical maps.

Most recently, I was tasked with creating a masterplan (architectural, not the evil villain kind) for Little Kelham, a property development at Kelham Island in Sheffield built by Citu. They needed two versions of the masterplan: a map to highlight the residential properties they have available, and a map for their commercial properties. Each plot needed to have its own well defined shape and have a hover state & click events.

What am I making?

In this tutorial we’re going to be drawing vector graphics in the browser using a combination of SVG (Scalable Vector Graphics) and an excellent JavaScript framework called Raphaël to create a map of the United Kingdom.

SVG is supported in most browsers except IE8 and below. Raphaël makes drawing vector art easy and, more crucially, cross-browser compatible. It currently supports Firefox 3.0+, Safari 3.0+, Chrome 5.0+, Opera 9.5+ and Internet Explorer 6.0+.

The United Kingdom is made up of England, Scotland, Wales, Northern Ireland and a fairly exhaustive list of thousands of smaller islands. For the purpose of this tutorial we’re just going to focus on England, and in particular its regions. We’ve split England up into nine regions: the North West, North East, Yorkshire & Humber, West Midlands, Midlands, East Anglia, Greater London, the South East and the West Country.

How will it look?

View Demo Download the source

How do I make it?

Tip: save your progress often!

Prepare the SVG image

You’ll need an SVG image to start with. I used this United Kingdom – Region 3.svg file from Wikipedia.

Later, when we use Raphaël, it’ll ask us for dimensions for our map. I’d recommend you create a new artboard in Adobe Illustrator at your preferred dimensions, then scale your vector objects to fit. Use File -> Save As… to save your map as .svg, after which you’ll be presented with the SVG Options dialog. Click on the ‘SVG Code…’ button, which will allow you to save your image out to SVG in XML format. In my example, I saved this file as ‘map_england.svg’.

Convert the SVG into a Raphaël-friendly format

Your .svg file will be full of (and possibly , etc. if you have some perfectly shaped objects) tags, each with a ‘d’ attribute containing path data. Raphaël doesn’t consume SVG data, it just uses the path data, so we’ll use a SVG to Raphaël converter (I use http://readysetraphael.com/) to output our Raphaël JavaScript code.


Update (22/09/14)

@dalehay tweeted me asking if I knew any alternatives to readysetraphael.com as he had trouble with larger pieces of SVG code. He found http://toki-woki.net/p/SVG2RaphaelJS/, which may be of use to you. Thanks @dalehay!


Paste the JavaScript code into an empty file and save it to an appropriate place, such as your website’s js directory. I’ve called mine ‘map.js’.

Make sense of your JavaScript

var rsr = Raphael('rsr', '903', '1108');

var path2650 = rsr.path("M 175.51861,656.85102 L 190.60845,674.5882...");
path2650.attr({id: 'path2650',fill: ' rgb(230, 230, 230)'," fill-rule": ' evenodd', stroke: ' rgb(255, 255, 255)'," stroke-width": ' 2.67616'," stroke-linecap": ' butt'," stroke-linejoin": ' miter'," stroke-miterlimit": ' 4'," stroke-dasharray": ' none'," stroke-opacity": ' 1'}).data('id', 'path2650');
var rect3590 = rsr.path("M 578.39246,-178.26028 L 674.58451,-178.26028 L 674.58451,-95.83876 L 578.39246,-95.83876 L 578.39246,-178.26028 z");
rect3590.attr({id: 'rect3590',fill: ' none'," fill-opacity": ' 1'," fill-rule": ' nonzero', stroke: ' rgb(0, 0, 0)'," stroke-width": ' 1.3'," stroke-miterlimit": ' 4'," stroke-opacity": ' 1'}).data('id', 'rect3590');
var rect3592 = rsr.path("M 485.72406,-178.17694 L 573.36571,-178.17694 L 573.36571,-50.775925 L 485.72406,-50.775925 L 485.72406,-178.17694 z");
rect3592.attr({id: 'rect3592',fill: ' none'," fill-opacity": ' 1'," fill-rule": ' nonzero', stroke: ' rgb(0, 0, 0)'," stroke-width": ' 1.3'," stroke-miterlimit": ' 4'," stroke-opacity": ' 1'}).data('id', 'rect3592');
var rect3509 = rsr.path("M 426.57037,1192.6533 L 514.21202,1192.6533 L 514.21202,1282.4281 L 426.57037,1282.4281 L 426.57037,1192.6533 z");
rect3509.attr({id: 'rect3509',fill: ' none'," fill-opacity": ' 1'," fill-rule": ' nonzero', stroke: ' rgb(0, 0, 0)'," stroke-width": ' 1.3'," stroke-miterlimit": ' 4'," stroke-opacity": ' 1'}).data('id', 'rect3509');
var path6139 = rsr.path("M 338.10865,800.73527 L 345.19969,795.28063 L 357.47266,784.64406");
path6139.attr({id: 'path6139',fill: ' none'," fill-rule": ' evenodd', stroke: ' rgb(255, 255, 255)'," stroke-width": ' 3.6'," stroke-linecap": ' butt'," stroke-linejoin": ' miter'," stroke-miterlimit": ' 4'," stroke-dasharray": ' none'," stroke-opacity": ' 1'}).data('id', 'path6139');
/* ... */

At this point your JavaScript will look like spaghetti so, although it’s not entirely necessary, I’d advise you untangle & tidy it up at this stage to clarify things going forward (for other developers as well as yourself!)

  1. Rename the first argument that Raphaël takes to correspond with the ID of your HTML element where the map will sit:

    var rsr = Raphael('map', '700', '1100');
    
    <div id="map" class="united-kingdom">
        <!-- Raphaël JS Map Here -->
    </div>
    

  2. Change the 2nd & 3rd arguments to the width & height dimensions you originally chose when you created your artboard in Illustrator.

    var rsr = Raphael('map', '700', '1100'); 
    

  3. Rename your variables to be more descriptive, or at least add comments above each line to indicate which path corresponds to which part of your map:

    // Yorkshire & Humber
    var yorkshire_humber = ...
    yorkshire_humber.attr({ [...] });
    yorkshire_humber.transform( [...] );
    

  4. Create an suitably named array, such as ‘regions’ in this example, then push each path on to this array as you go along:

    var regions = [];
    // ...
    // Midlands
    var midlands = rsr.path( /* ... */ );
    midlands.attr({ /* ... */ });
    regions.push(midlands);
    

  5. Often designers will use Groups in Illustrator to group layers to move them around & manipulate them more easily. These groups translate across to your code, but they’re not necessary. I’ve pulled all the bits of code you can remove into a screenshot below:

    var g2629 = rsr.set();
    g2629.attr({ /* ... */ });
    
    var g2639 = rsr.set();
    g2639.attr({ /* ... */ });
    
    var rsrGroups = [g2629, g2639];
    

    This means you can also remove any references to these groups from your path variables, such as:

    parent: 'g2629'
    

  6. Remove any unnecessary attributes from your paths, such as ‘id’, ‘nodetypes’ & ‘parent’.

    {id: 'path36', nodetypes: /* ... */ }
    

  7. Although data arguments aren’t entirely necessary, they can be useful if you’d like to manipulate the path objects later on in your JavaScript. I’ve changed Yorkshire’s path’s ID to something more descriptive, such as ‘heaven-on-earth’. Later on, when iterating over our ‘regions’ array, we can use this data ID as an operand to compare against the current object in the array. This will make more sense later on.

    /* ... */).data('id', 'path36');
    /* ... */).data('id', 'heaven-on-earth');
    

    You’ll now be left with a really nice section of JavaScript code, which is really understandable and less bloated.

    var rsr = Raphael('map', '700', '1100');
    
    var regions = [];
    
    // Midlands
    var midlands = rsr.path( /* ... */ );
    midlands.attr({ /* ... */ });
    regions.push(midlands);
    
    // Greater London
    var greater_london = rsr.path( /* ... */ );
    greater_london.attr({ /* ... */ });
    regions.push(greater_london);
    
    /* ... */
    

Managing lots of paths

If, like on the Little Kelham properties map, you have a lot of objects, then it’s really useful to be familiar with regular expressions as you can much more easily find & replace sections of code. Just be careful when using mass find & replace as debugging missing parts of code can be a bit of a nightmare when handling hundreds of objects!

Manipulating the paths

As we kept the data ID on each of our paths earlier, we can now style our objects. I’ve used a bit of JavaScript to iterate over the regions array, then modify the fill colour if we’ve found the Yorkshire path:

// Iterate through the regions & change Yorkshire’s fill colour to gold
for (var i = 0; i < regions.length; i++) {
    if (regions[i].data('id') == 'heaven-on-earth') {
        regions[i].node.setAttribute('fill', 'gold');
    }
}

The markup

The only HTML you’ll need (other than the rest of your website’s markup) is the element where you’d like your interactive map to appear, and the references to the Raphaël script and your JavaScript file containing your custom Raphaël code.

In the example below I’ve added a class to the map’s container so that I could add a background image to show the other parts of the United Kingdom (the translucent bits) to provide some context.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Raphaël JS Map</title>
        <style type="text/css">
            .united-kingdom {
                background-image: url(img/united_kingdom.png);
                background-repeat: no-repeat;
            }
        </style>
    </head>
    <body>
        <div id="map" class="united-kingdom">
            <!-- Raphaël JS Map Here -->
        </div>
        <script src="raphael-min.js"></script>
        <script src="map.js"></script>
    </body>
</html>

If you load your page you (should!) now see something along the lines of this:

Because everyone loves a show-off let’s add a bit more JavaScript make the name of the region appear below the map whenever we hover over the regions. Note: I added another data attribute to each path, called ‘region’, to attach a human-readable name. To use more than one attribute, you must pass it as an object using curly braces:

/* ... */}).data({'id': 'west-country', 'region': 'West Country'});

We add a bit more JavaScript to attach mouseover and mouseout events to each path, and our map is complete!

// Iterate through the regions & change Yorkshire’s fill colour to gold
for (var i = 0; i < regions.length; i++) {

    // Change Yorkshire’s fill colour to gold
    if (regions[i].data('id') == 'heaven-on-earth') {
        regions[i].node.setAttribute('fill', 'gold');
    }

    // Showing off
    regions[i].mouseover(function(e){
        this.node.style.opacity = 0.7;
        document.getElementById('region-name').innerHTML = this.data('region');
    });

    regions[i].mouseout(function(e){
        this.node.style.opacity = 1;
    });
}

View Demo Download the source


Update

A reader emailed me asking for help adding a popup next to the clicked region, so here’s the code I wrote to help him out (note it now uses jQuery):

Download