John Cappiello - Dojo.common-0.4.1

Documentation | Source
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);
		}
	};
}();