blob: b3ba168fe02891fd60368f31233586aecc9e3493 [file] [log] [blame]
/*
PlotKit Canvas
--------------
Provides HTML Canvas Renderer. This is supported under:
- Safari 2.0
- Mozilla Firefox 1.5
- Opera 9.0 preview 2
- IE 6 (via VML Emulation)
It uses DIVs for labels.
Notes About IE Support
----------------------
This class relies on iecanvas.htc for Canvas Emulation under IE[1].
iecanvas.htc is included in the distribution of PlotKit for convenience. In order to enable IE support, you must set the following option when initialising the renderer:
var renderOptions = {
"IECanvasHTC": "contrib/iecanvas.htc"
};
var engine = new CanvasRenderer(canvasElement, layout, renderOptions);
Where "contrib/iecanvas.htc" is the path to the htc behavior relative
to where your HTML is.
This is only needed for IE support.
Copyright
---------
Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
For use under the BSD license. <http://www.liquidx.net/plotkit>
*/
// --------------------------------------------------------------------
// Check required components
// --------------------------------------------------------------------
try {
if (typeof(PlotKit.Layout) == 'undefined')
{
throw "";
}
}
catch (e) {
throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Base and PlotKit.Layout"
}
// ------------------------------------------------------------------------
// Defines the renderer class
// ------------------------------------------------------------------------
if (typeof(PlotKit.CanvasRenderer) == 'undefined') {
PlotKit.CanvasRenderer = {};
}
PlotKit.CanvasRenderer.NAME = "PlotKit.CanvasRenderer";
PlotKit.CanvasRenderer.VERSION = PlotKit.VERSION;
PlotKit.CanvasRenderer.__repr__ = function() {
return "[" + this.NAME + " " + this.VERSION + "]";
};
PlotKit.CanvasRenderer.toString = function() {
return this.__repr__();
}
PlotKit.CanvasRenderer = function(element, layout, options) {
if (arguments.length > 0)
this.__init__(element, layout, options);
};
PlotKit.CanvasRenderer.prototype.__init__ = function(element, layout, options) {
var isNil = MochiKit.Base.isUndefinedOrNull;
var Color = MochiKit.Color.Color;
// default options
this.options = {
"drawBackground": true,
"backgroundColor": Color.whiteColor(),
"padding": {left: 30, right: 30, top: 5, bottom: 10},
"colorScheme": PlotKit.Base.palette(PlotKit.Base.baseColors()[0]),
"strokeColor": Color.whiteColor(),
"strokeColorTransform": "asStrokeColor",
"strokeWidth": 0.5,
"shouldFill": true,
"shouldStroke": true,
"drawXAxis": true,
"drawYAxis": true,
"axisLineColor": Color.blackColor(),
"axisLineWidth": 0.5,
"axisTickSize": 3,
"axisLabelColor": Color.blackColor(),
"axisLabelFont": "Arial",
"axisLabelFontSize": 9,
"axisLabelWidth": 50,
"pieRadius": 0.4,
"enableEvents": true,
"IECanvasHTC": "PlotKit/iecanvas.htc"
};
MochiKit.Base.update(this.options, options ? options : {});
// we need to refetch the element because of this horrible Canvas on IE
// crap
this.element_id = element.id ? element.id : element;
// Stuff relating to Canvas on IE support
var self = PlotKit.CanvasRenderer;
this.isIE = self.IECanvasEmulationIfNeeded(this.options.IECanvasHTC);
this.IEDelay = 0.5;
this.maxTries = 5;
this.renderDelay = null;
this.clearDelay = null;
this.layout = layout;
this.style = layout.style;
this.element = MochiKit.DOM.getElement(this.element_id);
//this.element = element;
this.container = this.element.parentNode;
this.height = this.element.height;
this.width = this.element.width;
// --- check whether everything is ok before we return
if (isNil(this.element))
throw "CanvasRenderer() - passed canvas is not found";
if (!this.isIE && !(PlotKit.CanvasRenderer.isSupported(this.element)))
throw "CanvasRenderer() - Canvas is not supported.";
if (isNil(this.container) || (this.container.nodeName.toLowerCase() != "div"))
throw "CanvasRenderer() - <canvas> needs to be enclosed in <div>";
// internal state
this.xlabels = new Array();
this.ylabels = new Array();
this.isFirstRender = true;
this.area = {
x: this.options.padding.left,
y: this.options.padding.top,
w: this.width - this.options.padding.left - this.options.padding.right,
h: this.height - this.options.padding.top - this.options.padding.bottom
};
MochiKit.DOM.updateNodeAttributes(this.container,
{"style":{ "position": "relative", "width": this.width + "px"}});
// load event system if we have Signals
try {
this.event_isinside = null;
if (MochiKit.Signal && this.options.enableEvents) {
this._initialiseEvents();
}
}
catch (e) {
// still experimental
}
};
PlotKit.CanvasRenderer.IECanvasEmulationIfNeeded = function(htc) {
var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
if ((!ie) || (ie[1] < 6) || (opera))
return false;
if (isUndefinedOrNull(MochiKit.DOM.getElement('VMLRender'))) {
// before we add VMLRender, we need to recreate all canvas tags
// programmatically otherwise IE will not recognise it
var nodes = document.getElementsByTagName('canvas');
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.getContext) { return; } // Other implementation, abort
var newNode = MochiKit.DOM.CANVAS(
{id: node.id,
width: "" + parseInt(node.width),
height: "" + parseInt(node.height)}, "");
newNode.style.width = parseInt(node.width) + "px";
newNode.style.height = parseInt(node.height) + "px";
node.id = node.id + "_old";
MochiKit.DOM.swapDOM(node, newNode);
}
document.namespaces.add("v");
var vmlopts = {'id':'VMLRender',
'codebase':'vgx.dll',
'classid':'CLSID:10072CEC-8CC1-11D1-986E-00A0C955B42E'};
var vml = MochiKit.DOM.createDOM('object', vmlopts);
document.body.appendChild(vml);
var vmlStyle = document.createStyleSheet();
vmlStyle.addRule("canvas", "behavior: url('" + htc + "');");
vmlStyle.addRule("v\\:*", "behavior: url(#VMLRender);");
}
return true;
};
PlotKit.CanvasRenderer.prototype.render = function() {
if (this.isIE) {
// VML takes a while to start up, so we just poll every this.IEDelay
try {
if (this.renderDelay) {
this.renderDelay.cancel();
this.renderDelay = null;
}
var context = this.element.getContext("2d");
}
catch (e) {
this.isFirstRender = false;
if (this.maxTries-- > 0) {
this.renderDelay = MochiKit.Async.wait(this.IEDelay);
this.renderDelay.addCallback(bind(this.render, this));
}
return;
}
}
if (this.options.drawBackground)
this._renderBackground();
if (this.style == "bar") {
this._renderBarChart();
this._renderBarAxis();
}
else if (this.style == "pie") {
this._renderPieChart();
this._renderPieAxis();
}
else if (this.style == "line") {
this._renderLineChart();
this._renderLineAxis();
}
};
PlotKit.CanvasRenderer.prototype._renderBarChartWrap = function(data, plotFunc) {
var context = this.element.getContext("2d");
var colorCount = this.options.colorScheme.length;
var colorScheme = this.options.colorScheme;
var setNames = MochiKit.Base.keys(this.layout.datasets);
var setCount = setNames.length;
for (var i = 0; i < setCount; i++) {
var setName = setNames[i];
var color = colorScheme[i%colorCount];
context.save();
context.fillStyle = color.toRGBString();
if (this.options.strokeColor)
context.strokeStyle = this.options.strokeColor.toRGBString();
else if (this.options.strokeColorTransform)
context.strokeStyle = color[this.options.strokeColorTransform]().toRGBString();
context.lineWidth = this.options.strokeWidth;
var forEachFunc = function(obj) {
if (obj.name == setName)
plotFunc(context, obj);
};
MochiKit.Iter.forEach(data, bind(forEachFunc, this));
context.restore();
}
};
PlotKit.CanvasRenderer.prototype._renderBarChart = function() {
var bind = MochiKit.Base.bind;
var drawRect = function(context, bar) {
var x = this.area.w * bar.x + this.area.x;
var y = this.area.h * bar.y + this.area.y;
var w = this.area.w * bar.w;
var h = this.area.h * bar.h;
if ((w < 1) || (h < 1))
return;
if (this.options.shouldFill)
context.fillRect(x, y, w, h);
if (this.options.shouldStroke)
context.strokeRect(x, y, w, h);
};
this._renderBarChartWrap(this.layout.bars, bind(drawRect, this));
};
PlotKit.CanvasRenderer.prototype._renderLineChart = function() {
var context = this.element.getContext("2d");
var colorCount = this.options.colorScheme.length;
var colorScheme = this.options.colorScheme;
var setNames = MochiKit.Base.keys(this.layout.datasets);
var setCount = setNames.length;
var bind = MochiKit.Base.bind;
var partial = MochiKit.Base.partial;
for (var i = 0; i < setCount; i++) {
var setName = setNames[i];
var color = colorScheme[i%colorCount];
var strokeX = this.options.strokeColorTransform;
// setup graphics context
context.save();
context.fillStyle = color.toRGBString();
if (this.options.strokeColor)
context.strokeStyle = this.options.strokeColor.toRGBString();
else if (this.options.strokeColorTransform)
context.strokeStyle = color[strokeX]().toRGBString();
context.lineWidth = this.options.strokeWidth;
// create paths
var makePath = function() {
context.beginPath();
context.moveTo(this.area.x, this.area.y + this.area.h);
var addPoint = function(context, point) {
if (point.name == setName)
context.lineTo(this.area.w * point.x + this.area.x,
this.area.h * point.y + this.area.y);
};
MochiKit.Iter.forEach(this.layout.points, partial(addPoint, context), this);
context.lineTo(this.area.w + this.area.x,
this.area.h + this.area.y);
context.lineTo(this.area.x, this.area.y + this.area.h);
context.closePath();
};
if (this.options.shouldFill) {
bind(makePath, this)();
context.fill();
}
if (this.options.shouldStroke) {
bind(makePath, this)();
context.stroke();
}
context.restore();
}
};
PlotKit.CanvasRenderer.prototype._renderPieChart = function() {
var context = this.element.getContext("2d");
var colorCount = this.options.colorScheme.length;
var slices = this.layout.slices;
var centerx = this.area.x + this.area.w * 0.5;
var centery = this.area.y + this.area.h * 0.5;
var radius = Math.min(this.area.w * this.options.pieRadius,
this.area.h * this.options.pieRadius);
if (this.isIE) {
centerx = parseInt(centerx);
centery = parseInt(centery);
radius = parseInt(radius);
}
// NOTE NOTE!! Canvas Tag draws the circle clockwise from the y = 0, x = 1
// so we have to subtract 90 degrees to make it start at y = 1, x = 0
for (var i = 0; i < slices.length; i++) {
var color = this.options.colorScheme[i%colorCount];
context.save();
context.fillStyle = color.toRGBString();
var makePath = function() {
context.beginPath();
context.moveTo(centerx, centery);
context.arc(centerx, centery, radius,
slices[i].startAngle - Math.PI/2,
slices[i].endAngle - Math.PI/2,
false);
context.lineTo(centerx, centery);
context.closePath();
};
if (Math.abs(slices[i].startAngle - slices[i].endAngle) > 0.001) {
if (this.options.shouldFill) {
makePath();
context.fill();
}
if (this.options.shouldStroke) {
makePath();
context.lineWidth = this.options.strokeWidth;
if (this.options.strokeColor)
context.strokeStyle = this.options.strokeColor.toRGBString();
else if (this.options.strokeColorTransform)
context.strokeStyle = color[this.options.strokeColorTransform]().toRGBString();
context.stroke();
}
}
context.restore();
}
};
PlotKit.CanvasRenderer.prototype._renderBarAxis = function() {
this._renderAxis();
}
PlotKit.CanvasRenderer.prototype._renderLineAxis = function() {
this._renderAxis();
};
PlotKit.CanvasRenderer.prototype._renderAxis = function() {
if (!this.options.drawXAxis && !this.options.drawYAxis)
return;
var context = this.element.getContext("2d");
var labelStyle = {"style":
{"position": "absolute",
"fontSize": this.options.axisLabelFontSize + "px",
"zIndex": 10,
"color": this.options.axisLabelColor.toRGBString(),
"width": this.options.axisLabelWidth + "px",
"overflow": "hidden"
}
};
// axis lines
context.save();
context.strokeStyle = this.options.axisLineColor.toRGBString();
context.lineWidth = this.options.axisLineWidth;
if (this.options.drawYAxis) {
if (this.layout.yticks) {
var drawTick = function(tick) {
var x = this.area.x;
var y = this.area.y + tick[0] * this.area.h;
context.beginPath();
context.moveTo(x, y);
context.lineTo(x - this.options.axisTickSize, y);
context.closePath();
context.stroke();
var label = DIV(labelStyle, tick[1]);
label.style.top = (y - this.options.axisLabelFontSize) + "px";
label.style.left = (x - this.options.padding.left - this.options.axisTickSize) + "px";
label.style.textAlign = "right";
label.style.width = (this.options.padding.left - this.options.axisTickSize * 2) + "px";
MochiKit.DOM.appendChildNodes(this.container, label);
this.ylabels.push(label);
};
MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this));
}
context.beginPath();
context.moveTo(this.area.x, this.area.y);
context.lineTo(this.area.x, this.area.y + this.area.h);
context.closePath();
context.stroke();
}
if (this.options.drawXAxis) {
if (this.layout.xticks) {
var drawTick = function(tick) {
var x = this.area.x + tick[0] * this.area.w;
var y = this.area.y + this.area.h;
context.beginPath();
context.moveTo(x, y);
context.lineTo(x, y + this.options.axisTickSize);
context.closePath();
context.stroke();
var label = DIV(labelStyle, tick[1]);
label.style.top = (y + this.options.axisTickSize) + "px";
label.style.left = (x - this.options.axisLabelWidth/2) + "px";
label.style.textAlign = "center";
label.style.width = this.options.axisLabelWidth + "px";
MochiKit.DOM.appendChildNodes(this.container, label);
this.xlabels.push(label);
};
MochiKit.Iter.forEach(this.layout.xticks, bind(drawTick, this));
}
context.beginPath();
context.moveTo(this.area.x, this.area.y + this.area.h);
context.lineTo(this.area.x + this.area.w, this.area.y + this.area.h);
context.closePath();
context.stroke();
}
context.restore();
};
PlotKit.CanvasRenderer.prototype._renderPieAxis = function() {
if (!this.options.drawXAxis)
return;
if (this.layout.xticks) {
// make a lookup dict for x->slice values
var lookup = new Array();
for (var i = 0; i < this.layout.slices.length; i++) {
lookup[this.layout.slices[i].xval] = this.layout.slices[i];
}
var centerx = this.area.x + this.area.w * 0.5;
var centery = this.area.y + this.area.h * 0.5;
var radius = Math.min(this.area.w * this.options.pieRadius,
this.area.h * this.options.pieRadius);
var labelWidth = this.options.axisLabelWidth;
for (var i = 0; i < this.layout.xticks.length; i++) {
var slice = lookup[this.layout.xticks[i][0]];
if (MochiKit.Base.isUndefinedOrNull(slice))
continue;
var angle = (slice.startAngle + slice.endAngle)/2;
// normalize the angle
var normalisedAngle = angle;
if (normalisedAngle > Math.PI * 2)
normalisedAngle = normalisedAngle - Math.PI * 2;
else if (normalisedAngle < 0)
normalisedAngle = normalisedAngle + Math.PI * 2;
var labelx = centerx + Math.sin(normalisedAngle) * (radius + 10);
var labely = centery - Math.cos(normalisedAngle) * (radius + 10);
var attrib = {"position": "absolute",
"zIndex": 11,
"width": labelWidth + "px",
"fontSize": this.options.axisLabelFontSize + "px",
"overflow": "hidden",
"color": this.options.axisLabelColor.toHexString()
};
if (normalisedAngle <= Math.PI * 0.5) {
// text on top and align left
attrib["textAlign"] = "left";
attrib["verticalAlign"] = "top";
attrib["left"] = labelx + "px";
attrib["top"] = (labely - this.options.axisLabelFontSize) + "px";
}
else if ((normalisedAngle > Math.PI * 0.5) && (normalisedAngle <= Math.PI)) {
// text on bottom and align left
attrib["textAlign"] = "left";
attrib["verticalAlign"] = "bottom";
attrib["left"] = labelx + "px";
attrib["top"] = labely + "px";
}
else if ((normalisedAngle > Math.PI) && (normalisedAngle <= Math.PI*1.5)) {
// text on bottom and align right
attrib["textAlign"] = "right";
attrib["verticalAlign"] = "bottom";
attrib["left"] = (labelx - labelWidth) + "px";
attrib["top"] = labely + "px";
}
else {
// text on top and align right
attrib["textAlign"] = "right";
attrib["verticalAlign"] = "bottom";
attrib["left"] = (labelx - labelWidth) + "px";
attrib["top"] = (labely - this.options.axisLabelFontSize) + "px";
}
var label = DIV({'style': attrib}, this.layout.xticks[i][1]);
this.xlabels.push(label);
MochiKit.DOM.appendChildNodes(this.container, label);
}
}
};
PlotKit.CanvasRenderer.prototype._renderBackground = function() {
var context = this.element.getContext("2d");
context.save();
context.fillStyle = this.options.backgroundColor.toRGBString();
context.fillRect(0, 0, this.width, this.height);
context.restore();
};
PlotKit.CanvasRenderer.prototype.clear = function() {
if (this.isIE) {
// VML takes a while to start up, so we just poll every this.IEDelay
try {
if (this.clearDelay) {
this.clearDelay.cancel();
this.clearDelay = null;
}
var context = this.element.getContext("2d");
}
catch (e) {
this.isFirstRender = false;
this.clearDelay = MochiKit.Async.wait(this.IEDelay);
this.clearDelay.addCallback(bind(this.clear, this));
return;
}
}
var context = this.element.getContext("2d");
context.clearRect(0, 0, this.width, this.height);
for (var i = 0; i < this.xlabels.length; i++) {
MochiKit.DOM.removeElement(this.xlabels[i]);
}
for (var i = 0; i < this.ylabels.length; i++) {
MochiKit.DOM.removeElement(this.ylabels[i]);
}
this.xlabels = new Array();
this.ylabels = new Array();
};
PlotKit.CanvasRenderer.prototype._initialiseEvents = function() {
var connect = MochiKit.Signal.connect;
var bind = MochiKit.Base.bind;
MochiKit.Signal.registerSignals(this, ['onmouseover', 'onclick', 'onmouseout', 'onmousemove']);
//connect(this.element, 'onmouseover', bind(this.onmouseover, this));
//connect(this.element, 'onmouseout', bind(this.onmouseout, this));
//connect(this.element, 'onmousemove', bind(this.onmousemove, this));
connect(this.element, 'onclick', bind(this.onclick, this));
};
PlotKit.CanvasRenderer.prototype._resolveObject = function(e) {
// does not work in firefox
//var x = (e.event().offsetX - this.area.x) / this.area.w;
//var y = (e.event().offsetY - this.area.y) / this.area.h;
var x = (e.mouse().page.x - PlotKit.Base.findPosX(this.element) - this.area.x) / this.area.w;
var y = (e.mouse().page.y - PlotKit.Base.findPosY(this.element) - this.area.y) / this.area.h;
//log(x, y);
var isHit = this.layout.hitTest(x, y);
if (isHit)
return isHit;
return null;
};
PlotKit.CanvasRenderer.prototype._createEventObject = function(layoutObj, e) {
if (layoutObj == null) {
return null;
}
e.chart = layoutObj
return e;
};
PlotKit.CanvasRenderer.prototype.onclick = function(e) {
var layoutObject = this._resolveObject(e);
var eventObject = this._createEventObject(layoutObject, e);
if (eventObject != null)
MochiKit.Signal.signal(this, "onclick", eventObject);
};
PlotKit.CanvasRenderer.prototype.onmouseover = function(e) {
var layoutObject = this._resolveObject(e);
var eventObject = this._createEventObject(layoutObject, e);
if (eventObject != null)
signal(this, "onmouseover", eventObject);
};
PlotKit.CanvasRenderer.prototype.onmouseout = function(e) {
var layoutObject = this._resolveObject(e);
var eventObject = this._createEventObject(layoutObject, e);
if (eventObject == null)
signal(this, "onmouseout", e);
else
signal(this, "onmouseout", eventObject);
};
PlotKit.CanvasRenderer.prototype.onmousemove = function(e) {
var layoutObject = this._resolveObject(e);
var eventObject = this._createEventObject(layoutObject, e);
if ((layoutObject == null) && (this.event_isinside == null)) {
// TODO: should we emit an event anyway?
return;
}
if ((layoutObject != null) && (this.event_isinside == null))
signal(this, "onmouseover", eventObject);
if ((layoutObject == null) && (this.event_isinside != null))
signal(this, "onmouseout", eventObject);
if ((layoutObject != null) && (this.event_isinside != null))
signal(this, "onmousemove", eventObject);
this.event_isinside = layoutObject;
//log("move", x, y);
};
PlotKit.CanvasRenderer.isSupported = function(canvasName) {
var canvas = null;
try {
if (MochiKit.Base.isUndefinedOrNull(canvasName))
canvas = MochiKit.DOM.CANVAS({});
else
canvas = MochiKit.DOM.getElement(canvasName);
var context = canvas.getContext("2d");
}
catch (e) {
var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
if ((!ie) || (ie[1] < 6) || (opera))
return false;
return true;
}
return true;
};