Wednesday, April 9, 2008

Floating labels for Charts in Dojo (dojox.gfx)

As I've written before, I have the opportunity to work as a consultant for a startup company which *really* want to do Ajax stuff. They want things to be decoupled, load asynchronously and show up as available and also emphasize charts and other graphics quite a lot.

Naturally the discussion has veered conspicuously towards flash and flex (which I detest for religious reasons) to give pizzazz and dynamic glamour to charts and whatnot in the overall customer experience.

Digging into dojox.gfx, I made several interesting discoveries which has made me keep the hounds at bay for the time being. Not that the following things aren't part of dojo (yet), but it's fairly simple to add (sort of :).

What I wanted to have, above all, was an animated label which shows the current value the mouse is onmouseing over, so that you can have a bar chart where you drag the mouse cursor over one bar after another and a label "floats" animatedly between the bars you choose, showing the exact value, pretty much as in Google Analytics (a popular comparison).

What I ended up doing was a two-fold approach. First I had to collect the "label" strings from the charts. I started out with the "Default" or Lines chart defined in dojox.charting.plot2d.Default;

An example of what I'm talking about can be found here; http://genericwitticism.com/ld/dojo-1.1b1/widespace/test_chart.html

Inside the render function, the markers for the charts are created (If you don't use markers for the chart, no labels are shown this way. So there). And I then added some code to save away the actual 'y' value for the marker inside the object for each marker, which was then stuffed into an array for later;

//------------------------------------------- ************ (1) ************ Add label info to the lpoly array to be collected later
if(typeof run.data[0] == "number"){
lpoly = dojo.map(run.data, function(v, i){
return {
x: this._hScaler.scale * (i + 1 - this._hScaler.bounds.lower) + offsets.l,
y: dim.height - offsets.b - this._vScaler.scale * (v - this._vScaler.bounds.lower),
label: v //------------------------------------------------------------------------------------------- Foo
};
}, this);
}else{
lpoly = dojo.map(run.data, function(v, i){
return {
x: this._hScaler.scale * (v.x - this._hScaler.bounds.lower) + offsets.l,
y: dim.height - offsets.b - this._vScaler.scale * (v.y - this._vScaler.bounds.lower),
label: v //------------------------------------------------------------------------------------------- Foo
};
}, this);
}

It's a bit sloppily done, since I've replicated the code twice, but I was tired, OK. :) As you can see, I've marked the new line in the code with the "Foo" comment.

Further down in the render method, the markers get created by use of dojox.gfx, which wraps a host of usefult 2d graphics api's and detects whether to use vml (IE) or svg (Everybody else (as usual)). Since dojo.connect can be used on graphical objects as well it wasn't difficult to add a callback function for onmouseover;

if(this.opt.markers){
//------------------------------------------- ************ (2) ************ Add callbacks with the collected label info
dojo.forEach(lpoly, function(c)
{
var path = "M" + c.x + " " + c.y + " " + marker;
//---------------------------------- >
var self = this;
var label = c.label;
function foofun(e)
{
//console.log("foofun called. e = "+e+", s = "+s+", label = "+label);
self._showMarkerLabel(e, s, label);
}
//---------------------------------- <
if(outline){
s.createPath(path).setStroke(outline);

}
var path = s.createPath(path);
path.setStroke(stroke).setFill(stroke.color);
path.connect("onmouseover", foofun); //---------------------------------- Foo
}, this);

}

Of course, this leaves out the magic function self._showMarkerLabel, which actually does the job. As things would have it, most Charts (not Pie, though) extends dojox.charting.plot2d.Base, where I put the remaining function(s);

,
_group: "", // group of marker shapes
//_markerLabel: "", // The 'flashy', 'floaty' marker label which will wow the crowd. Hurrah! :)
//_markerLabelShadow: "",
_labelText: "",
_curPos: "",
_oldPos: "", // Collect old positions for label, shadow and text
_showMarkerLabel: function(evt, surface, label)
{
label = label || "Foo";

var x = evt.target.x.baseVal.value;
var y = evt.target.y.baseVal.value;
var pos = {dx: x, dy: y-20};
var spos = {dx: pos.dx+2, dy: pos.dy+2};
var txtpos = {dx: pos.dx+7, dy:pos.dy+18};
this._curPos = {pos: pos, spos: spos, txtpos: txtpos};

if(this._group == "")
{
this._group = surface.createGroup();
}

if(this._labelText)
{
this._group.remove(this._labelText);
}

var txt = {width: 100, height: 30, text: label};
this._labelText = surface.createText(txt);
this._labelText.setFont({family: "Arial", size: "9pt", weight: "normal"});
this._labelText.setFill("black");
//this._labelText.setStroke("black");
this._group.add(this._labelText);

if(this._oldPos == "")
{

this.smoothMoves(this._curPos, this._curPos); // Gratitious call to shake loose initialization bug in fx
}
else
{
this.smoothMoves(this._oldPos, this._curPos);
}
// Save old positions
this._oldPos = this._curPos;
},

smoothMoves: function(oldPos, curPos)
{
var oldpos = oldPos.pos;
var oldspos = oldPos.spos;
var oldtxtpos = oldPos.txtpos;

var pos = curPos.pos;
var spos = curPos.spos;
var txtpos = curPos.txtpos;

console.log("smoothMoves called markerlabel = "+this._markerLabel+", labelShadow = "+this._markerLabelShadow+", text = "+this._labelText);
var a1 = dojox.gfx.fx.animateTransform(
{
shape: this._group,
duration: 100,
transform:
[
{name: "translate", start: [oldtxtpos.dx, oldtxtpos.dy], end: [txtpos.dx, txtpos.dy]}
]
});
}
a1.play();

I put all this at the end of Base.js in dojox/charting/plot2d, which did the trick. I had been playing with having a rounded label with a drop shadow, but can't seem to be able to offset the drop-shadow rectangle properly. If you know how to do this (within a group) , please mail or comment!

Cheers,
PS

No comments: