Tutorial: Building a data-driven SVG map with AngularJS

“I have an existential map; it has ‘you are here’ written all over it”
Steven Wright


2017 Edit: I just realised I never added a link to the demo for this tutorial! Here it is: http://tweededbadger.com/examples/svg_maps/

On a recent AngularJS project I needed to display regional data on an interactive map. I’d already decided on using SVG as it’s great for displaying vector based assets such as maps. Keeping the integrity of the SVG file format was also important. After allot of searching I couldn't find an approach which I was happy with for the project that didn't require converting the SVG file into JavaScript or use another library such as Rapheal or d3.

The final product of this tutorial will look something like this.

All of the source code for this tutorial is available here.

This tutorial assumes you:

  • Can run a site from a local HTTP server
  • know HTML, JavaScript & CSS
  • have a basic knowledge of AngularJS

Let’s find a map!

Wikimedia Commons is a great place to find SVG maps, but don’t forget to adhere to the licence of the map that you intend to use. This map of the US states will do the job for this tutorial. Now we have our map it’s time to prepare it to work with. Open the file up in a text editor such as Notepad++:

First let’s remove the styling from the file, this is because we want to control the style of the map via external CSS and dynamically with AngularJS. To do this delete everything within the <style></style> tag at the top, then search the file to see if there’s any other styling as attributes on elements within the rest of the file.

Tip: You can easily get rid of all the in-line styles within the SVG file by using Replace with Regular Expressions. For example in Notepad++ go to Search > Replace, select “Regular Expression” and put
style=”.*”
in the “Find what:” box. Make sure there’s nothing in “Replace with”, click “Replace All” and Voila! all the styles are gone.

Next, we want to make sure that the map will scale correctly when embedded. To do this edit the following tag:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="959" height="593">

Add an attribute called “viewbox” using the width & height, then change the width & height attributes to 100%:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" viewbox="0 0 959 593">

We’ll need to be able reference the sections of the map in 2 ways: by class to reference them as a whole, and by ID in order to reference them individually. Luckily in this case the map has a class for each region called “state” and an ID for each region using 2 letter US state abbreviations.


Project Prep

We’re going to use Bootstrap for styling so go ahead and download it from http://getbootstrap.com/. Then, download AngularJS from https://angularjs.org/. Create a new folder for your project and copy Bootstrap and Angular into it. Create some blank files as highlighted so your folder structure looks something like this:

Now, set-up our base index.html:

https://gist.github.com/TweededBadger/d38466c88b446d4b691e

Loading the map

Let’s get our map loaded up. Create the AngularJS app and controller in controllers.js:

https://gist.github.com/TweededBadger/3dfb8ec7b06c927862b3

Then, create a directive to load up our map as a template and add a tag for it within the “container” div:

https://gist.github.com/TweededBadger/ccdaac3e53aee9c0637d

The great thing about using the SVG file as a template is that it makes it easy to edit your map in an application such as Inkscape on the fly. Want to delete bits of your map? Just load it up, edit, and save!

Load up index.html from your local server, you should see a big black map of the US of A.

Let’s add some styles to main.css to make it a bit prettier and add some hover interaction:

https://gist.github.com/TweededBadger/98dc9a003f5bee9733a4

You should now have white outlines which turn to black when you hover. I know it looks a bit weird but once we get some colour in there it’ll do the job.


Data, dummy!

In the real world away from tutorial land, we’d be loading our data for the map from some external service with Angular. For this tutorial though we’ll write a function to create the dummy data to be displayed on the map.

Here’s the function to go in the main controller and some html to view the values plus a button to create new data (and some styles).

https://gist.github.com/TweededBadger/17a6f13a787578a69fc5

Reload and you should see list of the states with the random data values:

Click the button and the random values should change before your very eyes!


Let’s Angularize this map..

In order to get the map to react to the data, the sections of the map need to be integrated into Angular somehow. We can do this with a directive for each region:

https://gist.github.com/TweededBadger/ae0efe18fdab666a922a

We could reference the directive in the SVG like so:

<path region ng-click=”regionClick()” d=”m 380.03242,320.96457 4.90324,-86.32496 -113.38856,-12.64396 -12.21382,87.93916 120.69914,11.02976 z” id=”CO” class=”state” />

This sucks though for 2 reasons: Firstly it adds non standard mark-up to our SVG file and secondly and even more importantly it’s bloody tedious!

