HTML5 Zone is brought to you in partnership with:

Raymond Camden is a developer evangelist for Adobe. His work focuses on web standards, mobile development and Cold Fusion. He's a published author and presents at conferences and user groups on a variety of topics. He is the happily married proud father of three kids and is somewhat of a Star Wars nut. Raymond can be reached via his blog at www.raymondcamden.com or via email at raymondcamden@gmail.com Raymond is a DZone MVB and is not an employee of DZone and has posted 259 posts at DZone. You can read more from them at their website. View Full User Profile

Playing with SVG and JavaScript

02.07.2013
| 4218 views |
  • submit to reddit

For some reason, I never took a real good look at SVG (MozDev link) in the past. I knew, conceptually, that it was a way to describe graphics in XML format and I knew Adobe had a large amount of history/support behind it, but I never really thought it was useful for what I do in my day to day work. That was until last week when a reader sent in an interesting question.

The reader wanted to take county data for America and render it on screen. Initially I was at a loss as to how this would be done. Obviously the AJAX portion wasn't a big deal, but I had no idea how in the heck this would be rendered. The reader then linked me to this resource, a map of America in SVG with each county defined in pure, glorious data. I did a bit of research and discovered that SVG data exists in the browser DOM. This means you can edit - as far as I know - pretty much anything - related to the data. Woot!

I decided to start simple though. I knew that Adobe Illustrator could spit out SVG files, so I opened up Illustrator and used all of my artistic talent to create this.

While not the most exciting graphic in the world, it gave me something to start with. The SVG file is completely XML based. You can see the source for it here:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="300px" height="300px" viewBox="0 0 300 300" enable-background="new 0 0 300 300" xml:space="preserve">
<rect x="19" y="37" fill="#52B848" stroke="#000000" stroke-miterlimit="10" width="80" height="59" id="greenBlock" />
<rect x="168" y="53" fill="#E61E25" stroke="#000000" stroke-miterlimit="10" width="80" height="59"/>
<rect x="35" y="179" fill="#DFE21D" stroke="#000000" stroke-miterlimit="10" width="227" height="81"/>
<text transform="matrix(1 0 0 1 102 215)" font-family="'MyriadPro-Regular'" font-size="12" id="textBlock">TESTING</text>
</svg>

Having never seen SVG before you can take a good guess as to what each part does. I did a bit of research and discovered that one way to access the SVG data is via the getSVGDocument() DOM call. In order for this to work though you have to wait for a load event on the image. I used an object tag to embed the SVG file, added an event listener, and then did two simple modifications.

<script>

function svgloaded() {
  console.log("test");
	var svgEmbed = document.querySelector("#svgembed");
	var svg = svgEmbed.getSVGDocument();

	var td = svg.getElementById("textBlock");
	td.textContent = "Foo";

	var gn = svg.getElementById("greenBlock");
	gn.setAttribute("fill", "#ff0000");

}

document.addEventListener("DOMContentLoaded", function(){

	var svgEmbed = document.querySelector("#svgembed");
	svgEmbed.addEventListener("load", svgloaded);

},false);
</script>


<object data="Untitled-1.svg" id="svgembed"></object>

Not terribly exciting, but also pretty darn simple. You can run the demo here, or look at the screen shot below.

I tested this in Chrome, Firefox, and IE10 and it worked great there. From what I could see in my research, the "basics" of SVG worked pretty well everywhere, but the more advanced options weren't universally available. (Check out this list of SVG options at CanIUse.com for more information.)

So given that we have JavaScript access to individual parts of a SVG document and we can easily modify that data, I took a look back at the American county SVG document. It is a very large file (about 2 Megs) and includes data that 'draws' each county in a path tag. Here is an example of two counties.

  <path
     style="font-size:12px;fill:#d0d0d0;fill-rule:nonzero;stroke:#000000;stroke-opacity:1;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel"
     d="M 345.25498,268.988 L 345.54298,269.051 L 345.62898,269.092 L 347.56698,273.203 L 345.67898,273.306 L 345.24598,272.761 L 344.01998,271.43 L 343.64598,271.151 L 345.25498,268.988"
     id="22121"
     inkscape:label="West Baton Rouge, LA" />
  <path
     style="font-size:12px;fill:#d0d0d0;fill-rule:nonzero;stroke:#000000;stroke-opacity:1;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel"
     d="M 336.45198,274.027 L 338.12498,273.058 L 338.70498,276.839 L 338.56598,277.345 L 337.41198,276.88 L 336.69998,276.384 L 335.77298,276.078 L 334.94298,276.037 L 336.45198,274.027"
     id="22055"
     inkscape:label="Lafayette, LA" />

As I didn't have access to any real data to display, I whipped up a quick ColdFusion script that parsed the XML, reads in the county data, and creates a value for each county from 1 to 10. For now, let's assume the number represents how likely you are to be eaten by a grue. My script then saved this file as X.json, where X represented a year. I created files for 1990 to 1995. Now I had my data, I just needed to build the application around it.

One of the first things I wanted to do was ensure that this huge SVG file wasn't dragging down the entire application. My boss, Alan Greenblatt, recently posted on this topic: Asynchronously Loading SVG. I decided to do something a bit more complex. While his solution would allow me to delay loading the SVG, I wanted to skip loading the SVG altogether if the browser supported IndexedDB. I quickly whipped up some logic to handle this.

