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) {
for (var c = 0; c < countries.length; c++) {
var country = countries[c];
var g;
for (var i = 0; i < country.pathCollection.length; i++) {
var splitted = country.pathCollection[i].split(' ');
var path = svg.createPath();
var index = 0;
while (index < splitted.length) {
var command = splitted[index];
switch (command) {
case 'M':
var moveconfig1 = splitted[index + 1].split(',');
path.move(moveconfig1[0], moveconfig1[1]);
index += 2;
break;
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;
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 });
}
}
}
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 mapselector
: (string) CSS selector to which the map SVG element will be attachedscale
: (number) the scale used to render the map.margin
: (string) the CSS margin for the map SVG elementtop
: (string) the CSS top margin for the map SVG elementheight
: (number) the CSS height for the map SVG elementwidth
: (number) the CSS width for the map SVG elementinactiveCountryFill
: (string) the CSS fill used to fill inactive countriesinactiveCountryStroke
: (string) the CSS stroke used to outline inactive countriesinactiveCountryStrokeWidth,
: (number) the stroke thickness used to outline inactive countriesactiveCountryFill
: (string) the CSS fill used to fill active countriesactiveCountryStroke
: (string) the CSS stroke used to outline active countriesactiveCountryStrokeWidth,
: (number) the stroke thickness used to outline active countriesshowCountryBoxOnMouserEnter
: (bool) determines whether the name box must be shown when the user moves the mouse over the countrydrawNorthAmerica
: (bool) determines whether North America should be rendereddrawCentralAmerica
: (bool) determines whether Central America should be rendereddrawSouthAmerica
: (bool) determines whether South America should be rendereddrawEurope
: (bool) determines whether Europe should be rendereddrawAfrica
: (bool) determines whether Africa should be rendereddrawAsia
: (bool) determines whether Asia should be rendereddrawOceania
: (bool) determines whether Oceania should be rendereddrawAntarctic
: (bool) determines whether Antarctica should be rendered
Events
onCountryMouseEnter
: triggered when the mouse enters a countryonCountryMouseMove
: triggered when the mouse moves over a countryonCountryMouseOut
: triggered when the mouse leaves a countryonCountryMouseClick
: 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.