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.

8 comments:

Anonymous said...

I am working on something similar: I'm working only in firefox, and using javascript to write an object tag with inline SVG.

But I'm running into a problem where, if I change the SVG, then refresh the browser, the change doesn't take effect (?). If I open the document in a new window/tab I see the change.

Any idea what causes this & how to solve it? I notice you have a function called clearPage() that runs for firefox - what exactly does that do?

Steven Pothoven said...

See update to posting above.

Anonymous said...

All your articles about SVG -XSLT are very instesting!! :).
I'm new in web development and i can't undertand what is or where SVG var come from. Example:

SVG.prototype.render = function() ....


What i have to include or create to use that function?

Steven Pothoven said...

In that case, SVG is just the name of the class (in object-oriented terms) for which the render function (or method) is being defined. It can be declared a couple different ways in JavaScript. For example:

function SVG() {
// this acts as the SVG constructor
// put class initialization code here
};

or

var SVG = {};

Anonymous said...

ok :). This class is provided for any js or it's yours?? :)

Thanks for your quickly answer.

Steven Pothoven said...

The SVG class is just something I made for this situation.

Unknown said...

Sorry about the six-month delay, but here are some things I've come across that might help you here.

First, regarding committing the changes, that happens a lot with event-triggered functions that alter the DOM; I've found that a setTimeout("functionCall(args)", 0) works like a charm. Don't quite know why, though.

Second, regarding the XSLT in your update, you should be able to add that via literal result elements, like so:
<xsl:template match="svg">
<script type="text/ecmascript"><![CDATA[
...
]]></script>
</xsl:template>

The trick to that is adding cdata-section-elements="script" to your <xsl:output/> element up top; that'll wrap the <script> content in fail-safe CDATA tags. Otherwise you're just telling the XSLT processor to ignore XML entities within the stylesheet itself.



Thanks for the info! It's really cool to see how SVG is building itself up, and how to take control of it. My results so far have been underwhelming, but is it ever cool to see it turning from regular text into beautiful vector graphics. Cheers!

Unknown said...

w3schools : http://www.w3schools.com/svg/svg_inhtml.asp says that object tag does not allow scripting ?!