Ever needed to display charts or other vector graphics in your browser-based application? Which technologies did you evaluate and what did you finally go for? Well, an obvious choice would be a Flash-based solution, but Flash is - even though widely spread - still a proprietary plug-in, and there are alternatives...
Freitag, 2. Mai 2008
Interactive Cross-Browser Vector Graphics on Top of SVG, VML, Canvas, Silverlight using Dojo's GFX
As far as I know, there are the following options to get native vector graphics support from the browsers:
- Scalable Vector Graphics (SVG)
- Vector Markup Language (VML)
- HTML 5 Canvas-Tag
- Pixel emulation with <div> elements like Walter Zorn's jsGraphics
The table below shows what works in recent versions of popular browsers.
| VML | SVG | Canvas | div-Pixels | |
| Firefox | - | + | + | + |
| IE | + | - | - | + |
| Safari | - | + | + | + |
| Opera | - | + | + | + |
| Chrome | - | + | + | + |
| All | - | - | - | + |
And this is where frustration kicks in. The only standard IE supports is the ten years old VML specification. SVG is also from the late 1990s and it still didn't find its way into Microsoft's browsers. HTML 5 is still a draft and whether I live to see it implemented in IE is questionable.
The only cross-browser technique available seems to be the pixel emulation method. It actually works pretty well, if you use Walter Zorn's library, but it naturally has a lot of limitations. Just imagine the number of div-elements you need to display a single circle, even considering that neighbouring pixels with the same X or Y coordinate can be represented with one div-tag. This is possibly the reason why jsGraphics doesn't have any support for curves. Also, if you need interaction and hence events and transformations you'll have to implement that yourself if you use jsGraphics. Sure, it's possible, but it means a lot of work and it will probably be difficult to use. This restriction is partially also true for the canvas-element: Events are not supported.
Now, if you want cross-browser vector graphics, you need a software layer, which provides a unique API for the different graphics implementations. E.g. something that uses SVG as a default and gracefully falls back to VML if SVG support is not present. Luckily exactly this exists: The dojo gfx extension supports SVG, VML, Canvas and Silverlight. Btw., I didn't mention Microsoft's Silverlight because like Flash it's a browser plug-in. Talking about plug-ins, Adobe's SVG Viewer is a good way to get SVG support in IE.
Basic Example
But let's have a closer look at how we actually draw in the browser window. Out of sheer laziness I'm not going to write a tutorial, but the code snippets below should be plenty to get you started with the actual drawing, transformations and events. Because Ext is the widget set of my choice I wrapped gfx a little and provided some Ext-style event management. Please note, that none of the code presented here is ready for a production system, however, if you can use any of it, you're welcome to help yourself, just leave a comment. The examples on this page will only work with recent browsers and I most certainly didn't check the all. So forgive me if something goes wrong. Ok, here is a first simple example:
Yeah, I know it's pretty basic. But notice the 50% opacity of the fill colour, move your mouse cursor into a shape and watch the colour of the outer lines, and try to drag the shapes around... Well, are you with me, when I say that's quite amazing? Now look at how little code is needed:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<style type="text/css">
</style>
</head>
<body>
<script type="text/javascript" src="lib/ext-2.1/ext-custom.js"></script>
<script type="text/javascript" src="lib/dojo-1.1.0/dojo/dojo.js"></script>
<script type="text/javascript" src="lib/naxos/naxos-graphics.js"></script>
<script type="text/javascript">
Ext.onReady(function() {
var canvas = new Naxos.graphics.Canvas({ });
var panel = new Ext.Panel({ layout: 'fit', items: [ canvas ], renderTo: 'sample', height: 200});
var g = canvas.getGraphics();
var onMouseOver = function(evt, shape) { shape.setStroke({color:"red",width:5}) };
var onMouseOut = function(evt, shape) { shape.setStroke({color:"green",width:5}) };
var circle = g.createCircle({ cx: 80, cy: 80, r: 70})
.setStroke({color:"green",width:5}).setFill("rgba(0,0,255,0.5)");
var rect = g.createRect({ x: 80, y: 80, width: 300, height: 100})
.setStroke({color:"green",width:5}).setFill("rgba(255, 255, 0, 0.5)");
circle.addListener("mouseover", onMouseOver);
circle.addListener("mouseout", onMouseOut);
rect.addListener("mouseover", onMouseOver);
rect.addListener("mouseout", onMouseOut);
g.makeMoveable(circle);
g.makeMoveable(rect);
});
</script>
<div id="sample" style="border:1px solid gray;"></div>
</body>
</html>
I successfully tested the example above with Firefox 2 and Safari 3.1 on Mac OS 10.5 and with Firefox 2, Firefox 3 beta 5, Safari 3.1.1, IE 6 and IE 7 on Windows 2000 (except IE 7) and Windows XP. Update: The example also works with Firefox 3 (final) under Windows 2000, Windows XP and Mac OS 10.5. Update Sep. 2008: Example works fine in Google Chrome 0.2 under Win XP
ScatterBox
Now, let's take things a bit further. Below is what you'll hopefully see as a pile of half decently rendered polaroid photos (If not, you're probably using Internet Explorer). If you click on a photo it will move to the front and rotate in horizontal position. You can also drag the photos with the mouse. This example may well serve as a test case for the vector graphics support of the different browsers. It features more advanced event handling, grouping, the use of images and transformations.
The mark-up is pretty simple:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<style type="text/css">
</style>
</head>
<body>
<script type="text/javascript" src="lib/ext-2.1/ext-custom.js"></script>
<script type="text/javascript" src="lib/dojo-1.1.0/dojo/dojo.js"></script>
<script type="text/javascript" src="lib/naxos/naxos-graphics.js"></script>
<script type="text/javascript" src="js/ScatterBox.js"></script>
<script type="text/javascript">
Ext.onReady(function() {
var canvas = new Naxos.graphics.Canvas({ });
var panel = new Ext.Panel({ layout: 'fit', items: [ canvas ], renderTo: 'scatterbox', height: 400 });
var g = canvas.getGraphics();
var images = [
{ url: "resources/images/castle.jpg", title: "Ancient Sirmione", width: 300, height: 225 },
{ url: "resources/images/mountains.jpg", title: "Somewhere in the Alps", width: 300, height: 225 },
{ url: "resources/images/frosty.jpg", title: "Frosty Night", width: 300, height: 225 },
{ url: "resources/images/hever.jpg", title: "Hever Castle", width: 300, height: 225 },
{ url: "resources/images/danesfield.jpg", title: "Danesfield House", width: 300, height: 225 },
{ url: "resources/images/sunset.jpg", title: "Greek Sunset", width: 300, height: 225 },
{ url: "resources/images/entrance.jpg", title: "Various Curves", width: 300, height: 225 },
{ url: "resources/images/lake.jpg", title: "Lago di Garda", width: 300, height: 225 }
];
var scatterBox = new Naxos.ScatterBox({ canvas: canvas, images: images });
scatterBox.scatter();
});
</script>
<div id="scatterbox" style="border:1px solid gray;"></div>
</body>
</html>
All drawing and event handling is done in the JavaScript class ScatterBox:
Naxos.ScatterBox = function(config) {
Ext.apply(this, config);
// config must contain canvas and images
var imgW = 300;
var imgH = 225;
if(this.images && this.images.length > 0) {
imgW = this.images[0].width;
imgH = this.images[0].height;
}
this.gx = this.canvas.getGraphics();
this.center = { x: this.canvas.getSize().width / 2, y: this.canvas.getSize().height / 2 };
this.rx = this.canvas.getSize().width / 2 - (imgW + 20) / 2;
this.ry = this.canvas.getSize().height / 2 - (imgH + 70) / 2;
};
Naxos.ScatterBox.prototype = {
// config parameters
images: [], // [{ url: 'xxx', title: 'xxx', width: xxx, height: xxx }, ... ]
canvas: null,
// public properties
polaroids: [],
inFront: null,
gx: null,
center: { x: 300, y: 200 },
rx: 150, ry: 100,
/**
* Create the polaroids from the images and scatter them randomly on the canvas
*/
scatter: function() {
var create = this.polaroids.length < this.images.length;
for(var i = 0; i < this.images.length; i++) {
var image = this.images[i];
// set up random angle and position
var angle = Math.random() * 60 - 30; // -30 .. 30 degrees
var w = image.width;
var h = image.height;
var x = this.center.x + Math.random() * this.rx * 2 - this.rx - w / 2;
var y = this.center.y + Math.random() * this.ry * 2 - this.ry - h / 2;
var polaroid = null;
if(create) {
// create the polaroid
polaroid = this._createPolaroid(x, y, w, h, angle, image.url, image.title);
// use mouse down and up instead of click event, because
// click also fires when an object was dragged
polaroid.addListener("mousedown", this.onMouseDown, this);
polaroid.addListener("mouseup", this.onMouseUp, this);
// make the photo movable and register a listener (which isn't used)
this.gx.makeMoveable(polaroid).addListener("movestop", function(source, polaroid, matrix) {
}, this);
// store the photo for later access
this.polaroids.push(polaroid);
} else {
// restore original positions
polaroid = this.polaroids[i];
polaroid.setTransform(dojox.gfx.matrix.rotategAt(polaroid.angle, polaroid.center.x, polaroid.center.y));
}
}
// no particular picture in the front, yet
this.inFront = null;
},
/**
* Create the polaroid from the image. Note that this function is far from perfect.
* E.g. the support for different / variable image sizes is poor.
* @param x The x-coordinate of the image (not the white frame) within the canvas
* @param y The y-coordinate of the image
* @param w The width of the image
* @param h The width of the image
* @param angle The angle of the polaroid
* @param url The URL of the image
* @param desc The title of the image
* @return a group with the elements of the polaroid
*/
_createPolaroid: function(x, y, w, h, angle, url, desc) {
// create the group
var polaroid = this.gx.createGroup();
// draw shadow, frame, the image with a thin border and the title
var shadow = this.gx
.createRect({ x: x - 5, y: y - 5, width: w + 20, height: h + 70 })
.setFill([0,0,0,.1]);
var frame = this.gx
.createRect({ x: x - 10, y: y - 10, width: w + 20, height: h + 70 })
.setStroke({color:"rgba(0,0,0,.2)", width: 1})
.setFill("white");
var img = this.gx
.createImage({x: x, y: y, width: w, height: h, src: url});
var edge = this.gx
.createRect({ x: x, y: y, width: w, height: h })
.setStroke({color:"black", width: 1});
var title = this.gx
.createText({x: x + w / 2, y: (y - 10) + (h + 70) - 25,text:desc,align:"middle",kerning:true})
.setFont({family: "Helvetica", style:"italic", size: "16px"})
//.setStroke("white")
.setFill("black");
// workaround for safari image rendering bug
setTimeout(function() {
// redraw the image after some time
img.applyTransform({xx: 1, xy: 0, yx: 0, yy: 1, dx: 0, dy: 0});
}, 150);
// assemble everything in the group
polaroid.add(shadow);
polaroid.add(frame);
polaroid.add(img);
polaroid.add(edge);
polaroid.add(title);
// store center coordinates and angle
polaroid.center = this._getCenter(x, y, w, h);
polaroid.angle = angle;
// position and rotate the polaroid
polaroid.setTransform(dojox.gfx.matrix.rotategAt(polaroid.angle, polaroid.center.x, polaroid.center.y));
return(polaroid);
},
/**
* An ugly funtion to determine the center of the polaroid from the image coordinates and size
* @param x The x-coordinate of the image (not the white frame) within the canvas
* @param y The y-coordinate of the image
* @param w The width of the image
* @param h The width of the image
* @return an object with the x- and y-coordinates of the center
*/
_getCenter: function(x, y, w, h) {
return({ x: x - 10 + (w + 20) / 2, y: y - 10 + (h + 70) / 2});
},
/**
* On the mouse down event store the current x/y-offset of the polaroid
*/
onMouseDown: function(evt, polaroid) {
polaroid._onDownOffset = { dx: polaroid.matrix.dx, dy: polaroid.matrix.dy };
},
/**
* On the mouse up event call the moveToFront function, if the polaroid wasn't dragged
*/
onMouseUp: function(evt, polaroid) {
if(polaroid._onDownOffset && polaroid._onDownOffset.dx == polaroid.matrix.dx && polaroid._onDownOffset.dy == polaroid.matrix.dy) {
this.moveToFront(polaroid);
}
},
/**
* Move a polaroid to the front and rotate in a horizontal position.
*/
moveToFront: function(polaroid) {
if(this.inFront == null || this.inFront != polaroid) {
polaroid.moveToFront();
polaroid.applyTransform(dojox.gfx.matrix.rotategAt(-polaroid.angle, polaroid.center.x, polaroid.center.y));
// If there was a polaroid in the fron rotate by its original angle
if(this.inFront != null) {
this.inFront.applyTransform(dojox.gfx.matrix.rotategAt(this.inFront.angle, this.inFront.center.x, this.inFront.center.y));
}
// Remember this polaroid is in the front
this.inFront = polaroid;
}
}
}
Well, after the good experience with the basic example this is a bit disappointing:
- IE 6 and IE 7 show misplaced images and act extremly slow. The example is not usable.
- FF 2 and FF 3 beta 5 render alright on Windows 2K and XP but are a bit unresponsive. Text is displayed quite poorly.
- FF 2 on Mac OS (FF 3 not tested) doesn't display the text
- Safari 3.1 works very well on both Windows XP and Mac OS. Very responsive, neat antialiasing. Only rotated text could stick tighter to the baseline.
- Update: FF 3 (final) on Mac OS renders very nicely including the text and is quite responsive
- Update: FF 3 (final) on Win 2k and XP is the same as FF 3 beta 5
- Update Sep. 2008: Google Chrome 0.2 on Win XP renders ok and is as responsive as Safari. However, the poor quality of text rendering indicates a relationship to FF's SVG rendering engine
- Update 2009: FF 3.5 does not display the images anymore

- Update 2009: Safari 4 works perfectly
Conclusion
Nothing is perfect (yet). Using gfx, simple vector graphics can be easily displayed in the recent versions of the major browsers without any noticable compatibility issues. Interactivity in more complex scenarios might be a problem, most certainly with IE using VML (might be better using Silverlight or Adobe's SVG plug-in) and potentially with Firefox. Text rendering is not always optimal and image transformation problematic in IE/VML (maybe this could be solved in gfx). Nevertheless, gfx is a huge step in the right direction, it's easy and fun to use with often very good results.
Links
You might also want to take a look at
- Drawing in JavaScript - new Canvas Tag - client side Chart building - IE implementation - SVG competitor
- Canvas in IE
- Speculating on the Future of HTML Canvas
- VML, SVG and Canvas
- Ajax/Javascript: 8 Ways to Create Graphics on the Fly
- Google, Silverlight and Apollo
- Why Javascript, not Flash? - Ask Zoho
Wicket meets Javascript
Generierung von Offline Webapplikationen mit HTML 5 / Beispiel auf dem iPhone
Clientseitige Datenspeicherung im Safari mit Javascript und SQLite
Track'n'Mash: Geolocation mit dem Safari auf dem iPhone mit OS 3.0
Alle Jahre wieder - Berechnung beweglicher Feiertage


A quick mention of raphael to add to the good information above. www.raphaeljs.org
It's raphaeljs.com rather than raphaeljs.org
If I find some time, I try porting the examples above.