blob: c6e3f384a59c869efe2f4a9f303706ca973770e9 [file] [log] [blame]
/*
Copyright (c) 2004-2006, The Dojo Foundation
All Rights Reserved.
Licensed under the Academic Free License version 2.1 or above OR the
modified BSD license. For more information on Dojo licensing, see:
http://dojotoolkit.org/community/licensing.shtml
*/
dojo.provide("dojo.widget.vml.Chart");
dojo.require("dojo.widget.HtmlWidget");
dojo.require("dojo.widget.Chart");
dojo.require("dojo.math");
dojo.require("dojo.html.layout");
dojo.require("dojo.gfx.color");
dojo.widget.defineWidget(
"dojo.widget.vml.Chart",
[dojo.widget.HtmlWidget, dojo.widget.Chart],
function(){
// summary: Creates a chart based on the passed data and plotter choice, using VML.
// description:
// Renders a basic chart set based on the chosen data source and plotter, using
// VML. Note that a lot of the public properties are not meant to be altered,
// and that some usable attributes passed with the HTML widget definition do not
// correspond to equivilent properties that are used.
this.templatePath=null;
this.templateCssPath=null;
this._isInitialize=false;
this.hasData=false;
this.vectorNode=null;
this.plotArea=null;
this.dataGroup=null;
this.axisGroup=null;
this.properties={
height:0, // defaults, will resize to the domNode.
width:0,
defaultWidth:600,
defaultHeight:400,
plotType:null,
padding:{
top:10,
bottom:2,
left:60,
right:30
},
axes:{
x:{
plotAt:0,
label:"",
unitLabel:"",
unitType:Number,
nUnitsToShow:10,
range:{
min:0,
max:200
}
},
y:{
plotAt:0,
label:"",
unitLabel:"",
unitType:Number,
nUnitsToShow:10,
range:{
min:0,
max:200
}
}
}
};
},
{
parseProperties:function(/* HTMLElement */node){
// summary
// Parse the properties off the main tag
var bRangeX=false;
var bRangeY=false;
if (node.getAttribute("width")){
this.properties.width=node.getAttribute("width");
}
if (node.getAttribute("height")){
this.properties.height=node.getAttribute("height");
}
if (node.getAttribute("plotType")){
this.properties.plotType=node.getAttribute("plotType");
}
if (node.getAttribute("padding")){
if (node.getAttribute("padding").indexOf(",") > -1)
var p=node.getAttribute("padding").split(",");
else var p=node.getAttribute("padding").split(" ");
if (p.length==1){
var pad=parseFloat(p[0]);
this.properties.padding.top=pad;
this.properties.padding.right=pad;
this.properties.padding.bottom=pad;
this.properties.padding.left=pad;
} else if(p.length==2){
var padV=parseFloat(p[0]);
var padH=parseFloat(p[1]);
this.properties.padding.top=padV;
this.properties.padding.right=padH;
this.properties.padding.bottom=padV;
this.properties.padding.left=padH;
} else if(p.length==4){
this.properties.padding.top=parseFloat(p[0]);
this.properties.padding.right=parseFloat(p[1]);
this.properties.padding.bottom=parseFloat(p[2]);
this.properties.padding.left=parseFloat(p[3]);
}
}
if (node.getAttribute("rangeX")){
var p=node.getAttribute("rangeX");
if (p.indexOf(",")>-1) p=p.split(",");
else p=p.split(" ");
this.properties.axes.x.range.min=parseFloat(p[0]);
this.properties.axes.x.range.max=parseFloat(p[1]);
bRangeX=true;
}
if (node.getAttribute("rangeY")){
var p=node.getAttribute("rangeY");
if (p.indexOf(",")>-1) p=p.split(",");
else p=p.split(" ");
this.properties.axes.y.range.min=parseFloat(p[0]);
this.properties.axes.y.range.max=parseFloat(p[1]);
bRangeY=true;
}
return { rangeX:bRangeX, rangeY:bRangeY };
},
setAxesPlot:function(/* HTMLElement */table){
// summary
// figure out where to plot the axes
if (table.getAttribute("axisAt")){
var p=table.getAttribute("axisAt");
if (p.indexOf(",")>-1) p=p.split(",");
else p=p.split(" ");
// x axis
if (!isNaN(parseFloat(p[0]))){
this.properties.axes.x.plotAt=parseFloat(p[0]);
} else if (p[0].toLowerCase()=="ymin"){
this.properties.axes.x.plotAt=this.properties.axes.y.range.min;
} else if (p[0].toLowerCase()=="ymax"){
this.properties.axes.x.plotAt=this.properties.axes.y.range.max;
}
// y axis
if (!isNaN(parseFloat(p[1]))){
this.properties.axes.y.plotAt=parseFloat(p[1]);
} else if (p[1].toLowerCase()=="xmin"){
this.properties.axes.y.plotAt=this.properties.axes.x.range.min;
} else if (p[1].toLowerCase()=="xmax"){
this.properties.axes.y.plotAt=this.properties.axes.x.range.max;
}
} else {
this.properties.axes.x.plotAt=this.properties.axes.y.range.min;
this.properties.axes.y.plotAt=this.properties.axes.x.range.min;
}
},
drawVectorNode:function(){
// summary
// Draws the main canvas for the chart
if(this.vectorNode){ this.destroy(); }
this.vectorNode=document.createElement("div");
this.vectorNode.style.width=this.properties.width+"px";
this.vectorNode.style.height=this.properties.height+"px";
this.vectorNode.style.position="relative";
this.domNode.appendChild(this.vectorNode);
},
drawPlotArea:function(){
// summary
// Draws the plot area for the chart
var plotWidth=this.properties.width-this.properties.padding.left-this.properties.padding.right;
var plotHeight=this.properties.height-this.properties.padding.top-this.properties.padding.bottom;
if(this.plotArea){
this.plotArea.parentNode.removeChild(this.plotArea);
this.plotArea=null;
}
this.plotArea=document.createElement("div");
this.plotArea.style.position="absolute";
this.plotArea.style.backgroundColor="#fff";
this.plotArea.style.top=(this.properties.padding.top)-2+"px";
this.plotArea.style.left=(this.properties.padding.left-1)+"px";
this.plotArea.style.width=plotWidth+"px";
this.plotArea.style.height=plotHeight+"px";
this.plotArea.style.clip="rect(0 "+plotWidth+" "+plotHeight+" 0)";
this.vectorNode.appendChild(this.plotArea);
},
drawDataGroup:function(){
// summary
// Draws the data group for the chart
var plotWidth=this.properties.width-this.properties.padding.left-this.properties.padding.right;
var plotHeight=this.properties.height-this.properties.padding.top-this.properties.padding.bottom;
if(this.dataGroup){
this.dataGroup.parentNode.removeChild(this.dataGroup);
this.dataGroup=null;
}
this.dataGroup=document.createElement("div");
this.dataGroup.style.position="absolute";
this.dataGroup.setAttribute("title", "Data Group");
this.dataGroup.style.top="0px";
this.dataGroup.style.left="0px";
this.dataGroup.style.width=plotWidth+"px";
this.dataGroup.style.height=plotHeight+"px";
this.plotArea.appendChild(this.dataGroup);
},
drawAxes:function(){
// summary
// Draws the axes for the chart
var plotWidth=this.properties.width-this.properties.padding.left-this.properties.padding.right;
var plotHeight=this.properties.height-this.properties.padding.top-this.properties.padding.bottom;
if(this.axisGroup){
this.axisGroup.parentNode.removeChild(this.axisGroup);
this.axisGroup=null;
}
this.axisGroup=document.createElement("div");
this.axisGroup.style.position="absolute";
this.axisGroup.setAttribute("title", "Axis Group");
this.axisGroup.style.top="0px";
this.axisGroup.style.left="0px";
this.axisGroup.style.width=plotWidth+"px";
this.axisGroup.style.height=plotHeight+"px";
this.plotArea.appendChild(this.axisGroup);
var stroke=1;
// x axis
var line=document.createElement("v:line");
var y=dojo.widget.vml.Chart.Plotter.getY(this.properties.axes.x.plotAt, this);
line.setAttribute("from", "0px,"+y+"px");
line.setAttribute("to", plotWidth+"px,"+y+"px");
line.style.position="absolute";
line.style.top="0px";
line.style.left="0px";
line.style.antialias="false";
line.setAttribute("strokecolor", "#666");
line.setAttribute("strokeweight", stroke*2+"px");
this.axisGroup.appendChild(line);
// y axis
var line=document.createElement("v:line");
var x=dojo.widget.vml.Chart.Plotter.getX(this.properties.axes.y.plotAt, this);
line.setAttribute("from", x+"px,0px");
line.setAttribute("to", x+"px,"+plotHeight+"px");
line.style.position="absolute";
line.style.top="0px";
line.style.left="0px";
line.style.antialias="false";
line.setAttribute("strokecolor", "#666");
line.setAttribute("strokeweight", stroke*2+"px");
this.axisGroup.appendChild(line);
// labels
var size=10;
// x axis labels.
var t=document.createElement("div");
t.style.position="absolute";
t.style.top=(this.properties.height-this.properties.padding.bottom)+"px";
t.style.left=this.properties.padding.left+"px";
t.style.fontFamily="sans-serif";
t.style.fontSize=size+"px";
t.innerHTML=dojo.math.round(parseFloat(this.properties.axes.x.range.min),2);
this.vectorNode.appendChild(t);
t=document.createElement("div");
t.style.position="absolute";
t.style.top=(this.properties.height-this.properties.padding.bottom)+"px";
t.style.left=(this.properties.width-this.properties.padding.right-size)+"px";
t.style.fontFamily="sans-serif";
t.style.fontSize=size+"px";
t.innerHTML=dojo.math.round(parseFloat(this.properties.axes.x.range.max),2);
this.vectorNode.appendChild(t);
// y axis labels.
t=document.createElement("div");
t.style.position="absolute";
t.style.top=(size/2)+"px";
t.style.left="0px";
t.style.width=this.properties.padding.left + "px";
t.style.textAlign="right";
t.style.paddingRight="4px";
t.style.fontFamily="sans-serif";
t.style.fontSize=size+"px";
t.innerHTML=dojo.math.round(parseFloat(this.properties.axes.y.range.max),2);
this.vectorNode.appendChild(t);
t=document.createElement("div");
t.style.position="absolute";
t.style.top=(this.properties.height-this.properties.padding.bottom-size)+"px";
t.style.left="0px";
t.style.width=this.properties.padding.left + "px";
t.style.textAlign="right";
t.style.paddingRight="4px";
t.style.fontFamily="sans-serif";
t.style.fontSize=size+"px";
t.innerHTML=dojo.math.round(parseFloat(this.properties.axes.y.range.min),2);
this.vectorNode.appendChild(t);
},
init:function(){
// summary
// Initialize the chart
if(!this.properties.width || !this.properties.height){
var box=dojo.html.getContentBox(this.domNode);
if(!this.properties.width){
this.properties.width=(box.width<32)?this.properties.defaultWidth:box.width;
}
if(!this.properties.height){
this.properties.height=(box.height<32)?this.properties.defaultHeight:box.height;
}
}
// set up the chart; each is a method so that it can be selectively overridden.
this.drawVectorNode();
this.drawPlotArea();
this.drawDataGroup();
this.drawAxes();
// this is last.
this.assignColors();
this._isInitialized=true;
},
destroy:function(){
// summary
// Node cleanup
while(this.domNode.childNodes.length>0){
this.domNode.removeChild(this.domNode.childNodes[0]);
}
this.vectorNode=this.plotArea=this.dataGroup=this.axisGroup=null;
},
render:function(){
// summary
// Draws the data on the chart
if (this.dataGroup){
while(this.dataGroup.childNodes.length>0){
this.dataGroup.removeChild(this.dataGroup.childNodes[0]);
}
} else {
this.init();
}
for(var i=0; i<this.series.length; i++){
dojo.widget.vml.Chart.Plotter.plot(this.series[i], this);
}
},
postCreate:function(){
// summary
// Parse any data if included with the chart, and kick off the rendering.
var table=this.domNode.getElementsByTagName("table")[0];
if (table){
var ranges=this.parseProperties(table);
var bRangeX=false;
var bRangeY=false;
// fix the axes
var axisValues = this.parseData(table);
if(!bRangeX){
this.properties.axes.x.range={min:axisValues.x.min, max:axisValues.x.max};
}
if(!bRangeY){
this.properties.axes.y.range={min:axisValues.y.min, max:axisValues.y.max};
}
this.setAxesPlot(table);
// table values should be populated, now pop it off.
this.domNode.removeChild(table);
}
if(this.series.length>0){
this.render();
}
}
});
dojo.widget.vml.Chart.Plotter=new function(){
// summary
// Singleton for plotting series of data.
var self=this;
var plotters = {};
var types=dojo.widget.Chart.PlotTypes;
this.getX=function(/* string||number */value, /* dojo.widget.Chart */chart){
// summary
// Calculate the x coord on the passed chart for the passed value
var v=parseFloat(value);
var min=chart.properties.axes.x.range.min;
var max=chart.properties.axes.x.range.max;
var ofst=0-min;
min+=ofst; max+=ofst; v+=ofst;
var xmin = 0;
var xmax=chart.properties.width-chart.properties.padding.left-chart.properties.padding.right;
var x=(v*((xmax-xmin)/max))+xmin;
return x;
};
this.getY=function(/* string||number */value, /* dojo.widget.Chart */chart){
// summary
// Calculate the y coord on the passed chart for the passed value
var v=parseFloat(value);
var max=chart.properties.axes.y.range.max;
var min=chart.properties.axes.y.range.min;
var ofst=0;
if(min<0)ofst+=Math.abs(min);
min+=ofst; max+=ofst; v+=ofst;
var ymin=chart.properties.height-chart.properties.padding.top-chart.properties.padding.bottom;
var ymax = 0;
var y=(((ymin-ymax)/(max-min))*(max-v))+ymax;
return y;
};
this.addPlotter=function(/* string */name, /* function */func){
// summary
// add a custom plotter function to this object.
plotters[name]=func;
};
this.plot=function(/* dojo.widget.Chart.DataSeries */series, /* dojo.widget.Chart */chart){
// summary
// plot the passed series.
if (series.values.length==0) return; // void
if (series.plotType && plotters[series.plotType]){
return plotters[series.plotType](series, chart); // void
}
else if (chart.plotType && plotters[chart.plotType]){
return plotters[chart.plotType](series, chart); // void
}
};
// plotting
plotters["bar"]=function(/* dojo.widget.Chart.DataSeries */series, /* dojo.widget.Chart */chart){
// summary
// plot the passed series as a set of bars.
var space=1;
var lastW = 0;
var ys = [];
var yAxis=self.getY(chart.properties.axes.x.plotAt, chart);
var yA = yAxis;
for (var i=0; i<series.values.length; i++){
var x=self.getX(series.values[i].x, chart);
var w;
if (i==series.values.length-1){
w=lastW;
} else{
w=self.getX(series.values[i+1].x, chart)-x-space;
lastW=w;
}
x-=(w/2);
var y=self.getY(series.values[i].value, chart);
var h=Math.abs(yA-y);
if (parseFloat(series.values[i].value) < chart.properties.axes.x.plotAt){
y=yA;
}
var bar=document.createElement("v:rect");
bar.style.position="absolute";
bar.style.top=y+"px";
bar.style.left=x+"px";
bar.style.width=w+"px";
bar.style.height=h+"px";
bar.setAttribute("fillColor", series.color);
bar.setAttribute("stroked", "false");
bar.style.antialias="false";
bar.setAttribute("title", series.label + " (" + i + "): " + series.values[i].value);
var fill=document.createElement("v:fill");
fill.setAttribute("opacity", "0.9");
bar.appendChild(fill);
chart.dataGroup.appendChild(bar);
}
};
plotters["line"]=function(/* dojo.widget.Chart.DataSeries */series, /* dojo.widget.Chart */chart){
// summary
// plot the passed series as a line with tensioning
var tension=1.5;
var line=document.createElement("v:shape");
line.setAttribute("strokeweight", "2px");
line.setAttribute("strokecolor", series.color);
line.setAttribute("fillcolor", "none");
line.setAttribute("filled", "false");
line.setAttribute("title", series.label);
line.setAttribute("coordsize", chart.properties.width + "," + chart.properties.height);
line.style.position="absolute";
line.style.top="0px";
line.style.left="0px";
line.style.width= chart.properties.width+"px";
line.style.height=chart.properties.height+"px";
var stroke=document.createElement("v:stroke");
stroke.setAttribute("opacity", "0.85");
line.appendChild(stroke);
var path = [];
for (var i=0; i<series.values.length; i++){
var x = Math.round(self.getX(series.values[i].x, chart));
var y = Math.round(self.getY(series.values[i].value, chart));
if (i==0){
path.push("m");
path.push(x+","+y);
}else{
var lastx=Math.round(self.getX(series.values[i-1].x, chart));
var lasty=Math.round(self.getY(series.values[i-1].value, chart));
var dx=x-lastx;
var dy=y-lasty;
path.push("c");
var cx=Math.round((x-(tension-1)*(dx/tension)));
path.push(cx+","+lasty);
cx=Math.round((x-(dx/tension)));
path.push(cx+","+y);
path.push(x+","+y);
}
}
line.setAttribute("path", path.join(" ")+" e");
chart.dataGroup.appendChild(line);
};
plotters["area"]=function(/* dojo.widget.Chart.DataSeries */series, /* dojo.widget.Chart */chart){
// summary
// plot the passed series as an area with tensioning.
var tension=1.5;
var line=document.createElement("v:shape");
line.setAttribute("strokeweight", "1px");
line.setAttribute("strokecolor", series.color);
line.setAttribute("fillcolor", series.color);
line.setAttribute("title", series.label);
line.setAttribute("coordsize", chart.properties.width + "," + chart.properties.height);
line.style.position="absolute";
line.style.top="0px";
line.style.left="0px";
line.style.width= chart.properties.width+"px";
line.style.height=chart.properties.height+"px";
var stroke=document.createElement("v:stroke");
stroke.setAttribute("opacity", "0.8");
line.appendChild(stroke);
var fill=document.createElement("v:fill");
fill.setAttribute("opacity", "0.4");
line.appendChild(fill);
var path = [];
for (var i=0; i<series.values.length; i++){
var x = Math.round(self.getX(series.values[i].x, chart));
var y = Math.round(self.getY(series.values[i].value, chart));
if (i==0){
path.push("m");
path.push(x+","+y);
}else{
var lastx=Math.round(self.getX(series.values[i-1].x, chart));
var lasty=Math.round(self.getY(series.values[i-1].value, chart));
var dx=x-lastx;
var dy=y-lasty;
path.push("c");
var cx=Math.round((x-(tension-1)*(dx/tension)));
path.push(cx+","+lasty);
cx=Math.round((x-(dx/tension)));
path.push(cx+","+y);
path.push(x+","+y);
}
}
path.push("l");
path.push(x + "," + self.getY(0, chart));
path.push("l");
path.push(self.getX(0, chart) + "," + self.getY(0,chart));
line.setAttribute("path", path.join(" ")+" x e");
chart.dataGroup.appendChild(line);
};
plotters["scatter"]=function(/* dojo.widget.Chart.DataSeries */series, /* dojo.widget.Chart */chart){
// summary
// plot the passed series as a scatter chart
var r=6;
for (var i=0; i<series.values.length; i++){
var x=self.getX(series.values[i].x, chart);
var y=self.getY(series.values[i].value, chart);
var mod=r/2;
var point=document.createElement("v:rect");
point.setAttribute("fillcolor", series.color);
point.setAttribute("strokecolor", series.color);
point.setAttribute("title", series.label + ": " + series.values[i].value);
point.style.position="absolute";
point.style.rotation="45";
point.style.top=(y-mod)+"px";
point.style.left=(x-mod)+"px";
point.style.width=r+"px";
point.style.height=r+"px";
var fill=document.createElement("v:fill");
fill.setAttribute("opacity", "0.6");
point.appendChild(fill);
chart.dataGroup.appendChild(point);
}
};
plotters["bubble"]=function(/* dojo.widget.Chart.DataSeries */series, /* dojo.widget.Chart */chart){
// summary
// plot the passed series as a series of bubbles (scatter with 3rd dimension)
// added param for series[n].value: size
var minR=1;
// do this off the x axis?
var min=chart.properties.axes.x.range.min;
var max=chart.properties.axes.x.range.max;
var ofst=0-min;
min+=ofst; max+=ofst;
var xmin=chart.properties.padding.left;
var xmax=chart.properties.width-chart.properties.padding.right;
var factor=(max-min)/(xmax-xmin)*25;
for (var i=0; i<series.values.length; i++){
var size = series.values[i].size;
if (isNaN(parseFloat(size))) size=minR;
var radius=(parseFloat(size)*factor)/2;
var diameter=radius * 2;
var cx=self.getX(series.values[i].x, chart);
var cy=self.getY(series.values[i].value, chart);
var top=cy-radius;
var left=cx-radius;
var point=document.createElement("v:oval");
point.setAttribute("fillcolor", series.color);
point.setAttribute("title", series.label + ": " + series.values[i].value + " (" + size + ")");
point.setAttribute("stroked", "false");
point.style.position="absolute";
point.style.top=top+"px";
point.style.left=left+"px";
point.style.width=diameter+"px";
point.style.height=diameter+"px";
var fill=document.createElement("v:fill");
fill.setAttribute("opacity", "0.8");
point.appendChild(fill);
chart.dataGroup.appendChild(point);
}
};
}();