var storeIndexedDB = false;
var db;
		
$(document).ready(function() {
			
	$("#svg-content").html("<p>Loading content...</p>");

	//check for indexeddb and cache
	if("indexedDB" in window) {
		loadViaIndexedDB();                
	} else {
		loadViaAjax();
	}
});

function loadViaIndexedDB() {
	var openRequest = indexedDB.open("svgdemo",1);

	openRequest.onupgradeneeded = function(e) {
		var thisDB = e.target.result;
		
		console.log("running onupgradeneeded");
		
		if(!thisDB.objectStoreNames.contains("binarycache")) {
			var os = thisDB.createObjectStore("binarycache", {autoIncrement:true});
			os.createIndex("name", "name", {unique:true});
		}
	}

	openRequest.onsuccess = function(e) {
		console.log("running onsuccess");
		
		db = e.target.result;

		//Do we have it?
		var transaction = db.transaction(["binarycache"],"readonly");
		var store = transaction.objectStore("binarycache");
		var index = store.index("name");

		var request = index.get("usa.svg");

		request.onsuccess = function(e) {
			
			var result = e.target.result;
			if(result) {
				console.log('had it in store');
				loadSVG(result.data);
			} else {
				//Don't have it, so load Ajax way and flag to cache
				storeIndexedDB = true;
				loadViaAjax();
			}	
		}	

	}	

	openRequest.onerror = function(e) {
		//Do something for the error
	}
   
}
		
function loadViaAjax() {
	$.get("USA_Counties.svg", {}, function(res) {
		console.log("svg loaded via ajax");
		if(storeIndexedDB) {
			console.log("I supposed indexeddb, so will store");
			db.transaction(["binarycache"],"readwrite").objectStore("binarycache").add({data:res,name:"usa.svg"});
		}
		loadSVG(res);
	},"text");

}

That's quite a bit of code I suppose, but it really just boils down to the following steps:

  1. The user's browser supports IndexedDB
    1. Open the database and see if we need to create the initial store
    2. See if we have the data in there already
    3. If not, set a flag saying we are ready to store the SVG and fire off the request to load it via Ajax
  2. No IndexedDB? No problem. Just load it via Ajax
  3. And do one last check to see if we did support IndexedDB but just had not loaded yet. If so, store that puppy.

The end result is to then call loadSVG.

function loadSVG(data) {
	console.log('about to use the svg');
	$("#svg-content").html(data);
	$(".yearButton").on("click", loadYear);
}

All this does is draw out the SVG into the DOM and add listeners to a few buttons. The buttons are used to load the remote JSON data I created earlier. Now let's look at that.

function loadYear(e) {
	var year = $(this).text();
	$.get(year + ".json", {}, function(res) {
		for(var i=0, len=res.length;i<len; i++) {
			//console.log(res[i].ID);
			var id = res[i].ID;
			var total = res[i].DATA;
			$("#"+id).attr("style","fill:"+dataToCol(total));
		}
	},"json");
}

//I just translate 1-10 to a color
function dataToCol(x) {
	switch(x) {
		case 1: return "#ff0000";
		case 2: return "#d61a00";
		case 3: return "#cd3300";
		case 4: return "#b34d00";
		case 5: return "#9a6600";
		case 6: return "#808000";
		case 7: return "#669A00";
		case 8: return "#4db300";
		case 9: return "#1ad600";
		case 10: return "#00ff00";
	}
}

loadYear is a relatively simple Ajax call to our JSON file. Once we have the data, I loop over each county, get the value, and translate the 1-10 value to a color from red to green. Thanks to Cliff Johnston (@iClifDotMe) for the RGB values.

The end result is pretty cool I think. The JSON files clock in at about 70K each so they aren't too bad to load. You can see the full demo here: http://www.raymondcamden.com/demos/2013/feb/2/test3.html

For me, it took about 5 seconds to load initially, and each Ajax call "feels" about a second or so. Considering the amount of data being pushed around I feel like it performs adequately.

But then I decided that wasn't enough. I figured if we are caching the county shapes, why not cache the remote data? I whipped up a quick modification to store the data in the SessionStorage cache of the browser.

function loadYear(e) {

	var year = $(this).text();

	if("sessionStorage" in window && sessionStorage[year]) {
		renderYearData(JSON.parse(sessionStorage[year]));
	} else {
		$.get(year + ".json", {}, function(res) {
			console.log("storing to "+year);
			if("sessionStorage" in window) sessionStorage[year] = res;
			renderYearData(JSON.parse(res));
		},"text")
	}
}

A trivial modification, but if you click around a bit it should be a bit snappier. You can find that demo here: http://www.raymondcamden.com/demos/2013/feb/2/test4.html




Published at DZone with permission of Raymond Camden, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Fabrizio Giudici replied on Thu, 2013/02/07 - 7:19am

It depends on what you want to do. For a customer that heavily relies upon SVG rendering (and much more complex files, converted from Autocad drawings) no JS solution has proved to be viable, so far, in terms of performance; also considering that the document not only needs to be rendered once, but also zoomed, panned, and animated promptly. The Java solution (with Batik) works very well and it seems one of the field where JS is still (far) behind.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.