A List Apart

Menu

Cross Platform Scalable Vector Graphics with svgweb

by Published in Browsers, HTML · 15 Comments

Pity Scalable Vector Graphics. It’s been an official standard since before IE6 was released yet has never found much of an audience on the web, certainly not the one it deserves. Just as SVG was starting to establish some browser support, along came the canvas tag, stealing the thunder of dynamically generated client-side images.

Issue № 323

Despite all the attention being paid to canvas, there’s still a place for good ole SVG, particularly for developers looking to replace plug-ins like Flash for data visualization.

Canvas and SVG

One of the reasons that Flash has been a popular choice for data visualization is the rich graphics API that was introduced several versions back. This graphics API, native to Flash’s programming language ActionScript, makes it possible to draw dynamic charts, graphs, and maps programmatically, based on data or user input—without having to fiddle with any server-side middleware, and all in the browser. This was a revelation when it was introduced and drove a great deal of innovation in interactive info graphics.

A few years ago, Apple introduced the canvas tag to allow developers building Dashboard widgets a mechanism to draw arbitrary shapes using just HTML. That proposal, as you no doubt are aware, is now an official part of the HTML5 spec. The drawing methods canvas introduced are actually quite similar to the ones that ActionScript developers have been using for a while. In fact, I’d wager that anyone who’s done any dynamic drawing in Flash could move over to canvas pretty easily. I’d double-down on that bet and say that the similarity between drawing with ActionScript and canvas is at least partially responsible for canvas’ success.

There are also JavaScript-based drawing libraries like Raphaël—in fact, you’ve probably already seen Brian Suda’s excellent article on using the Raphaël JavaScript library to render SVG dynamically in the browser. Raphaël is great for abstracting a lot of the complexity of SVG and for working cross-browser, all the way back to IE6; but one thing it doesn’t do is let you load external SVG files, similar to how you would load an image using the img tag.

What does the ability to load an SVG file have to do with anything? Well, for one, it means that you can use third party tools like Illustrator or the excellent (and free!) Inkscape for drawing your SVG images, which can then be loaded and manipulated in the browser using JavaScript. The difference is all about how you approach the problem—ActionScript, canvas, and Raphaël let you draw images programmatically with code, while SVG lets you draw images in a drawing tool, load that image into a browser and add scripting.

To make all of this work, we’re going to be using an open-source JavaScript library called svgweb to load the SVG file, add events, and handle cross-browser issues. The way svgweb handles older browsers, like IE, is really quite clever: Rather than try to map SVG to IE’s proprietary drawing APIs—which is what Raphaël does—they built their own SVG renderer in Flash. That Flash-based SVG renderer loads the external SVG files and also handles JavaScript events that are passed from the browser, completely transparently.

The svgweb library detects if the browser has native SVG support and, if not, it embeds the Flash shim that translates SVG into Flash-native drawing commands. (This is similar to how some HTML5 audio and video players are using Flash as a fallback for playing H.264 encoded video in browsers that don’t support the video tag or H.264 encoded video.) DOM-style events are handled and passed back and forth between the Flash shim and the browser so that you only have to write the event logic once, in JavaScript, instead of having to write ActionScript handlers as well. This means that svgweb brings SVG to something like 95% of browsers, including mobile devices like the iPhone and iPad that handle SVG natively. (Android devices don’t currently support native SVG rendering, but they’re working on it.)

Before we go further, I just want to point out that I’m not advocating one method over another; I think there’s actually room for canvas, Raphaël, and SVG. There’s probably even overlap, which is good news for developers because it lets us pick the approach that works best for our circumstances. I’ve found that canvas seems particularly well suited for immediate, client-side raster graphic rendering; simple charts are a great use for canvas and the excellent Flot library makes building charts with JavaScript incredibly easy. SVG is well suited for displaying complex shapes, like a map, that you might prefer to draw in an application like Illustrator first and then use JavaScript to link dynamic data on top of it.

A data map with SVG

Here’s a request my team of designers and developers at msnbc.com gets all the time: Create a map to illustrate a news story. Maps are handy and we’ve developed a set of templates to quickly publish them, some of which are built on top of existing tile-based services like Google or Bing, some of which are custom Flash applications. Plenty has already been said about the power of building on top of something like Google maps or even rolling your own tile-based map engine. For info graphics, however, tile-based maps can be overkill or even distracting; it’s nice often to have a custom drawing.

A classic example would be a nation-wide map showing data by state, like the red/blue maps on election night. For this example, we’ll use data from the most recent U.S. census to show which states gained or lost representatives.

