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!

Blog renamed

I decided to rename my blog. Until now, it had been entitled Pothoven's Pansophy

When I originally named it, I picked pansophy because I wanted a word for knowledge or wisdom that started with 'P' for some alliteration with my name. So, looking up knowledge or wisdom in a thesaurus I found:

pansophy
\Pan"so*phy\, n. [Pan- + Gr. ? wisdom, ? wise: cf. F. pansophie.] Universal wisdom; esp., a system of universal knowledge proposed by Comenius (1592 -- 1671), a Moravian educator.

However, every time I look at it, it looks too much like pansy. So, I'm renaming the blog to Pothoven Post to sound more like a newspaper, but still with the alliteration.

Thursday, April 20, 2006

Ajax: HTML vs XML responses

A friend of mine has a discussion on his blog regarding returning straight HTML or XML in your AJAX responses (see What type of data should an Ajax call return?)

I voiced a comment regarding my appreciation for the direction used by Rico where XML is always returned, though it may contain an HTML snippet such as:


<ajax-response>
<response type="element" id="personInfo">
[valid XHTML here]
</response>
</ajax-response>


where the type="element" id="personInfo" indicates to Rico to replace the innerHTML of the element with id personInfo with the contents of the response.

Or alternatively:


<ajax-response>
<response type="object" id="formLetterUpdater">
[valid XML data here]
</response>
</ajax-response>


The type="object" id="formLetterUpdater" calls a JavaScript object registered under the name matching the id of formLetterUpdater. That Javascript would then process the contents of the response as XML to extract whatever data is necessary.

For either mechanism, multiple "response" clauses can be returned in a single "ajax-response" allowing multiple sections of a web page to be updated from a single AJAX action if necessary.

My apprehension about this method is that the server side processing has too much knowledge about how the data will be used (by dictating the ids). Of course, that might be able to be handled by having the AJAX response send which ids it wants data for.

Of these two mechanisms from Rico, I think the XML data mechanism has the most flexibility, as it describes the data in XML and the UI can extract what it needs and format it correctly whether by coding manual JavaScript DOM processing as indicated with the formLetterUpdater above, or by utilizing XSLT on the browser to format it as desired. Using XSLT provides multiple advantages for formatting and re-formatting the data for the user without the need to communicate with the server over and over to format the HTML. For example, switching themes, languages, or switching from a table to a graph.

Tuesday, April 18, 2006

Wrapping long strings with XSL 1.0

For my recent work with XML and XSL, I needed to wrap strings when generating a text report. I found examples online to do it, but they depended on XSL 2.0 functions such as the string tokenizer. I needed a way to do it with plain old XSL 1.0, so the solution I came up with is below. This solution is a bit more complex than necessary since I needed the ability to add markup around the wrapped lines (the prefix and suffix). Additionally, I needed the ability to add different markup before and after wrapped lines as well (the hangingPrefix and hangingSuffix). I could have taken it out to simplify this example, however, I thought some people might find it useful. Additionally, it's designed for all the prefixes and suffixes to be optional anyway. Here's my solution (the second template called splitString is the entry point):


<!-- recursive section of splitString... don't call directly -->
<xsl:template name="firstString">
<xsl:param name="string" />
<xsl:param name="length" />
<xsl:param name="currentpos" />
<xsl:param name="prefix" />
<xsl:param name="hangingPrefix" />
<xsl:param name="suffix" />
<xsl:param name="hangingSuffix" />
<xsl:param name="padding" />

<xsl:choose>
<xsl:when test="substring($string, $currentpos, 1) = ' '">
<xsl:value-of select="substring($string, 1, $currentpos - 1)" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="$newline" />

<xsl:call-template name="splitString">
<xsl:with-param name="string" select="substring($string, $currentpos + 1)" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="prefix" select="$hangingPrefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$hangingSuffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:when>
<xsl:when test="substring($string, $currentpos, 1) = '/'">
<xsl:value-of select="substring($string, 1, $currentpos)" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="$newline" />