Instead, let’s inject the directive into the DOM with Angular. $compile to the rescue!

Edit our directives thus:

https://gist.github.com/TweededBadger/5a9deec961399e91e714

When you click on a region you should be alerted to its id. We’re getting somewhere now! Let’s look at what’s going on here in a bit more detail. First the “svgMap” directive:

var regions = element[0].querySelectorAll(‘.state’);

Create an array containing all elements of class “state” using querySelectorAll.

angular.forEach(regions, function (region, key) {
//...
})

Loop through the regions array.

var regionElement = angular.element(region);

Wrap the region DOM element as an Angular jqLite element.

regionElement.attr(“region”, “”);

Add the “region” attribute to the DOM element so it can be tied to the directive of the same name.

$compile(regionElement)(scope);

Compile the element so Angular can do it’s magic.

On to the region directive:

element.attr(“ng-click”, “regionClick()”);

Add the ng-click attribute to the region element in order to run the regionClick function, you guessed it, when we click the region.

element.removeAttr(“region”);

In order for the ng-click attribute to take effect we’ll need to compile the element again. However, if we do this, Angular will read the region attribrite, reattach the directive again, and consequently the universe will explode.

Luckily interstellar catastrophe can be avoided if we remove the region attribute from the element. Phew!

$compile(element)(scope);

More Angular magic, you know the drill by now.

Our map regions are now augmented with the mighty power of Angular. Onward!

Mapping in the data

If we want our map regions to tie up the dummyData object, we’ll need to give each of the regions access to the data via its scope:

https://gist.github.com/TweededBadger/383f73171d29da6b5964

When you click on a region it will hopefully alert to you the corresponding value for that region. Hardly a huge leap forward I know but don’t worry we’ll get to the pretty stuff in a minute.

A note: I am not going to go into great detail about the sometimes confusing world of Angular directive scopes. However, if you’re having trouble I shall point you in the direction of this great blog post on the subject: http://www.undefinednull.com/2014/02/11/mastering-the-scope-of-a-directive-in-angularjs/

Here’s the new bits:

regionElement.attr(“dummy-data”, “dummyData”);

Set the dummyData attribute on the region element so we can tie it into the region directive scope.

scope: {
dummyData: “=”
},

Create a two-way binding with the dummyData object.

scope.regionClick = function () {
alert(scope.dummyData[scope.elementId].value);
};

Use the id of the SVG element to access the corresponding data value.


Pretty Colours

Time to bring our data to life with pretty colours! The easiest way to do this is with a custom Angular filter.

https://gist.github.com/TweededBadger/91f8d660b7d1898ccc39

This filter is quick & ugly but it does the job for the purposes of this tutorial. It will take our data value and convert it in a rgba string that we can use as a fill with the SVG region.

Add the following to the region directive:

https://gist.github.com/TweededBadger/cf4d60becb94978ee04a

Reload again and bask in the majesty of our colourful map!

Click away on the button to see the colours of the map regions change in response.


Tying things up

To add some extra usability, lets’s highlight the specific region in the table when you hover over the map and vice versa.

Make the following edits and additions:

Nhttps://gist.github.com/TweededBadger/d5069823501761936d6a

When you hover over a region on the map the corresponding data should be highlighted in the table. What is happening here is that when you hover over a region we are setting the hoverRegion variable within the main controller scope. Subsequently, the active class is set on the corresponding region div.

To make this work the other way around add the following:

https://gist.github.com/TweededBadger/c2cce0b36adc8e986e8f

The hoverRegion variable is now changed when you hover over the divs in the table and the map regions will behave as if being hovered over.


Scaling new heights

The colours on the map don’t mean much at the moment. Let’s create a scale to add some context using the custom filter we created earlier.

https://gist.github.com/TweededBadger/95ae3e62699c62e7e536

Your scale should look like this:


Tidy, tidy, tidy.

The current layout looks a bit rubbish, so let’s take advantage of some Bootstrap styles by adding some extra divs. Edit index.html:

https://gist.github.com/TweededBadger/e0926b599b049585ceff

This puts the map and data into 2 columns and adds a few panels.

And there you have it, a data driven SVG map using AngularJS.

Questions? Comments? Abuse? All are welcome on this reddit thread or on twitter.


Enjoyed this tutorial? Please hit the “recommend” button to scratch the part of my brain that thrives on meaningless internet points. If you want to know more about me go to http://tweededbadger.com/.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.