Before we get started, there’s one bit of configuration to set up. Because some browsers have a hard time loading the necessary script and Flash file directly from your computer’s filesystem, it’s best to serve them from a web server. You could upload everything to your web host but you probably have a webserver built right into the computer you’re developing on. On a Mac, create a new folder called svg-info in the Sites folder in your home directory, then go into System Preferences > Sharing and enable Web Sharing. Point a web browser to http://localhost/~your-username/svg-info (replacing your-username with your actual username) and you’ll see your svg-info folder in the browser.

With that done, you’re going to need a map, in SVG. These can come from a variety of sources—the designer you’re working with could build one in Illustrator or use a tool like ArcGIS to generate one from a shapefile. Or you can nab one from Wikipedia, which has a whole collection of free SVG maps, including a blank US map with each state as its own labeled shape. Properly labeling each shape is important when it comes time to link the data with the map; fortunately, SVG files have a DOM much like HTML files, with each state contained in its own labeled node. This is definitely something you’ll want to keep in mind if you’re generating your own maps or shapes.

This map’s a little big for most web pages, so the first thing you’ll want to do is size it down a little, using Illustrator or Inkscape, both of which will open and edit SVG files natively. This already shows one of the advantages of using SVG—a designer can open and edit vector files using the tools she uses every day. I sized the map down to 500 pixels wide so that it would fit on a page. Make sure to size the containing document down as well. (In Illustrator, go to File > Document Setup > Edit Artboards then set “Fit Artboard to Artwork bounds” from the presets.)

Next, you’ll need some data, which we’ll write into the page as a table to make it as accessible as possible. The table will look something like this:

<table id="results-data">
  <tr>
  <th>State</th>
  <th>Change</th>
 </tr>
 <tr>
  <td>AL</td>
  <td>0</td>
 </tr>
 <!-- snip! -->
 <tr>
  <td>WI</td>
  <td>0</td>
 </tr>
</table>

Now, this table doesn’t exactly reflect the sort of real-world complexities you run into with real data, but it should do for our purposes.

You might ask why write out a table at all if the purpose is to show a graphic and the answer is all about making your data accessible. Search engine spiders and screen readers, even JavaScript-aware ones, probably won’t be able to understand the data on your map in any kind of a meaningful way, so this table will help them index your data. SVG is, in theory, more accessible than raster graphics like JPEG, GIF, and PNG (for one, SVG is scalable so readers with low vision can zoom in without losing fidelity) because the image itself is described as XML. The reality, however, is that support for accessibility in SVG is still fairly nascent so you can’t count on screen readers being able to access the information in SVG. A humble table will work wonders here.

With that data on the page, now we’ll start writing some JavaScript to do something more interesting with it. First, download the latest svgweb files (the latest official release is from this past August but a new one is coming any day now) and unzip them.

Open the svgweb folder you uncompressed, then open the src folder and copy svg.js, svg.swf, and svg.htc to a folder on your webserver. Using a subfolder like js is fine, just make sure to keep all three files in the same folder. Now, include the svg.js as the first script tag in the head of your HTML file.

<script src="js/svg.js"></script>

Now, let’s write a function that will grab the data from the table and convert it into a JSON object. Drop this in a script tag after the script source to include the svgweb library (Line wraps marked » —Ed.):

//convert table to json
//@param t is the id of the table (string)
//returns a json object with an array containing an object for each row
function tableToJSON(t){
    var table = document.getElementById(t),
        header_row = table.rows[0],
        n_headers = header_row.cells.length,
        n_rows = table.rows.length,
        keys = [],
        n_keys = 0,
        json_data = [],
        json = {};    //rip through every row to create the JSON data objects
    //assume the first row contains the headers
    for(var i=0; i < n_rows; i++){
        if(i===0){
            for(var j=0; j < n_headers; j++){
               keys.push(header_row.cells[j].firstChild»
.nodeValue.toLowerCase());
            }
        } else{
            var this_row = table.rows[ i ]
            var data = {};
            n_keys = keys.length;            for( var j=0; j < n_keys; j++){
                data[keys[j]] = this_row.cells[j].firstChild.nodeValue;
            }            json_data.push(data);
        }    }    json[t] = json_data
    return(json);
}
   

There’s really not much to this function—it takes as a single parameter (the name of the table) and then loops through every row to assemble and return a JSON object. It assumes the first row of the table are labels for each column, which are then used for each of the name value pairs. Given the table above, our JSON object will look something like:

{ "results-data": [
    {"state": "AL", "change": 0},
     //... snip! ...
    {"state": "WI", "change": 0}
  ]
}

(One quick production note: this is an attempt to keep the code accessible without delving too deeply into the minutiae of backend implementations. Your needs might require a separate JSON feed from your database, for instance.)

