/* 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 For use under the BSD license. */ // -------------------------------------------------------------------- // 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() - needs to be enclosed in
"; // 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; };