Showing posts with label SVG. Show all posts
Showing posts with label SVG. Show all posts

Wednesday, November 08, 2006

Using XML, XPath, and XSLT with JavaScript

There are several JavaScript XML libraries available to try to provide a cross-browser XML library. I've previously cited zXML in my Inline SVG article, there's also Sarissa and XML for <Script> and I'm sure others. However, when I started using them I discovered various problems and limitations with them for what I needed to do. So, I decided to write my own cross-browser XML routines. I wanted an XML library that was as lightweight as possible, doing just what I needed and utilizing as much of the built in browser capabilities as possible. In this article, I will first present a simple XML class which just holds the XML DOM object returned from an AJAX request and provides functions to get and change node values based on XPaths. Secondly, I will give a simple XSLT class to preform XSL Transformations. The complete code for the XML and XSL library is available here

XML

First off is the constructor which determined if we will be using IE (ActiveX) functions or W3C standard functions for XML manipulation and either stores the XML DOM returned as the responseXML from an XMLHttpRequest(XHR) or creates an empty DOM object for later manipulation.
//
// XML
//
function XML(xmlDom) {
this.isIE = window.ActiveXObject;
if (xmlDom != null) {
 this.xmlDom = xmlDom;
} else {
 // create an empty document
 if (this.isIE) {
    Try.these (
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument.5.0"); },
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument.4.0"); },
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument.3.0"); },
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument"); },
       function() { axDom = new ActiveXObject("Microsoft.XmlDom"); }
    );
    this.xmlDom = axDom;
 } else {
    this.xmlDom = document.implementation.createDocument("", "", null);
 }
}
};
In case you're not getting the XML data from an XHR response, I also provided a load function to load an XML file from an URL.
// load
//
// Loads an XML file from an URL
XML.prototype.load = function(url) {
 this.xmlDom.async = false;
 this.xmlDom.load(url);
};
Next, I want to be able to find a single node in the XML DOM based on an XPath. The getNode function will do that for me.
// getNode
//
// get a single node from the XML DOM using the XPath
XML.prototype.getNode = function(xpath) {
 if (this.isIE) {
    var result = this.xmlDom.selectSingleNode(xpath);
 } else {
    var evaluator = new XPathEvaluator();
    var result = evaluator.evaluate(xpath, this.xmlDom, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
 }
 return result;
};
More often, I don't need the actual DOM node object, but the value of that node, so I provide the getNodeValue a function to get the value of a node.
// getNodeValue
//
// get the value of a the element specified by the XPath in the XML DOM
XML.prototype.getNodeValue = function(xpath) {
 var value = null;
 try {
    var node = this.getNode(xpath);

    if (this.isIE &&amp; node) {
       value = node.text;
    } else if (!this.isIE && node.singleNodeValue) {
       value = node.singleNodeValue.textContent;
    }
 } catch (e) {}

 return value;
};
If you're just looking for a particular node/value, the previous functions are fine, but more often you need a group of node or node values (ex. all the book titles). So, the full JavaScript file also contains corresponding functions to get multiple nodes (getNodes) and node values as an array (getNodeValues). At some point, you'll probably want to access this XML DOM as XML data (string), so we need a function to serialize the XML from the DOM to a String. The getNodeAsXml function will do that for you. If you pass in the root XPath ("/") it will serialize the complete XML file. Also, the included prettyPrintXml function can be used to escape the HTML characters.
// getNodeAsXml
//
// get the XML contents of a node specified by the XPath
XML.prototype.getNodeAsXml = function(xpath) {
 var str = null;
 var aNode = this.getNode(xpath);
 try {
    if (this.isIE) {
       str = aNode.xml;
    } else {
       var serializer = new XMLSerializer();
       str = serializer.serializeToString(aNode.singleNodeValue);
    }
 } catch (e) {
    str = "ERROR: No such node in XML";
 }
 return str;
};
So far, we've only read the XML data we received. But we may also want to change the contents of the XML. So, here are the functions necessary to change existing node values (updateNodeValue), add new nodes/values (insertNode), and delete existing nodes (removeNode). Note: the XPaths used for node insertions cannot be overly complex as I wanted to keep this library simple.
// updateNodeValue
//
// update a specific element value in the XML DOM
XML.prototype.updateNodeValue = function(xpath, newvalue) {
 var node = this.getNode(xpath);
 var changeMade = false;
 newvalue = newvalue.trim();

 if (this.isIE &&amp; node) {
    if (node.text != newvalue) {
       node.text = newvalue;
       changeMade = true;
    }
 } else if (!this.isIE && node.singleNodeValue) {
    if (node.singleNodeValue.textContent != newvalue) {
       node.singleNodeValue.textContent = newvalue;
       changeMade = true;
    }
 } else {
    if (newvalue.length > 0) {
       this.insertNode(xpath);
       changeMade = this.updateNodeValue(xpath, newvalue);
    }
 }

 return changeMade;
};

// insertNode
//
// insert a new element (node) into the XML document based on the XPath
XML.prototype.insertNode = function(xpath) {
 var xpathComponents = xpath.split("/");
 var newChildName = xpathComponents.last();
 var parentPath = xpath.substr(0, xpath.length - newChildName.length - 1);
 var qualifierLoc = newChildName.indexOf("[");
 // remove qualifier for node being added
 if (qualifierLoc != -1) {
    newChildName = newChildName.substr(0, qualifierLoc);
 }
 var node = this.getNode(parentPath);
 var newChild = null;
 if (this.isIE && node) {
    newChild = this.xmlDom.createElement(newChildName);
    node.appendChild(newChild);
 } else if ((!this.isIE) && node.singleNodeValue) {
    newChild = this.xmlDom.createElement(newChildName);
    node.singleNodeValue.appendChild(newChild);
 } else {
    // add the parent, then re-try to add this child
    var parentNode = this.insertNode(parentPath);
    newChild = this.xmlDom.createElement(newChildName);
    parentNode.appendChild(newChild);
 }
 return newChild;
};

// removeNode
//
// remove an element (node) from the XML document based on the xpath
XML.prototype.removeNode = function(xpath) {
 var node = this.getNode(xpath);
 var changed = false;
 if (this.isIE &&amp; node) {
    node.parentNode.removeChild(node);
    changed = true;
 } else if ((!this.isIE) && node.singleNodeValue) {
    node.singleNodeValue.parentNode.removeChild(node.singleNodeValue);
    changed = true;
 }
 return changed;
};
You should now be able to fully read and manipulate XML using XPaths. For any significant reformatting or processing of the XML, you'll probably want to utilize XSLT. So, next I'll provide a simple XSLT class. It will preload (into a DOM object) an XSL file in its constructor. This provides rapid transformations of new XML data passed in to it's transform function (including parameters). First, the constructor which requires an URL pointing to the XSL file to be used for transformations. It does not accept a DOM object like the XML class did as it is assumed that the XSL (layout information) is more-or-less static and stored in a file whereas the XML data would most likely be dynamic and received via an XHR request.
//
// XSLT Processor
//
function XSLT(xslUrl) {
 this.isIE = window.ActiveXObject;
 if (this.isIE) {
    var xslDom = new ActiveXObject("MSXML2.FreeThreadedDOMDocument");
    xslDom.async = false;
    xslDom.load(xslUrl);
    if (xslDom.parseError.errorCode != 0) {
       var strErrMsg = "Problem Parsing Style Sheet:\n" +
          " Error #: " + xslDom.parseError.errorCode + "\n" +
          " Description: " + xslDom.parseError.reason + "\n" +
          " In file: " + xslDom.parseError.url + "\n" +
          " Line #: " + xslDom.parseError.line + "\n" +
          " Character # in line: " + xslDom.parseError.linepos + "\n" +
          " Character # in file: " + xslDom.parseError.filepos + "\n" +
          " Source line: " + xslDom.parseError.srcText;
         alert(strErrMsg);
       return false;
    }
    var xslTemplate = new ActiveXObject("MSXML2.XSLTemplate");
    xslTemplate.stylesheet = xslDom;
    this.xslProcessor = xslTemplate.createProcessor();
 } else {
    var xslDom = document.implementation.createDocument("", "", null);
    xslDom.async = false;
    xslDom.load(xslUrl);
    this.xslProcessor = new XSLTProcessor();
    this.xslProcessor.importStylesheet(xslDom);
}
};
Finally, the transform function will preform the XSL transformation and return the result. It accepts parameters to be passed to the XSL as an associative array of parameter name and value pairs.
// transform
//
// Transform an XML document
XSLT.prototype.transform = function(xml, params) {
 // set stylesheet parameters
 for (var param in params) {
    if (typeof params[param] != 'function') {
       if (this.isIE) {
          this.xslProcessor.addParameter(param, params[param]);
       } else {
          this.xslProcessor.setParameter(null, param, params[param]);
       }
    }
 }

 if (this.isIE) {
    this.xslProcessor.input = xml.xmlDom;
    this.xslProcessor.transform();
    var output = this.xslProcessor.output;
 } else {
    var resultDOM = this.xslProcessor.transformToDocument(xml.xmlDom);
    var serializer = new XMLSerializer();
    var output = serializer.serializeToString(resultDOM);
 }
 return output;
};
You should now be able to easily utilize XML and XSL on your web pages. An example usage of this XML/XSLT library would be to first build an XSLT object during the page initialization (onload).
xslProcessor = new XSLT(xslUrl);
Then, when XML responses are received from an AJAX request, process them. For example,
// get the XML data
try {
 var xmlData = new XML(request.responseXML);
} catch (e) {
 alert(request.responseText);
}

// get the username out of the XML
var userName = xmlData.getNodeValue('//userName');

// transform the XML to some HTML content
var newData = xslProcessor.transform(xmlData, {'param':'value'});
document.getElementById('someDiv').innerHTML = newData;
Update (February 27, 2007): I previously had a function in my XML class to generate valid HTML for the XML, but I've now modified it to use Oliver Becker's XML to HTML Verbatim Formatter with Syntax Highlighting stylesheet to format the XML. If that is not available, it will default to simply serialize the XML and convert the special characters. The code for this looks like:
/ Define a class variable which can be used to apply a style sheet to
// the XML for format it to HTML
XML.htmlFormatter = new XSLT("xsl/xmlverbatim.xsl");

// toHTML
//
// Transform the XML into formatted HTML
//
XML.prototype.toHTML = function() {
var html = null;
if (XML.htmlFormatter) {
 html = XML.htmlFormatter.transform(this);
} else {
 html = this.getNodeAsXml('/');
 if (html != null) {
  html = html.replace(/&/g, "&");
  html = html.replace(/</g, "&lt;");
  html = html.replace(/>/g, "&gt;<br/>");
 }
}
   return html;
}
As before, the complete package is available to download Update (March 7, 2007): I have updated this package to utilize Google's AJAXSLT XSL-T implementation when a native JavaScript XSLT function is not available. I need to test it more before releasing it however.

Thursday, October 26, 2006

Cross-browser inline SVG solved

In three previous posts (1, 2, 3) I've discussed getting browser-side dynamically generated SVGs to render properly across browsers. I now have a solution which is working for me.

Below is a screenshot my test application now. The XML used as sample data and the XSLT to product the SVG data was provided earlier.



(Note: I was also able to add some linking between the SVG and HTML so that as you mouse over each bar it will be highlighted in the SVG and simultaneously highlighted the corresponding row in the HTML table.)

In order to make this work I used the embed tag for IE and the object tag for all other browsers (tested with Firefox and Opera). My HTML has a div named svgDiv which will dynamically be given the SVG to render. For Firefox I can create my object with the dynamic SVG in one step, but for Internet Explorer, I first have to embed a static, blank SVG (as mentioned earlier) and then replace the blank SVG with the dynamic SVG. However, I determined that this could not be done all in one JavaScript function. I found that if I programatically add the embedded blank SVG, and them immediately replace the blank SVG with the dynamic one, it would fail, but having something like an alert message between adding the blank SVG and replacing it with the dynamic SVG would work. The only explanation I could reason for this is that the actual addition of the embed element is not completed by the browser until the JavaScript reaches a pause in processing (ie. not flushed/committed), and that the need to create the alert popup causes the changes to be committed. (does anyone know how to make the browser commit the changes?). My solution to this problem was to add the blank SVG when the page is loaded (if IE) so that it will be available for me to replace when I'm ready.

When the page is loaded, if the browser is IE I call this function to add the blank SVG:

// embedBlankSVG
//
// add an <embed> tag to display a blank static SVG
// this is needed for IE to replace with dynamic content later
SVG.prototype.embedBlankSVG = function(width, height) {
var svgEmbed = document.createElement('embed');
svgEmbed.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgEmbed.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgEmbed.setAttribute('pluginspage', 'http://www.adobe.com/svg/viewer/install/');
svgEmbed.setAttribute('src', 'blank.svg');
svgEmbed.setAttribute('width', width);
svgEmbed.setAttribute('height', height);
svgEmbed.setAttribute('type', 'image/svg+xml');
svgEmbed.setAttribute('id', 'embeddedSVG');
this.svgDiv.appendChild(svgEmbed);
};


Then, whenever I want to render a new SVG, I invoke this function:

// render
//
// render the dynamic SVG in the page
SVG.prototype.render = function() {
var str = this.toString();

if (this.isIE) {
// For IE, we use an <embed> tab

// build replacement SVG document
var newSvgElement = document.createElement("svg");
newSvgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
newSvgElement.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
newSvgElement.setAttribute("width", this.width);
newSvgElement.setAttribute("height", this.height);
newSvgElement.innerHTML = str;

// replace blank SVG with new SVG
domtodom(newSvgElement, 'embeddedSVG');
} else {
this.clearPage();

// For all others (mainly Mozilla based), we use an <object> tag
// with the SVG data added inline

var svgObject = document.createElement('object');
svgObject.setAttribute('name', 'svgObj');
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', '850');
svgObject.setAttribute('height', '350');
svgObject.setAttribute('type', 'image/svg+xml');
this.svgDiv.appendChild(svgObject);
}
}


where the toString function produces SVG data from XML data (whether read from a file or returned in an XMLHttpRequest response) and the domtodom function comes from Chris Bayes' domtodom.js

This was tested in Firefox 2.0, IE 7.0, and Opera 9.02 (though the table highlighting isn't currently working in Opera. It says I have a Security Error: attempted to read protected variable).


Help requested


I would like to be able to correspondingly highlight bars in the graph when I mouse over the table rows, but every attempt to add the:

<script type="text/ecmascript">
<![CDATA[
...
]]>
</script>

to the SVG data (as described here) immediately causes Firefox to crash.

Updated 12/07/2006 per comment below


The comment below asked what the clearPage function does. Since I can't format it nicely in the comments section, I decided to update the post with it. It doesn't really do anything too profound which is why I didn't post it originally.

// clear out any old displayed data from the page
SVG.prototype.clearPage = function() {
this.svgDiv.innerHTML = '';
this.tableDiv.innerHTML = '';

if (this.isIE) {
this.embedBlankSVG(this.svgWidth, this.svgHeight);
}
}


Additionally, while I'm updating this posting anyway, I was able to add JavaScript to my SVG through the XSLT (see Help Requested section) by adding it like this in the XSL:

<xsl:element name="script">
<xsl:attribute name="type">
<xsl:value-of select="'text/ecmascript'" />
</xsl:attribute>
<xsl:text>
&lt;![CDATA[
...
]]&gt;
</xsl:text>
</xsl:element>

I haven't gotten it to do everything that I want yet, but at least the browser no longer immediately crashes.

Tuesday, October 24, 2006

Scalable Vector Graphics (SVG) Intro

I ran across an introduction to SVG today entitled, Scalable Vector Graphics (SVG), Creating High-End 2D Graphics Using XML while I was looking for something else, but it gives a nice summary of SVG so I thought I'd share it. It covers the main questions:

  • What is SVG (and what is is not)?
  • How do you use SVG?
  • Why should you use SVG?
It's a few years old (2002), but still a nice article.

Thursday, July 06, 2006

Follow up on inline SVG

This is just a brief follow-up to my previous request for help.

I had built my XSLT to render the SVG as described in the article, Render dynamic graphs in SVG and my mouseover coloring events was modeled after the article, Add interactivity to your SVG, which were both articles from developerWorks.

So, my generated code for a bar in my graph looked like:

<svg:rect x="35" y="272.88" height="57.12" width="109" style="fill:rgb(49,0,0)">
<set attributeName="fill" from="rgb(49,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
However, after taking the advice from Martin Honnen (SVG Developers Yahoo! Group) my generated code for the same bar in the graph looks like:
<svg:rect x="35" y="272.88" height="57.12" width="109" fill="rgb(49,0,0)" onmouseover="evt.target.setAttribute('fill', 'red');" onmouseout="evt.target.setAttribute('fill','rgb(49,0,0)');"/>
The result is that the correct colorings are now displayed when rendered inline in IE, and the mouseover events work correctly in Firefox.

The only thing still not working is the mouseover events when rendered inline in IE.

Martin Honnen further suggested to start with a blank SVG placed in the page with the embed tag, and then access it to manipulate it with the getSVGDocument method (which should work in IE, Firefox 1.5+, and Opera 9) to add the content. However, my attempts at this method have not been successful to date.

Wednesday, June 28, 2006

Help with dynamic inline SVG

Normally, I like to post solutions to problems on this blog, but today I'm hoping someone can help me.

In a previous post I presented some methods to embed SVG inline after generating it from XML data. However, there's still a problem, particularly in Internet Explorer, which I discovered when I tried to add some event handling to the SVG. Let me walk through the problem.

First, we start out with some sample XML data like:

<?xml version="1.0" ?>
<SrmData>
<data>
<display_name>srmdb2</display_name>
<date>2006-04-30</date>
<processor_busy>19.04</processor_busy>
</data>
<data>
<display_name>srmxml05</display_name>
<date>2006-04-30</date>
<processor_busy>37.68</processor_busy>
</data>
<data>
<display_name>srmdev1</display_name>
<date>2006-04-30</date>
<processor_busy>26.6</processor_busy>
</data>
<data>
<display_name>srmxml01</display_name>
<date>2006-04-30</date>
<processor_busy>42.74</processor_busy>
</data>
<data>
<display_name>srmweb2</display_name>
<date>2006-04-30</date>
<processor_busy>1.66</processor_busy>
</data>
<data>
<display_name>srmweb1</display_name>
<date>2006-04-30</date>
<processor_busy>5.61</processor_busy>
</data>
<data>
<display_name>srmxml02</display_name>
<date>2006-04-30</date>
<processor_busy>22.74</processor_busy>
</data>
</SrmData>
Which I run through my XSLT and get the following SVG (notice the mouseover events):
<svg:svg baseProfile="full" width="850" height="350" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg:g>
<!--Now Draw the main X and Y axis-->
<svg:g style="stroke-width:3; stroke:black">
<!--X Axis-->
<svg:path d="M 30 330 L 830 330 Z" />
<!--Y Axis-->
<svg:path d="M 30 30 L 30 330 Z" />
</svg:g>
<svg:g style="fill:none; stroke:#B0B0B0; stroke-width:2; stroke-dasharray:2 4;text-anchor:end; font-size:12px; stroke-width:5; stroke:black ">
<!--Add dotted line at 50-->
<svg:path d="M 30 30 L 830 30 Z" />
<svg:path d="M 30 165 L 830165 Z" />
<!--Add Y-axis labels-->
<svg:text style="fill:black; stroke:none" x="20" y="30">
100 </svg:text>
<svg:text style="fill:black; stroke:none" x="20" y="165">
50 </svg:text>
<svg:text style="fill:black; stroke:none" x="20" y="330">
0 </svg:text>
</svg:g>
<svg:rect x="35" y="272.88" height="57.12" width="109" style="fill:rgb(49,0,0)">
<set attributeName="fill" from="rgb(49,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="35" y="347" rotate="45" font-size="8">
srmdb2</svg:text>
</svg:g>
<svg:rect x="149" y="216.96" height="113.03999999999999" width="109" style="fill:rgb(96,0,0)">
<set attributeName="fill" from="rgb(96,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="149" y="347" rotate="45" font-size="8">
srmxml05</svg:text>
</svg:g>
<svg:rect x="263" y="250.2" height="79.80000000000001" width="109" style="fill:rgb(68,0,0)">
<set attributeName="fill" from="rgb(68,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="263" y="347" rotate="45" font-size="8">
srmdev1</svg:text>
</svg:g>
<svg:rect x="377" y="201.78" height="128.22" width="109" style="fill:rgb(109,0,0)">
<set attributeName="fill" from="rgb(109,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="377" y="347" rotate="45" font-size="8">
srmxml01</svg:text>
</svg:g>
<svg:rect x="491" y="325.02" height="4.9799999999999995" width="109" style="fill:rgb(4,0,0)">
<set attributeName="fill" from="rgb(4,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="491" y="347" rotate="45" font-size="8">
srmweb2</svg:text>
</svg:g>
<svg:rect x="605" y="313.17" height="16.830000000000002" width="109" style="fill:rgb(14,0,0)">
<set attributeName="fill" from="rgb(14,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="605" y="347" rotate="45" font-size="8">
srmweb1</svg:text>
</svg:g>
<svg:rect x="719" y="261.78" height="68.22" width="109" style="fill:rgb(58,0,0)">
<set attributeName="fill" from="rgb(58,0,0)" to="red" begin="mouseover" end="mouseout" />
</svg:rect>
<svg:g>
<svg:text style="fill:black; stroke:none" x="719" y="347" rotate="45" font-size="8">
srmxml02</svg:text>
</svg:g>
</svg:g>
</svg:svg>
If I embed the above SVG from a static file (via <embed src="myfile.svg">), it is rendered correctly in IE as:
where in the above snapshot, the mouse is over the first bar so the mouseover event has changed it to bright red to highlight it.

The problem is that when it is rendered dynmically inline in Internet Explorer, as described in my previous post, it looks like:

where there is no coloring and no mouseover events. Both cases require the Adobe SVG Viewer to be installed to render it. So why is it rendered differently?

There is one interesting difference in how IE handles this. When using the embed tag, if you right click on the image, the pop-up context menu will look like this:

where it is obviously using the Adobe SVG Viewer as evidenced by the "About Adobe SVG Viewer..." menu option and other options such as copying, viewing, and saving the SVG. On the other hand, when using the inlined SVG method, the context menu will look like:

as it would on any other part of the web page. So even though the Adobe plugin rendered the image, it is not apparent.

Meanwhile, on the Firefox side of things, regardless of whether you embed a static SVG file, put the SVG directly inline (if you're using XHTML), or put it in an object tag, it always looks the same:
which is good except for the fact that the Firefox (Gecko) SVG rendering engine doesn't seem to support the mouseover events either.

As an alternative method, I tried PlotKit which can generate dynamic SVG graphs and charts. So, I built an HTML table from my XML data and told PlotKit to use that and in both IE and Firefox it worked correctly to give me a graph such as:
The context menu of this graph is like my inline approach and doesn't acknowledge the Adobe SVG Viewer, however, PlotKit was able to get coloring to work. The current version of PlotKit does not support events which made it both not acceptable for my overall solution as well as provided me no way to test if the mouseover would have worked with the approach used by PlotKit. However, since at least colors worked, I started investigating how he (Alastair Tse) was able to render the dynamic inline SVG in IE.

I found this note in one of his samples:
Note: With SVG, the most compatible way is NOT to put in SVG tags, but create the SVG element with Javascript. Therefore, instead of adding <svg> tag, we just have a <div> container and create the SVG element using SVGRenderer.SVG().

And looking at the PlotKit code I can see that his library is dynamically building the SVG using JavaScript DOM APIs to add each element.

Well, since I want to use XSLT to generate my SVG from XML and not write my own version of the PlotKit library to build my SVGs, I tried a hybrid approach. I used the DOM APIs to create a temporary DOM element. I assigned my generated SVG to that element's innerHTML in order to have the browser build a DOM tree out of my SVG. I then created an svg DOM element, and appended the first child DOM element of my SVG DOM (the first g element) to the svg DOM element and appened the svg DOM element to the document. Which I would think would be theoretically the same thing, but to no avail.


Does anyone out there know how to fix this? How can I make Internet Explorer render my dynamic inline SVG correctly in order to get proper coloring and event handling?

Update: See follow-up

Thursday, June 01, 2006

SVG in IE and Firefox

As follow-on work to my previous post Inline dynamic SVG from XML with Ajax where I discussed how to incorporate dynamically generated SVG in your web page, I've been trying to figure out how to make IE correctly prompt you to install the SVG plugin. That is usually accomplished with the PLUGINSPAGE attribute of the embed tag (see http://developer.apple.com/quicktime/compatibility.html for a nice explanation of the technique), but I'm using the object tag (which is recommended since it complies with the W3C HTML specifications while embed does not and it allows the data to be added inline). Unfortunately, adding the codebase attribute of http://www.adobe.com/svg/viewer/install/ (the Adobe SVG Viewer install site) and the classid attribute of clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2 (the Adobe SVG Viewer ActiveX control id) didn't do what I had hoped for.

I haven't figured out how to make IE prompt for the plugin yet, but I did find this page which provides an alternative method to check for IE and alternate using the object or the embed (in his case or inline SVG in my example) rather than using the isASVInstalled() method I used.

In his example, Spartanicus uses the MS "uplevel revealed" conditional comment to define a CSS type as:


.svg{display:none}
*>.svg{display:inline}


Then his HTML looks like:


<!--[if IE]><embed src="img/butterfly_vector.svg" height="120" width="170"><![endif]-->
<object data="img/butterfly_vector.svg" type="image/svg+xml" class="svg" height="120" width="170"/>


Where the class of svg in the object version will causes it not to display if not in Internet Explorer.

It's an interesting technique, but in my case of inline generated SVG where I can't use the embed tag since I have no src so I'd have to insert the SVG inline for IE and then also have the wrapped object version. Unfortunately, the non-object version would still display the data in the SVG XML even though it would not be an SVG graphic which would look terrible. So, it doesn't really help me in this case, but I thought it was interesting.

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!