// All Rights Reserved.
/**
- * @fileoverview Subclasses various parts of PlotKit to meet the additional
- * needs of Dygraph: grid overlays and error bars
+ * @fileoverview Based on PlotKit, but modified to meet the needs of dygraphs.
+ * In particular, support for:
+ * - grid overlays
+ * - error bars
+ * - dygraphs attribute system
*/
-// Subclass PlotKit.Layout to add:
-// 1. Sigma/errorBars properties
-// 2. Copy error terms for PlotKit.CanvasRenderer._renderLineChart
-
/**
- * Creates a new DygraphLayout object. Options are the same as those allowed
- * by the PlotKit.Layout constructor.
+ * Creates a new DygraphLayout object.
* @param {Object} options Options for PlotKit.Layout
* @return {Object} The DygraphLayout object
*/
for (var setName in this.datasets) {
var j = 0;
var dataset = this.datasets[setName];
- if (PlotKit.Base.isFuncLike(dataset)) continue;
for (var j = 0; j < dataset.length; j++, i++) {
var item = dataset[j];
var xv = parseFloat(item[0]);
*/
DygraphCanvasRenderer = function(dygraph, element, layout, options) {
// TODO(danvk): remove options, just use dygraph.attr_.
- PlotKit.CanvasRenderer.call(this, element, layout, options);
this.dygraph_ = dygraph;
- this.options.drawYGrid = true;
- this.options.drawXGrid = true;
- this.options.gridLineColor = MochiKit.Color.Color.grayColor();
+
+ // default options
+ this.options = {
+ "strokeWidth": 0.5,
+ "drawXAxis": true,
+ "drawYAxis": true,
+ "axisLineColor": Color.blackColor(),
+ "axisLineWidth": 0.5,
+ "axisTickSize": 3,
+ "axisLabelColor": Color.blackColor(),
+ "axisLabelFont": "Arial",
+ "axisLabelFontSize": 9,
+ "axisLabelWidth": 50,
+ "drawYGrid": true,
+ "drawXGrid": true,
+ "gridLineColor": MochiKit.Color.Color.grayColor()
+ };
MochiKit.Base.update(this.options, options);
- // TODO(danvk) This shouldn't be necessary: effects should be overlaid
- this.options.drawBackground = false;
+ this.layout = layout;
+ this.element = MochiKit.DOM.getElement(element);
+ this.container = this.element.parentNode;
+
+ // Stuff relating to Canvas on IE support
+ this.isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+
+ 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 (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element)))
+ throw "Canvas is not supported.";
+
+ // internal state
+ this.xlabels = new Array();
+ this.ylabels = new Array();
+
+ this.area = {
+ x: this.options.yAxisLabelWidth + 2 * this.options.axisTickSize,
+ y: 0
+ };
+ this.area.w = this.width - this.area.x - this.options.rightGap;
+ this.area.h = this.height - this.options.axisLabelFontSize -
+ 2 * this.options.axisTickSize;
+
+ MochiKit.DOM.updateNodeAttributes(this.container,
+ {"style":{ "position": "relative", "width": this.width + "px"}});
+};
+
+DygraphCanvasRenderer.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.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();
+};
+
+
+DygraphCanvasRenderer.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;
};
-DygraphCanvasRenderer.prototype = new PlotKit.CanvasRenderer();
/**
* Draw an X/Y grid on top of the existing plot
// Do the ordinary rendering, as before
// TODO(danvk) Call super.render()
this._renderLineChart();
- this._renderLineAxis();
+ this._renderAxis();
+};
+
+
+DygraphCanvasRenderer.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]);
+ var top = (y - this.options.axisLabelFontSize / 2);
+ if (top < 0) top = 0;
+
+ if (top + this.options.axisLabelFontSize + 3 > this.height) {
+ label.style.bottom = "0px";
+ } else {
+ label.style.top = top + "px";
+ }
+ label.style.left = "0px";
+ label.style.textAlign = "right";
+ label.style.width = this.options.yAxisLabelWidth + "px";
+ MochiKit.DOM.appendChildNodes(this.container, label);
+ this.ylabels.push(label);
+ };
+
+ MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this));
+
+ // The lowest tick on the y-axis often overlaps with the leftmost
+ // tick on the x-axis. Shift the bottom tick up a little bit to
+ // compensate if necessary.
+ var bottomTick = this.ylabels[0];
+ var fontSize = this.options.axisLabelFontSize;
+ var bottom = parseInt(bottomTick.style.top) + fontSize;
+ if (bottom > this.height - fontSize) {
+ bottomTick.style.top = (parseInt(bottomTick.style.top) -
+ fontSize / 2) + "px";
+ }
+ }
+
+ 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.textAlign = "center";
+ label.style.bottom = "0px";
+
+ var left = (x - this.options.axisLabelWidth/2);
+ if (left + this.options.axisLabelWidth > this.width) {
+ left = this.width - this.options.xAxisLabelWidth;
+ label.style.textAlign = "right";
+ }
+ if (left < 0) {
+ left = 0;
+ label.style.textAlign = "left";
+ }
+
+ label.style.left = left + "px";
+ label.style.width = this.options.xAxisLabelWidth + "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();
};
+
/**
* Overrides the CanvasRenderer method to draw error bars
*/