<xsl:call-template name="splitString">
<xsl:with-param name="string" select="substring($string, $currentpos + 1)" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="prefix" select="$hangingPrefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$hangingSuffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="firstString">
<xsl:with-param name="string" select="$string" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="currentpos" select="$currentpos - 1" />
<xsl:with-param name="prefix" select="$prefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$suffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<!-- split a string to a specified size.
Note: there is no newline at the end of the last line to allow additions to the end of the line

string: string to split
length: max length string can be per line
prefix: optional string to prepend to the first line
hangingPrefix: optional string to prepend subsequent lines with
suffix: optional string to append to the end of the first line
hangingSuffix: optional string to append to subsequent lines
padding: optional string to pad the last line of a splt string with to allow extra text to be added after it
-->
<xsl:template name="splitString">
<xsl:param name="string" />
<xsl:param name="length" select="$maxLineLength" />
<xsl:param name="prefix" />
<xsl:param name="hangingPrefix" select="$prefix" />
<xsl:param name="suffix" />
<xsl:param name="hangingSuffix" select="$suffix" />
<xsl:param name="padding" />

<xsl:choose>
<xsl:when test="(string-length($prefix) +
string-length($string) +
string-length($suffix)) > $length">
<xsl:variable name="currentpos" select="$length - string-length($suffix)" />
<xsl:choose>
<xsl:when test="contains(substring($string, 1, $currentpos), ' ') or
contains(substring($string, 1, $currentpos), '/')">
<xsl:call-template name="firstString">
<xsl:with-param name="string" select="concat($prefix, $string)" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="currentpos" select="$currentpos" />
<xsl:with-param name="prefix" select="$prefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$suffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- no line split character within maximum line length -->
<xsl:value-of select="$prefix" />
<xsl:value-of select="substring($string, 1, $currentpos - string-length($prefix))" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="$newline" />

<xsl:call-template name="splitString">
<xsl:with-param name="string" select="substring($string, $currentpos + 1 - string-length($prefix))" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="prefix" select="$hangingPrefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$hangingSuffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>

<!-- less than $length chars left, just write them -->
<xsl:otherwise>
<xsl:value-of select="$prefix" />
<xsl:value-of select="$string" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="substring($padding, 1, ($length - string-length($prefix) - string-length($string) - string-length($suffix))) " />
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Sorting groups in XSL

For some of my recent work I had to generate reports from XML using XSL. One of the techniques I had to use frequently was grouping data using the Muenchian Method.

The referenced web page shows an example of getting a group of contacts and sorting it by surname. That's all well and good if your only computing the group once and processing it straight-forward as shown. But what if the XML data is large so calculating the group is costly (in terms of memory and CPU) and you need to use it multiple times and the order is important because you're using it within the context of processing some larger processing loop (ex. 10 at a time associated with another value)?

For an example, let's just say you need to generate a report that has a cover sheet listing all the unique surnames in your file, then separate statistics for each surname such as how many people share that surname. This is a very contrived example, but at least it will demonstrate what I mean.

Using the example in the referenced web page, we could build a list of unique sorted surnames to use in various places in our XSL via:


<xsl:key name="contacts-by-surname" match="contact" use="surname"/>
<xsl:variable name="surnames" select="contact[count(. | key('contacts-by-surname', surname)[1]) = 1]/surname"/>

<xsl:template match="/">
<xsl:call-template name="sortList">
<xsl:with-param name="list" select="$surnames"/>
</xsl:call-template>

<!-- list is now sorted to use whenever necessary -->
<xsl:for-each select="$surnames">
[process sorted surname]
</xsl:for-each>
</xsl:template>


<!-- sort list by the value of its current node -->
<xsl:template name="sortList">
<xsl:param name="list"/>
<xsl:for-each select="$list">
<xsl:sort select="." />
</xsl:for-each>
</xsl:template>

The call to sortList will sort it for all subsequent uses.

So, to see it in action, here is our sample data:

