Click here to Skip to main content
Click here to Skip to main content
Go to top

SVG World Map

, 4 Oct 2011
Rate this:
Please Sign up or sign in to vote.
An interactive World Map using SVG and jQuery.

Table of Contents

Introduction

Given the latest developments in the technology area (dear future reader: this is September 2010), we have been flooded with news and also facts stating that web development is going to receive much more focus from now on. The growing number of mobile devices (SmartPhones/tablets) and portables (notebooks/netbooks) with access to reliable and steady broadband internet access justifies such a trend.

Also, the fact that some important mobile browsers (such as Safari in iPad, iPhone) and IE10 in Windows 8 Metro (notice that here I'm talking about the Metro interface, made for tablets, not Windows 8 for desktops/notebooks) do not/will not support plug-ins such as Flash and Silverlight. This means that, if you're a web developer working with either Flash or Silverlight, you will need to rethink your strategy if you want to deliver web applications to such devices. This is not the case with the Android platform, but here we have reached (again) the point where you, as a web developer, must be sure that your application runs in all intended platforms. Flash (and more recently, Silverlight) have been ubiquitous for many years now, and this was a good thing for developers working with these technologies. In many ways, these plug-ins solved the problems related with the inherent differences between browsers, so the results were uniform, no matter what platform used your application.

After some months playing with the basics of HTML5, I was waiting for the right moment to do something with SVG. Although this article is an excuse to put some SVG theories I learned into action, fortunately I ended up creating something that (hopefully) might be useful for us web developers.

System Requirements

In order to run the SVG World Map sample attached to this article, you must have a browser that supports HTML5 SVG:

  • Internet Explorer 8 or superior
  • Firefox
  • Safari
  • Chrome

The Case for SVG

Since I have some WPF (Windows Presentation Foundation) and JavaScript background, I must admit that I feel rather comfortable with the SVG "language" (which resembles WPF geometry elements) and the ways to manipulate web elements (jQuery, of course). It's nice when you can use previous knowledge to learn new things, isn't it?

In this project, I used jQuery SVG, a great jQuery plug-in created by Keith Wood that allows you to interact with the SVG Canvas in a programmatic way.

For example, the CIA Factbook website below features a very nice Flash-based world map (disclaimer: I have no connections with CIA at all). When you move the mouse over a country, its territories are highlighted and a little box with the country's name is shown. This is the kind of application I would like to create using native HTML5/SVG/JavaScript features, without relying on plug-in infrastructures like Flash/Silverlight.

In order to create a new SVG programmatically, I started by downloading an SVG file from Wikipedia (http://upload.wikimedia.org/wikipedia/commons/a/ad/World_map_blank_with_blue_sea.svg) and disassembling it. There may be many elements inside an SVG, but for the sake of brevity, let's assume that the particular SVG file that we are using has three basic elements: SVG, g, and path, SVG being the top-level element, while g stands for "group" of inner elements, and path is the element holding the complex geometry for the border of the countries.

jQuery SVG Plug-in

jQuery SVG is an awesome plug-in created by Keith Wood. Usually you wouldn't be able to do much with SVG using the standard jQuery framework. But fortunately, jQuery SVG makes your like much easier. If you are familiar with jQuery, then you know how to access DOM elements using selectors:

var myDiv = $('#myDiv');

Now suppose you want to create a brand new SVG and attach it to your div. All you need to do is:

var mySVG = $('#myDiv').svg();

From now on, you may want to refer to your newly created SVG. jQuery SVG allows you to use selectors like this:

var svg = $('#myDiv').svg('get');

And finally, you may want to draw a yellow circle with a 3-pixel thick red stroke. No need to dive into HTML, since you can do it programmatically:

svg.circle(100, 50, 50, {fill: 'yellow', stroke: 'red', strokeWidth: 3});

Reverse Engineering the Original World Map SVG File

Here was the really hard part of the project: I took the original .svg file and split it into multiple arrays of strings (that were later store in a JavaScript file), one group for each country. I call it "reverse engineering SVG". I did it all manually, and some countries were not included, so please forgive me if your country is not found in the map (I can include them in future versions of the article, by the way).

For example, the image below shows the elements that make up Bolivia in the original .svg file. You can notice that the g element and the path element have a lot of information:

<g xmlns="http://www.w3.org/2000/svg" class="landxx bo" id="bo" 
  style="fill:#ffffff;fill-opacity:1;stroke:#000000;
         stroke-opacity:1;stroke-width:0.64507740000000002;
         stroke-miterlimit:3.97999999999999998;stroke-dasharray:none">
  <path d="M 742.08629,854.27855 C 743.99329,854.40255 745.90929,854.56755 747.81129,
          854.71055 C 748.68329,854.77555 748.23629,854.92055 748.36629,855.44055 
          C 748.60129,856.38055 749.95729,855.41755 750.33829,855.25255 C 750.77829,
          855.06155 751.65329,854.87955 751.93629,854.43355 C 752.37129,853.74755 752.51029,
          852.77755 753.16229,852.24055 C 754.60629,851.05155 755.75629,852.96855 756.54129,
          851.05155 C 757.22429,849.38255 759.34629,849.30355...">
  <path d="M 750.24716,899.89622 C 750.26405,899.89762 749.7766,
           899.86692 749.13517,899.68022 C 748.83492,
           899.59283 749.36535,898.78795 749.64135,898.64095...>
</g>

The above SVG code was converted to JavaScript in the form of an array of strings, like this:

p = [];
p[p.length] = 'M 742.08629,854.27855 C 743.99329,854.40255 745.90929,
      854.56755 747.81129,854.71055 C 748.68329,854.77555 748.23629,854.92055...
p[p.length] = 'M 750.24716,899.89622 C 750.26405,899.89762 749.7766,
      899.86692 749.13517,899.68022 C 748.83492,899.59283 749.36535,898.78795...

var bo = { pathCollection: p, id: 'bo', translate: [29.90172, 45.07447] };

Where "bo" is the anonymous object holding the data for the country "Bolivia". Later in the JavaScript file, the South American continent is created when we pass an array of countries to the drawCountries function:

var countries = [ar, bo, br, cl, co, ec, gf, gu, py, pe, sr, uy, ve, gy];
drawCountries(svg, scale, countries);

The "reverse engineering" is complete when we run the drawCountries function. Notice the path.move, path.curveC, and path.line methods, that moves, draws a curve, and draws a line inside the SVG path, respectively.

function drawCountries(svg, config, countries, translate) {
    //Let's draw one country at a time
    for (var c = 0; c < countries.length; c++) {
        var country = countries[c];
        var g;

        //code removed for sake of brevity

        //Each country has a collection of path. Let's draw them
        for (var i = 0; i < country.pathCollection.length; i++) {
            var splitted = country.pathCollection[i].split(' ');

            var path = svg.createPath();

            var index = 0;
            
            //Here we parse each path
            while (index < splitted.length) {
                var command = splitted[index];

                switch (command) {
                    //M x,y = "Move to point (x,y)", that is, the path
                    // will start at this absolute position (x,y)
                    case 'M':
                        var moveconfig1 = splitted[index + 1].split(',');
                        path.move(moveconfig1[0], moveconfig1[1]);
                        index += 2;
                        break;
                    //C x1,y1 x2,y2 x3,y3 = "Curve (x1,y1,x2,y2,x3,y3)",
                    // and draws a bézier curve using 3 control points (xn,yn)
                    case 'C':
                        var curveCconfig1 = splitted[index + 1].split(',');
                        var curveCconfig2 = splitted[index + 2].split(',');
                        var curveCconfig3 = splitted[index + 3].split(',');
                        path.curveC(curveCconfig1[0], curveCconfig1[1],
                                    curveCconfig2[0], curveCconfig2[1],
                                    curveCconfig3[0], curveCconfig3[1]
                                    );
                        index += 4;
                        break;
                    //L x,y = "Straight Line (x,y)", a line segment starting
                    //at the current position ans ending in (x,y) point
                    case 'L':
                        var lineconfig1 = splitted[index + 1].split(',');
                        path.line(lineconfig1[0], lineconfig1[1]);
                        index += 2;
                        break;
                }
            }

            svg.path(g, path, { id: country.id, countryId: country.id });
        }
        
        //code removed for sake of brevity
    }
}

At this point, we have re-created the SVG. That was the hard part. Now is where the fun begins, where we can interact with the SVG and its components.

Interacting With SVG

In order to listen to mouse events triggered by SVG, we must bind the proper events. In this example, we are going to bind mousemove, mouseenter, mouseout, and click. Certainly there are more events we could use, but these are enough for the functionalities we need for our project:

$(svg.root()).bind('mousemove',
function (path) {
        var offset = $(config.selector).position();
        $('#box').attr('transform', 
          'translate(' + path.pageX + ' ' + path.pageY + ')');
    });

$('#' + country.id, svg.root()).bind('mousemove',
    function (path) {
        var g = path.target.parentNode;

        if (countryBoxFadeOut) {

            if (config.showCountryBoxOnMouserEnter && 
                (lastPoint[0] != path.pageX &&
                lastPoint[1] != path.pageY)) {
                showCountryBox(svg);
            }

            timer = setTimeout(function () {
                if (lastCountryId == currentCountryId) {
                    clearTimeout(timer);
                    hideCountryBox(svg);
                }
            }, 1000);
        }

        lastPoint = [path.pageX, path.pageY];
    });

$('#' + country.id, svg.root()).bind('mouseenter',
    function (path) {

        $('#box').attr('transform', 'translate(' + path.pageX + ' ' + path.pageY + ')');

        var g = path.target.parentNode;

        $(g).attr('opacity', config.activeCountryOpacity);
        $(g).attr('fill', config.activeCountryFill);
        $(g).attr('stroke', config.activeCountryStroke);
        $(g).attr('strokeWidth', config.activeCountryStrokeWidth);

        config.countryId = path.target.id;
        config.pos = [path.pageX, path.pageY];

        lastCountryId = currentCountryId;

        currentCountryId = config.countryId;

        if (config.showCountryBoxOnMouserEnter) {
            showCountryBox(svg);
            var box = $('#box', svg.root());
            $(box).stop();
            $(box).animate({ svgOpacity: 1.0 }, 100);
            var txt = $('#txtBox', svg.root());
            var name = getCountryName(config.countryId).split(',')[0];
            txt[0].textContent = name.toUpperCase();
        }

        timer = setTimeout(function () {
            if (lastCountryId == currentCountryId) {
                clearTimeout(timer);

                if (config.showCountryBoxOnMouserEnter)
                    hideCountryBox(svg);
            }
        }, 1000);

        if (config.onmouseenter) {
            config.onmouseenter(config);
        }
    }
);

$('#' + country.id, svg.root()).bind('mouseout',
function (path) {
    var g = path.target.parentNode;
    $(g).attr('fill', config.inactiveCountryFill);
    $(g).attr('opacity', config.inactiveCountryOpacity);
    $(g).attr('stroke', config.inactiveCountryStroke);
    $(g).attr('strokeWidth', config.inactiveCountryStrokeWidth);
    $('#box').stop();
});

$('#' + country.id, svg.root()).bind('click',
function (path) {
    if (config.onclick) {
        var g = path.target.parentNode;
        
        config.onclick(getCountryName(g.id));
    }
});

Some Sample Code

Before proceeding with the sample code, let's see some configurations required for the SVG World Map:

Config Options

  • id: (string) programmatic ID of the map
  • selector: (string) CSS selector to which the map SVG element will be attached
  • scale: (number) the scale used to render the map.
  • margin: (string) the CSS margin for the map SVG element
  • top: (string) the CSS top margin for the map SVG element
  • height: (number) the CSS height for the map SVG element
  • width: (number) the CSS width for the map SVG element
  • inactiveCountryFill: (string) the CSS fill used to fill inactive countries
  • inactiveCountryStroke: (string) the CSS stroke used to outline inactive countries
  • inactiveCountryStrokeWidth,: (number) the stroke thickness used to outline inactive countries
  • activeCountryFill: (string) the CSS fill used to fill active countries
  • activeCountryStroke: (string) the CSS stroke used to outline active countries
  • activeCountryStrokeWidth,: (number) the stroke thickness used to outline active countries
  • showCountryBoxOnMouserEnter: (bool) determines whether the name box must be shown when the user moves the mouse over the country
  • drawNorthAmerica: (bool) determines whether North America should be rendered
  • drawCentralAmerica: (bool) determines whether Central America should be rendered
  • drawSouthAmerica: (bool) determines whether South America should be rendered
  • drawEurope: (bool) determines whether Europe should be rendered
  • drawAfrica: (bool) determines whether Africa should be rendered
  • drawAsia: (bool) determines whether Asia should be rendered
  • drawOceania: (bool) determines whether Oceania should be rendered
  • drawAntarctic: (bool) determines whether Antarctica should be rendered

Events

  • onCountryMouseEnter: triggered when the mouse enters a country
  • onCountryMouseMove: triggered when the mouse moves over a country
  • onCountryMouseOut: triggered when the mouse leaves a country
  • onCountryMouseClick: triggered when the mouse is clicked over a country

var wm1 = WorldMap({
    id: 'map1',
    selector: '#svgWorldMap1',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: '#4af',
    inactiveCountryStroke: '#fff',
    inactiveCountryStrokeWidth: 6,
    showCountryBoxOnMouserEnter: true,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

var wm2 = WorldMap({
    id: 'map2',
    selector: '#svgWorldMap2',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: 'transparent',
    inactiveCountryStroke: '#ccc',
    inactiveCountryStrokeWidth: 4,
    activeCountryFill: 'orange',
    activeCountryStroke: '#ccc',
    activeCountryStrokeWidth: 0,
    showCountryBoxOnMouserEnter: false,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

var wm3 = WorldMap({
    id: 'map3',
    selector: '#svgWorldMap3',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: '#ccc',
    inactiveCountryStroke: 'gray',
    inactiveCountryStrokeWidth: 6,
    activeCountryFill: 'orange',
    activeCountryStroke: 'gray',
    activeCountryStrokeWidth: 6,
    showCountryBoxOnMouserEnter: true,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

var wm4 = WorldMap({
    id: 'map4',
    selector: '#svgWorldMap4',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: '#686',
    inactiveCountryStroke: '#fff',
    inactiveCountryStrokeWidth: 6,
    showCountryBoxOnMouserEnter: false,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

Final Considerations

Here we finish our little project with HTML5 SVG. Remember that SVG is your friend, there is a great potential behind it, and it's up to you to explore it.

The code is not 100% polished, and also certainly there is room for many interesting improvements. Feel free to use it as you wish, I will be glad if you do.

If you reached this line, then thank you very much for your patience. I hope this article was informative, or at least fun, in some way.

History

  • 2011-09-30: Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marcelo Ricardo de Oliveira
Software Developer
Brazil Brazil
Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for ILang Educação.
 
He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
 
Published Windows Phone apps:
 
 
Awards:
 
CodeProject MVP 2012
CodeProject MVP 2011
 
Best Web Dev article of March 2013
Best Web Dev article of August 2012
Best Web Dev article of May 2012
Best Mobile article of January 2012
Best Mobile article of December 2011
Best Mobile article of October 2011
Best Web Dev article of September 2011
Best Web Dev article of August 2011
HTML5 / CSS3 Competition - Second Prize
Best ASP.NET article of June 2011
Best ASP.NET article of May 2011
Best ASP.NET article of April 2011
Best C# article of November 2010
Best overall article of November 2010
Best C# article of October 2010
Best C# article of September 2010
Best overall article of September 2010
Best overall article of February 2010
Best C# article of November 2009

Comments and Discussions

 
QuestionExcellent work... Pinmemberian sexton1-Jan-13 8:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 4 Oct 2011
Article Copyright 2011 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid