Wednesday, September 26, 2012

Using SVG Elements with jQuery

Exploring how SVG can provide some interesting enhancements to a page becomes more possible as support and consistency increase across browsers.  SVG elements offer a lot of flexibility for shaping content beyond the square box.   Of course, with the added features, some complexity is to be expected.  SVG adds a whole new lexicon to the markup language.  One that has a much steeper learning curve than normal HTML and its accompanying styles.  Because of this, libraries have been built to try to make it easier to dynamically create SVG objects and get them into the DOM.  Before jumping into one of these libraries, I typically like to see how to make something work so I can fully understand the advantages the library is offering.  Since I'm already using jQuery, I thought I'd start by seeing what can be done with jQuery alone.  As a reference, I decided to compare my results with the jQuerySVG plugin since it is suppose to enhance jQuery to provide SVG functionality.  RaphaĆ«l is another popular SVG library which has no other dependencies.   However, at this point, I'm most interested in working with jQuery and learning how much I can do with it.

As a starting point, I attempted to add a SVG circle to an existing svg element on my page:


var $svg = $('#mysvg');
$('circle').appendTo($svg);


However, nothing happened. As it turns out, you can't just use document.createElement() to add SVG DOM Nodes. You have to use document.createElementNS() which is something jQuery is not doing. I'm not quite ready to admit defeat here. $() accepts a DOM object, so I decided to write a little function that could take a SVG tag and use createElementNS() to create and return the element:


function SVG(tag)
{
return document.createElementNS('http://www.w3.org/2000/svg', tag);
}


Then, I can just place SVG() into the $() call to get a jQuery object. Notice that I did not even add the element to the DOM - just created an element to work around jQuery.


var $svg = $('#mysvg');
$(SVG('circle')).appendTo($svg);


Ok, this technically doesn't add anything visible to the page. However, there is an element at least in the DOM now. For this circle to look like anything, I need to set the required attributes for a circle:


var $svg = $('#mysvg');

$(SVG('circle'))
.attr('cx', 130)
.attr('cy', 75)
.attr('r', 50)
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-width', 3)
.appendTo($svg);


I can set all the available options for the circle using the jQuery.attr() function. So, with only a minor work-around, I can dynamically create, modify, and add an SVG element to the DOM.

As a comparison, here is how you would do the equivalent with jQuerySVG:


var svg = $('#jqsvg').svg('get');
svg.circle(130, 75, 50, {fill: 'none', stroke: 'red', strokeWidth: 3});


Here, you need to first get a reference to the SVG object via the svg() plugin method. Then you can access the drawing functions to create a circle. If you want to set anything other than the object specific properties (in this case the circle's center and radius), you need to pass an object naming those specific options. In my opinion, your not really gaining anything so far. I prefer the explicit attribute names in my original example over having to remember what the first 3 parameters mean in the circle() call. It may seem overwhelming to learn all the possible tags and attibutes, but there is really good reference available at the W3 site

jQuerySVG exists for a reason, so I decided to try a more complex example. Just adding SVG objects to the DOM is clearly not enough. We want to try animate and hook event listeners to those objects.

I created a side-by-side example which illustrates adding a rectangle and animating its size and color. I also attached a handler to the click event to see how that works. What I found is jQuerySVG does provide some nice shortcuts for animating SVG properties. jQuery animate() won't do it automatically so you have to use the step callback to do the work yourself.

Without jQuerySVG's help:

$mr.animate({tabIndex: 0},
{
duration: 2000,
step: function (now, fx)
{
$(this)
.attr('width', 150 + Math.round(50 * fx.pos))
.attr('height', 25 + Math.round(25 * fx.pos)+'%')
// .attr('stroke', 'aqua') Need color transistion helper to make this happen
.attr('stroke-width', 3 + Math.round(7 * fx.pos));
}
});


Using jQuerySVG to animate properties:

$(sr).animate({
svgWidth: 200,
svgHeight: '50%',
svgStrokeWidth: '+=7',
svgStroke: 'aqua'
}, 2000);


As you can see, I have to do the math myself to animate the attributes and I can't really do the color animation without some helper logic to make it work.

One other caveat worth mentioning when working with jQuery and SVG is the case-sensitivity of the attribute names. The following animate tag will not work:


$(SVG('animate'))
.attr('attributeName', 'stroke')
.attr('dur', '2s')
.attr('begin', 'indefinite')
.attr('values', 'blue;aqua')
.attr('repeatCount', 1)
.attr('fill', 'freeze')
.appendTo($mr)


jQuery converts all attributes to lower case before setting them. Unfortunately, attributeName and repeatCount must maintain their case (at least in FireFox) to work. Anywhere there is a camel case attribute, you'll run into this problem. The circle tag used dashes to separate words (ie stroke-width) so I don't why there's an inconsistency other than animate is from the SMIL standards. To work around this problem, you'd have to call the DOM setAttribute() method directly on the animate object.

This exercise has shown me that I can use jQuery to work with SVG without any helper libraries. There are some clear limitations that need to be considered when taking this approach. A library like jQuerySVG does provide a means to abstract those details and add some clear convenience functionality (like animating SVG properties). Depending on the complexity of the project, using an SVG library may make sense for those reasons. However, if you know the limitations and are willing to work around them, you can get most of the functionality without any additional libraries.