Saturday, May 27, 2006

Inline dynamic SVG from XML with Ajax

I had this problem I was trying to solve for work, and surprisingly, I couldn't find the solution anywhere else online, so I solved it myself and now offer the fruits of my labor.

Problem: retrieve report data in XML format from a SOA application and generate an HTML textual representation of the report data (ex. a table) as well as a graphical representation (ex. bar graph, pie chart, etc) on the client side using Ajax.

Solution: ok, there's plenty of information online regarding generating the XMLHTTPRequest to fetch the XML data and render HTML, so I won't go into that here other than to say I utilized the prototype.js package to make my life a little simpler. Also, to make my XML manipulation easier cross-browser, I utilized the zXML package from Nickolas C. Zakas (co-author of Professional Ajax which is where I read about it). The one caveat regarding using the zXML package, is that I had to use it to parse the XML from the XMLHTTPRequest.responseText instead of using the pre-parsed XMLHTTPRequest.responseXML like so:


currentReportDOM = zXmlDom.createDocument();
currentReportDOM.async = false;
currentReportDOM.loadXML(request.responseText);


Ok, so I have the report XML data in a DOM object in the variable named currentReportDOM. For this example I chose to format the XML into SVG using XSLT. You could of course do direct manipulation of the DOM object in Javascript if you'd prefer. Nothing spectacular in the XSLT file so I won't show it's contents here, but I apply the XSLT as shown:


// first convert the XML to SVG using XSLT
var xslDom = zXmlDom.createDocument();
xslDom.async = false;
xslDom.load("chart.xsl");
var str = zXslt.transformToText(currentReportDOM.documentElement, xslDom);


Now, the str variable contains the SVG XML as a string, and here's where the difficulties start. If this was XHTML in Firefox, I could just paste in the SVG data in some div like:


$("reportSVGDiv").innerHTML = str;


However, since this needs to work cross-browser, and Internet Explorer doesn't handle .xhtml files, this is a .html file and when you do that in HTML Firefox no longer renders it as an SVG, but just as plain XML, and Internet Explorer doesn't know what to do with it either since it doesn't support SVG natively.

Most instructions for embedding inline SVG in an HTML page for Internet Explorer instruct you to use either an embed, an object, or an iframe and set the source as the SVG file. This works great as along as your SVG data is actually in a file and the file ends with either a .svg or a .svgz(compressed) extension. If it was in a file, I could add an object to my page dynamically with the SVG file like:


var svgObject = document.createElement('object');
svgObject.setAttribute('type', 'image/svg+xml');
svgObject.setAttribute('data', 'svgdata.svg');
$("reportSVGDiv").appendChild(svgObject);


And it would be rendered. However, in this case, the data was received as raw XML from a server and rendered to SVG by the browser. Theoretically, you could add the data to the object inline like this:


var svgObject = document.createElement('object');
svgObject.setAttribute('type', 'image/svg+xml');
svgObject.setAttribute('data', 'data:image/svg+xml,'+ str);
$("reportSVGDiv").appendChild(svgObject);


However, since you're no longer referencing a file with a .svg or .svgzextension, the IE MIME type handling -- which depends on file extensions, not the specified MIME type (image/svg+xml) -- won't recognize it as an SVG.

How to get it working in Internet Explorer:

To get Internet Explorer working you first have to install the Adobe SVG Viewer plugin so that IE can render SVGs. Then you have to define the svgnamespace in your HTML header like:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">


Then in the head section add the following lines to associate the Adobe SVG viewer with the svg namespace (since Internet Explorer bases MIME types off of file extensions and we have no file in this case).


<object id="AdobeSVG" classid="clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2"></object>
<?import namespace="svg" implementation="#AdobeSVG"?>


Now, as long as the SVG data you generate is tagged with the svg namespace (ex. <svg:svg>...</svg:svg>) then placing the SVG XML data in the innerHTML of some div will actually work! Additionally, you don't want the SVG embedded in an object tag in IE since Adobe SVG Viewer always disables scripting when it determines that the SVG file is embedded using the OBJECT tag.

