In previous articles I’ve demonstrated how SVG can be used to create responsive, scalable imagemaps, but have left the interactive part - aside from simple hover effects - largely unexplored, with a few exceptions. That changes with this article and the one following, which explore how to create a full, in-depth interface using SVG and JavaScript: in this case, a geographical map.
Sourcing The Map
One of the pleasures of using SVG today is the fact that a decade of relative obscurity has provided the opportunity for a long period of quiet professional content development, much of it free and/or open source. For example, every country is available as an SVG drawing, together with most geographical regions. I’ll use Wikipedia’s SVG map of Canada as a source for this interactive.
Cleaning up the source
This is not to say that every SVG drawing is exactly how you need it; many are over-coded, and most need to be cleaned up to some degree. In the case of Wikipedia’s map, I did the following:
- Used Adobe Illustrator to straighten the map and eliminate territory in the northernmost provinces (sorry, Nunavut!) in order to save space.
- I also used Illustrator’s
Simply
tool to reduce the number of points in the SVG, minimizing file size and clutter. UsingPreview
mode while doing this is strongly recommended, as too high a value will make the map appear somewhat “melted”.Reducing the vector map to ~85% precision is a good balance between accuracy and file size - I also renamed the
id
attributes of each layer; these become the values associated with each group or path.
After export, the SVG was further cleaned up:
- The opening SVG tag becomes:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1386 822" id="canada">
- Each province or territory is surrounded by a group, if one doesn’t already exist; the group has the appropriate
id
.<g id="Alberta"> <path d="M435.3,670.2c-13.1-1.1-44…" > </g>
- A stylesheet is added to the SVG:
#canada { fill: hsl(120, 98%, 30%); } #canada g { transition: .3s; } #canada g:hover { fill: hsl(120, 98%, 60%); cursor: pointer; }
- A
<title>
tag is added inside each group for accessibility:<g id="Alberta"> <title>Alberta</title> <path d="M435.3,670.2c-13.1-1.1-44…" > </g>
(The title text will also appear on mouse rollover for all users).
Adding Info
I’ve added extra information about each territory inside a <desc>
element for each group. <desc>
content is not rendered on the page, as it is traditionally reserved for accessibility. For Alberta, the added markup looks like this:
<desc>
<image xlink:href="moraine_lake.jpg" alt="A lake surrounded by mountains">
</image>
<p>One of Canada"s four prairie provinces, and
the most populous, Alberta borders a single US
state (Montana). It is divided from British
Columbia by the Rocky Mountains…</p>
</desc>
The entire SVG code is embedded into an HTML page, with a <div>
added above it:
<div id="provinceInfo"></div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1386 822" id="canada">
Adding Style
The initial CSS for the map is as follows:
* { box-sizing: border-box; }
body {
background: #333;
color: #fff;
font-family: Avenir, Calibri, sans-serif;
}
#canada-map {
fill: hsl(120, 55%, 20%);
}
#canada-map g {
transition: .3s;
}
#canada-map g:hover {
fill: hsl(120, 55%, 40%);
cursor: pointer;
}
HSL is used to create easy color shifts; fill
is applied the SVG root element so that the paths inside will inherit the color. Much the same philosophy is used to affect the paths through the g
element (since some provinces and territories consist of more than one path, a hover
style applied to a path
element would affect only that path; applied to the group element, all paths inside the group are affected equally.)
The provinceInfo
element is currently blank; we’re going to add CSS to style the content that will be applied to it with JavaScript.
#provinceInfo {
position: absolute;
top: 0; right: 0;
width: 25%;
background: rgba(0,0,0,0.3);
pointer-events: none;
opacity: 0;
transition: 1s;
}
@media all and (max-width: 800px) {
#provinceInfo { width: 40%; }
}
#provinceInfo h1 {
background: hsl(240, 55%, 40%);
padding: .3rem;
padding-left: 1rem;
margin-top: -.5rem;
font-weight: 400;
}
#provinceInfo p {
margin-left: 2rem;
margin-right: 2rem;
}
#provinceInfo img {
width: 100%;
}
There’s a little more styling to add, but for now let’s turn to a script added to the bottom of the page:
Adding Interaction
var canadamap = document.getElementById("canada-map"),
provinceInfo = document.getElementById("provinceInfo"),
allProvinces = canadamap.querySelectorAll("g");
canadamap.addEventListener("click", function(e){
var province = e.target.parentNode;
if (e.target.nodeName == "path") {
for (var i=0; i < allProvinces.length; i++) {
allProvinces[i].classList.remove("active");
}
province.classList.add("active");
var provinceName = province.querySelector("title").innerHTML,
provincePara = province.querySelector("desc p");
sourceImg = province.querySelector("img"),
imgPath = "";
provinceInfo.innerHTML = "";
provinceInfo.insertAdjacentHTML("afterbegin",
"<img src="+imgPath + sourceImg.getAttribute("xlink:href")+"
alt=""+sourceImg.getAttribute("alt")+""><h1>"+provinceName+
"</h1><p>"+provincePara.innerHTML+"</p>");
provinceInfo.classList.add("show");
}
})
An explanation: after identifying the provinceInfo
popup window, all of the provinces and territories are gathered together by finding all the g
elements. When the user clicks inside the SVG, event bubbling is used to determine which path the user clicked on (and to check that the user didn’t click on an area of ocean).
Since SVG doesn’t have a proper :focus
state, I’ve used an active
class applied to the group to indicate the current province / territory; the same class is removed from all the others.
Searching inside the current group, the title
of the group is used to determine the correct province name, the paragraph inside the desc
for the description, and the src
and alt
of the hidden image for the illustrative photograph. imgPath
is currently blank, as it is assumed that all files, including the photographs, are in the same location as the SVG.
All of this is concatenated together and written inside a cleared provinceInfo
using insertAdjacentHTML
. The window is then made visible with a class of show
, using a final declaration added to the CSS:
#provinceInfo.show { opacity: 1; }
At this stage, the info window does not have a Close button; to get around this issue, the <div>
element has had pointer-events: none
added to it, making it “transparent” to mouse actions and allowing the user to click through to paths underneath it.
Photographs by Mariusz Kluzniak, Kent McFarland, Daryl Mitchell, Vincent Lock, Vince Alongi, Timothy Neesam, Paul Bica, KingstonGal and Nicolas Raymond, licensed under Creative Commons.
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/vNoeyW