Tuesday, November 13, 2012

Using Raphael Sets to Mimic SVG Groups

Raphael provides a nice library for simplifying working with vector graphics. One of its strengths is to work with both SVG and VML making it compatible with older versions of IE. However, with that compatibility comes some sacrifice in features. For instance, you are not able to define SVG groups with the Raphael API. Instead, you use the sets feature to mimic some of the functionality available with the grouping element.

Consider the example on the W3 site related to SVG arcs. This example SVG document does two things - a) it defines a reusable set of SVG objects which are later referenced in the document so they don't have to be declared again, and b) it uses groups to apply transforms to all the contained elements which simplifies the definition.

If you look at the source, you'll see the two ellipses and some text are defined in the "defs" tag:

<g id="baseEllipses" font-size="20" >
<ellipse cx="125" cy="125" rx="100" ry="50"
fill="none" stroke="#888888" stroke-width="2" />
<ellipse cx="225" cy="75" rx="100" ry="50"
fill="none" stroke="#888888" stroke-width="2" />
<text x="35" y="70">Arc start</text>
<text x="225" y="145">Arc end</text>

That definition is then reference five times to create the base objects that form the background that other elements are drawn on. The "g" tag is utilized to define each section of the example and translate it to a different point on the screen:

<g transform="translate(400,0)">
<text x="50" y="210">large-arc-flag=0</text>
<text x="50" y="250">sweep-flag=0</text>
<use xlink:href="#baseEllipses"/>
<path d="M 125,75 a100,50 0 0,0 100,50"
fill="none" stroke="red" stroke-width="6" />

In Raphael, there is not a way to create "defs" and "g" tags. However, you can use the clone() and sets to achieve similar results. I replicated the example SVG document completely with Raphael on my sandbox. The first step was to define the SVG canvas and then create a set that will represent the base ellipse objects that were defined in the "defs" tag:

var r = Raphael( $('.wrapper')[0], '12cm', '5.25cm' ).setViewBox(0,0,1200,525,false),
g1 = r.set(),
g2, attr;

attr = { 'stroke':'#888888', 'stroke-width':'2' };
g1.push( r.ellipse(125,125,100,50).attr(attr) );
g1.push( r.ellipse(225,75,100,50).attr(attr) );

attr = { 'text-anchor': 'left', 'font':'1.3em "Verdana"' };
g1.push( r.text(70,60,'Arc start').attr(attr) );
g1.push( r.text(270,140,'Arc end').attr(attr) );

You can see that the code matches pretty well with the actual SVG document. The difference with the above code is that the objects created will serve as both the first ellipses/text displayed in the top/left of the page and as the cloned object for the remaining four groups. In the SVG example, the "defs" group is not visible - a separate declaration was required to use it.

Next, I used clone() on the set object to create a new set of SVG objects. I then added the specific elements to the group and then transformed them to move the elements to the correct position on the screen:

g2 = g1.clone();

attr = { 'stroke':'red', 'stroke-width':'6' };
g2.push( r.path('M 125,75 a100,50 0 0,0 100,50').attr(attr) );

attr = { 'text-anchor': 'left', 'font':'1.5em "Verdana"' };
g2.push( r.text(150,210,'large-arc-flag=0').attr(attr) );
g2.push( r.text(150,250,'sweep-flag=0').attr(attr) );


If you inspect the elements on the page, you'll see that no "defs" or "g" tags are generated by Raphael. It produces a fairly flat document constructed with the basic SVG objects. The transform operation is applied to all the elements in the set and not just to a containing group tag. The set operations are internal to Raphael and have no real implementation in the DOM. This allows Raphael to select between SVG/VML depending on the browser to ensure compatibility. However, the downside is that you don't get all the native SVG features. The Raphael API does still provide flexible features to manage groups of elements - it just abstracts it from the underlying implementation.