<records>
<contact id="0001">
<title>Mr</title>
<forename>John</forename>
<surname>Smith</surname>
</contact>
<contact id="0002">
<title>Dr</title>
<forename>Amy</forename>
<surname>Jones</surname>
</contact>
<contact id="0003">
<title>Mr</title>
<forename>John</forename>
<surname>Doe</surname>
</contact>
<contact id="0004">
<title>Mr</title>
<forename>Jack</forename>
<surname>Smith</surname>
</contact>
<contact id="0005">
<title>Mr</title>
<forename>Mike</forename>
<surname>Jordan</surname>
</contact>
<contact id="0006">
<title>Mr</title>
<forename>Jim</forename>
<surname>Johnson</surname>
</contact>
<contact id="0007">
<title>Mr</title>
<forename>Randy</forename>
<surname>Jones</surname>
</contact>
<contact id="0008">
<title>Mr</title>
<forename>Bobby</forename>
<surname>Jones</surname>
</contact>
<contact id="0009">
<title>Mr</title>
<forename>Glenn</forename>
<surname>Smith</surname>
</contact>
<contact id="0010">
<title>Mr</title>
<forename>Bill</forename>
<surname>McDonnald</surname>
</contact>

</records>


Here is the complete style sheet:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" omit-xml-declaration="yes" />

<xsl:variable name="newline">
<!-- Hex 10 is a line feed, and hex 13 is a carriage return -->
<xsl:value-of select="'&#13;'" />
</xsl:variable>

<!-- Remove all whitespace from the source XML so that it is not echoed into the new report -->
<xsl:strip-space elements="//*" />

<xsl:key name="contacts-by-surname" match="contact" use="surname" />
<xsl:variable name="surnames" select="//contact[count(. | key('contacts-by-surname', surname)[1]) = 1]/surname" />

<xsl:template match="/">
<xsl:text>Full list of surnames:</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="//surname">
<xsl:value-of select="." />
<xsl:value-of select="' '" />
</xsl:for-each>
<xsl:value-of select="$newline" />
<xsl:value-of select="$newline" />

<xsl:text>List of unique surnames:</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="$surnames">
<xsl:value-of select="." />
<xsl:value-of select="' '" />
</xsl:for-each>
<xsl:value-of select="$newline" />
<xsl:value-of select="$newline" />

<xsl:call-template name="sortList">
<xsl:with-param name="list" select="$surnames" />
</xsl:call-template>

<!-- list is now sorted to use whenever necessary -->
<xsl:text>List of unique surnames (sorted):</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="$surnames">
<xsl:value-of select="." />
<xsl:value-of select="' '" />
</xsl:for-each>
<xsl:value-of select="$newline" />
<xsl:value-of select="$newline" />

<xsl:text>People per surname:</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="$surnames">
<xsl:variable name="surname" select="."/>
<xsl:value-of select="$surname" />
<xsl:value-of select="' ('" />
<xsl:value-of select="count(//contact[surname = $surname])" />
<xsl:value-of select="'):'" />
<xsl:value-of select="$newline" />
<xsl:for-each select="//contact[surname = $surname]">
<xsl:sort select="forname"/>
<xsl:value-of select="' '" />
<xsl:value-of select="forename" />
<xsl:value-of select="$newline" />
</xsl:for-each>
<xsl:value-of select="$newline" />
</xsl:for-each>


</xsl:template>

<!-- sort list by the value of its current node -->
<xsl:template name="sortList">
<xsl:param name="list" />
<xsl:for-each select="$list">
<xsl:sort select="." />
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>


And here is the report:

Full list of surnames:
Smith Jones Doe Smith Jordan Johnson Jones Jones Smith McDonnald

List of unique surnames:
Smith Jones Doe Jordan Johnson McDonnald

List of unique surnames (sorted):
Doe Johnson Jones Jordan McDonnald Smith

People per surname:
Doe (1):
John

Johnson (1):
Jim

Jones (3):
Amy
Randy
Bobby

Jordan (1):
Mike

McDonnald (1):
Bill

Smith (3):
John
Jack
Glenn