How to get it working in Firefox:

Ironically, even though Firefox supports SVG natively (in XHTML) you can't just stick it in the HTML without embedding it in an object(or embed, or iframe). So, we have to build the object and add the SVG data inline as I demonstrated earlier, but I'll give the complete example now:


// remove any old charts
while ($("reportSVGDiv").hasChildNodes()) {
$("reportSVGDiv").removeChild($("reportSVGDiv").firstChild);
}
var svgObject = document.createElement('object');
svgObject.setAttribute('name', 'svg');
svgObject.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgObject.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgObject.setAttribute('data', 'data:image/svg+xml,'+ str);
svgObject.setAttribute('width', '1050');
svgObject.setAttribute('height', '550');
svgObject.setAttribute('type', 'image/svg+xml');
$("reportSVGDiv").appendChild(svgObject);


How to get it working cross-browser:

The last piece in the puzzle is how to determine when we're in IE with the Adobe SVG viewer installed, and when we're not. To do that, I use a method to check for the Adobe SVG viewer:


// isASVInstalled
//
// utililty function to check for Adobe SVG viewer
//
function isASVInstalled() {
try {
var asv = new ActiveXObject("Adobe.SVGCtl");
return true;
}
catch(e){ }
return false;
}


Putting it all together:

With all the pieces in place, my final Javascript function to render the SVG inline with dynamic XML data received via Ajax from an SOA looks like:


// toSVG
//
// convert the report XML data to a SVG (scalable vector graphic) image
//
function toSVG() {
// first convert the XML to SVG using XSLT
var xslDom = zXmlDom.createDocument();
xslDom.async = false;
xslDom.load("chart.xsl");
var str = zXslt.transformToText(currentReportDOM.documentElement, xslDom);

// check if we're using the AdobeSVG viewer (Internet Explorer)
if (isASVInstalled() ) {
$("reportSVGDiv").innerHTML = straw;
} else {
// otherwise, we're assuming firebug/Mozilla which can render the SVG directly
// if this was true XHTML instead of HTML, then firebug would also render it directly
// using the inheriting above, however, since it has to be HTML to make IA happy,
// we then have to wrap the SVG in an tag
// You can wrap the SVG in an tag for IA as well if you include the SVG from
// a file with a .svg extension, but since we're generating it dynamically, we don't
// have a file. The HTML spec supports loading the data inline, however, since IE's
// MIME types depend on file extensions, this doesn't work in IA.

// remove any old charts
while ($("reportSVGDiv").hasChildNodes()) {
$("reportSVGDiv").removeChild($("reportSVGDiv").firstChild);
}
var svgObject = document.createElement('object');
svgObject.setAttribute('name', 'svg');
svgObject.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgObject.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgObject.setAttribute('data', 'data:image/svg+xml,'+ straw);
svgObject.setAttribute('width', '1050');
svgObject.setAttribute('height', '550');
svgObject.setAttribute('type', 'image/svg+xml');
$("reportSVGDiv").appendChild(svgObject);
}
}


I hope this helps someone and saves them from the aggravation I encounted trying to get this to work!

5 comments:

Steven Pothoven said...
This comment has been removed by a blog administrator.
Steven Pothoven said...
This comment has been removed by a blog administrator.
Anonymous said...

You may have made a typo in the name of the "str" variable:
var str = zXslt.transformToText(currentReportDOM.documentElement, xslDom);
probably should be:
var straw = zXslt.transformToText(currentReportDOM.documentElement, xslDom);

Anonymous said...

Smiles said: "...all I get is the text elements in my svg rendered as though they were plain text."

Same here. Firefox just works by transforming the xml. But I can't get IE 7 to show anything except the raw text from my text elements. Anybody know why that might be?

Sumukh said...

Thanks a lot for you post. It did save me a lot of time. However, when I was adding content from javascript, I couldn't add my svn stuff in the 'data' attribute of the object as you have shown in the firefox case. After reading some portions of the w3c spec I learnt that the object element tries to render its contents if it's unable to fetch the url in data attribute. So assigning my svg text to its innerHTML worked.