remove dependence on PlotKit.Base as well
[dygraphs.git] / dygraph-canvas.js
index beef2c0..5b07496 100644 (file)
@@ -2,17 +2,15 @@
 // 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
  */
@@ -121,7 +119,6 @@ DygraphLayout.prototype.evaluateWithError = function() {
   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]);
@@ -164,17 +161,109 @@ DygraphLayout.prototype.updateOptions = function(new_options) {
  */
 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
@@ -217,9 +306,128 @@ DygraphCanvasRenderer.prototype.render = function() {
   // 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
  */