Enough about the data, let’s add the map. We’re going to use an object tag to load the SVG map as an external resource, with a conditional comment to deal with IE. The IE-specific object uses a few different parameters, namely src instead of data and classid instead of type. Also note that of the SVG files you load, most come from the same source (your webserver) as the HTML file itself—no cross-site scripting allowed.

<div id="results-map">
   <!--[if IE]-->
       <object src="states.svg" classid="image/svg+xml"
           width="500" height="375"
           id="state-map" alt="Interactive map graphic showing»
electoral gains and loses per state as of the 2010 census">
   <!--[endif]-->
   <!--[if !IE]-->
       <object data="states.svg" type="image/svg" 
           width="500" height="375"
           id="state-map" alt="Interactive map graphic showing»
electoral gains and loses per state as of the 2010 census">
   <!--![endif]-->
       </object>
</div>

This should be pretty straightforward—the src/data attribute (depending on the browser) points to the SVG file; the width and height should be set to at least as large as the size you saved the SVG file earlier. You should also give each object the same id for later reference. Note that setting the width or height in the object tag won’t affect the size of the embedded file, just the object containing it. If the object’s width/height are smaller, you’ll see scrollbars; bigger, you’ll have whitespace. The alt attributes will provide some guidance to bots and screen readers.

If you load this page in your browser, you’ll see a gray map of the US of A. And if you use the browser’s native zoom in and out, you’ll see that the graphic is, in fact, scalable.

Now, it’s time to link the data to the map. As is so often the case with JavaScript programming, you want to make sure the map has loaded before you try to access it with a script. Fortunately, svgweb provides an onsvgload event that you can listen for that fires once the SVG has loaded. We’ll have it call an init function that will handle the data-to-map processing:

function init(){}window.onsvgload = init;

Inside of that init function, we need to first grab the map, using the standard getElementById with an extra property, contentDocument, which lets us grab the SVG itself.

var map_svg = document.getElementById('state-map').contentDocument;

Once you’ve grabbed the SVG map, it behaves similarly to an ordinary DOM object.

Next, we’ll call our tableToJSON function to get the data.

//tableToJSON will return a JSON object with our data
var map_json = tableToJSON('results-data');
//grab the actual array from the json object
var map_data = map_json['results-data'];
//get the array length so you don't calculate it»
for every loop iteration
var n_map_data = map_data.length;

Now, we just loop through the results and apply the data to our map.

//loop through the map data and assign properties»
to each state on the map
for(var i=0; i < n_map_data; i++){
    //get the state on the map based on the ID in the data array
    //in this case, the ID is the state abbreviation
    var map_state = map_svg.getElementById(map_data[ i].state);    if(map_data[ i].change === '-2 or more' ){
        map_state.style.fill = '#B26600';
    } else if (map_data[ i].change === '-1'){
        map_state.style.fill = '#FF9200';
    } else if (map_data[ i].change === '+1'){
        map_state.style.fill = '#19ABFF';
    } else if (map_data[ i].change === '+2 or more'){
        map_state.style.fill = '#0071B2';
    }    //assign the state a title, in this case the name of the state»
and the change in seats
    map_state.title = map_data[ i].state + '»
will have a change of ' + map_data[ i].change + ' seats';    //give each state an event
    //for simplicity, the event is a simple alert that displays»
   the title param we just assigned
    //yes, addEventListener even works in IE, thanks to svgweb
    map_state.addEventListener('mouseup', function(e){
        alert(e.target.title);
    }, false);
}

What this loop does is first look for any SVG node with an ID that matches the state in our data array—both are abbreviations such as “AL” for Alabama. When it matches the state on the map to the state on our data array, it colors the state according to the change, from a dark orange to dark blue.

Finally, we give each node a title tag. This will let us access this information when we assign a click handler to each state. You may notice that we use addEventListener, a DOM-but-not-IE-supported standard for listening for JavaScript events. Thankfully, the smart folks behind svgweb have handled that for us and have abstracted event handling to use the standard model. This is a very simple event that pops up a JavaScript alert with the title that we just assigned.

Like it’s 1999

SVG has held a lot of unrealized promise for over a decade now, with baked in support finally becoming mainstream (even IE9 will include native SVG rendering when it launches later this year). Unlike canvas or other script-only approaches, SVG can be easily divided into design and code elements, with just a little code to add interactivity. It even works on devices like the iPad and iPhone. And now, thanks to svgweb and a clever use of Flash, it works on older platforms no one could have ever imagined supporting SVG.

It’s time to add SVG to our arsenal of tools for building an amazing web.

About the Author

15 Reader Comments

Load Comments