/* 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. Copyright --------- Copyright 2005,2006 (c) Alastair Tse For use under the BSD license. */ // -------------------------------------------------------------------- // Check required components // -------------------------------------------------------------------- try { if ((typeof(PlotKit.Base) == 'undefined') || (typeof(PlotKit.Layout) == 'undefined')) { throw ""; } } catch (e) { throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.{Base,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 }; MochiKit.Base.update(this.options, options ? options : {}); this.layout = layout; this.element = MochiKit.DOM.getElement(element); this.container = this.element.parentNode; // Stuff relating to Canvas on IE support this.isIE = PlotKit.Base.excanvasSupported(); if (this.isIE && !isNil(G_vmlCanvasManager)) { this.IEDelay = 0.5; this.maxTries = 5; this.renderDelay = null; this.clearDelay = null; this.element = G_vmlCanvasManager.initElement(this.element); } 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 /* Disabled until we have a proper implementation try { this.event_isinside = null; if (MochiKit.Signal && this.options.enableEvents) { this._initialiseEvents(); } } catch (e) { // still experimental } */ }; 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.layout.style == "bar") { this._renderBarChart(); this._renderBarAxis(); } else if (this.layout.style == "pie") { this._renderPieChart(); this._renderPieAxis(); } else if (this.layout.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(ctx) { ctx.beginPath(); ctx.moveTo(this.area.x, this.area.y + this.area.h); var addPoint = function(ctx_, point) { if (point.name == setName) ctx_.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, ctx), this); ctx.lineTo(this.area.w + this.area.x, this.area.h + this.area.y); ctx.lineTo(this.area.x, this.area.y + this.area.h); ctx.closePath(); }; if (this.options.shouldFill) { bind(makePath, this)(context); context.fill(); } if (this.options.shouldStroke) { bind(makePath, this)(context); 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) { if (typeof(tick) == "function") return; 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) { if (typeof(dataset) == "function") return; 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); MochiKit.Iter.forEach(this.xlabels, MochiKit.DOM.removeElement); MochiKit.Iter.forEach(this.ylabels, MochiKit.DOM.removeElement); this.xlabels = new Array(); this.ylabels = new Array(); }; // ---------------------------------------------------------------- // Everything below here is experimental and undocumented. // ---------------------------------------------------------------- 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; }; // Namespace Iniitialisation PlotKit.Canvas = {} PlotKit.Canvas.CanvasRenderer = PlotKit.CanvasRenderer; PlotKit.Canvas.EXPORT = [ "CanvasRenderer" ]; PlotKit.Canvas.EXPORT_OK = [ "CanvasRenderer" ]; PlotKit.Canvas.__new__ = function() { var m = MochiKit.Base; m.nameFunctions(this); this.EXPORT_TAGS = { ":common": this.EXPORT, ":all": m.concat(this.EXPORT, this.EXPORT_OK) }; }; PlotKit.Canvas.__new__(); MochiKit.Base._exportSymbols(this, PlotKit.Canvas);