Click here to Skip to main content
15,867,686 members
Articles / Web Development / HTML5

SVG World Map

Rate me:
Please Sign up or sign in to vote.
4.92/5 (60 votes)
4 Oct 2011CPOL7 min read 216.4K   6.6K   102   60
An interactive World Map using SVG and jQuery.

Image 1

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:

Image 2

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

The Case for SVG

Image 3

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.

Image 4

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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

Image 5

HTML
<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:

JavaScript
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:

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

JavaScript
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:

JavaScript
$(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

Image 6

JavaScript
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;
    }
});

Image 7

JavaScript
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;
    }
});

Image 8

JavaScript
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;
    }
});

Image 9

JavaScript
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)


Written By
Instructor / Trainer Alura Cursos Online
Brazil Brazil

Comments and Discussions

 
QuestionEasier alternative Pin
Anna Ivanov8-Jul-20 2:44
Anna Ivanov8-Jul-20 2:44 
QuestionMicrosoft JScript runtime error Pin
Member 960134318-Feb-13 20:35
Member 960134318-Feb-13 20:35 
QuestionMuitObrigado Pin
Joan Blanq8-Feb-13 8:51
Joan Blanq8-Feb-13 8:51 
QuestionExcellent work... Pin
ian sexton1-Jan-13 8:30
ian sexton1-Jan-13 8:30 
QuestionGreat !! Pin
Member 942749113-Sep-12 2:08
Member 942749113-Sep-12 2:08 
GeneralMy vote of 5 Pin
Raj2016429-Jul-12 3:39
Raj2016429-Jul-12 3:39 
QuestionGreat Job Pin
Member 196519118-Mar-12 16:00
Member 196519118-Mar-12 16:00 
AnswerRe: Great Job Pin
Marcelo Ricardo de Oliveira20-Mar-12 4:34
mvaMarcelo Ricardo de Oliveira20-Mar-12 4:34 
GeneralMy vote of 5 Pin
Akram El Assas10-Mar-12 0:14
Akram El Assas10-Mar-12 0:14 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira20-Mar-12 4:33
mvaMarcelo Ricardo de Oliveira20-Mar-12 4:33 
GeneralMy vote of 5 Pin
Paulo Bueno11-Dec-11 18:51
Paulo Bueno11-Dec-11 18:51 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira1-Mar-12 15:45
mvaMarcelo Ricardo de Oliveira1-Mar-12 15:45 
Obrigado Paulo!
There's no free lunch. Let's wait for the dinner.

Take a look at Windows Phone Labyrinth here in The Code Project.

QuestionWorks nice. Future request. Pin
@dri@n23-Nov-11 2:10
@dri@n23-Nov-11 2:10 
AnswerRe: Works nice. Future request. Pin
Marcelo Ricardo de Oliveira1-Mar-12 15:45
mvaMarcelo Ricardo de Oliveira1-Mar-12 15:45 
Questionsweden Pin
Seçkin24-Oct-11 19:08
Seçkin24-Oct-11 19:08 
AnswerRe: sweden Pin
Marcelo Ricardo de Oliveira1-Mar-12 15:44
mvaMarcelo Ricardo de Oliveira1-Mar-12 15:44 
GeneralMy vote of 5 Pin
jawed.ace20-Oct-11 2:06
jawed.ace20-Oct-11 2:06 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira1-Mar-12 15:44
mvaMarcelo Ricardo de Oliveira1-Mar-12 15:44 
GeneralMy vote of 5 Pin
User 625546418-Oct-11 1:52
User 625546418-Oct-11 1:52 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira18-Oct-11 4:38
mvaMarcelo Ricardo de Oliveira18-Oct-11 4:38 
GeneralMy vote of 5 Pin
Abhijit Jana14-Oct-11 3:11
professionalAbhijit Jana14-Oct-11 3:11 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira15-Oct-11 4:16
mvaMarcelo Ricardo de Oliveira15-Oct-11 4:16 
GeneralVote 5 Pin
AditSheth12-Oct-11 18:16
AditSheth12-Oct-11 18:16 
GeneralRe: Vote 5 Pin
Marcelo Ricardo de Oliveira13-Oct-11 12:08
mvaMarcelo Ricardo de Oliveira13-Oct-11 12:08 
GeneralMy vote of 5 Pin
Ahsan Murshed11-Oct-11 22:44
Ahsan Murshed11-Oct-11 22:44 

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

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