fix bug in toDomCoords
[dygraphs.git] / dygraph.js
index 7fc778f..6bacea2 100644 (file)
@@ -162,6 +162,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.dateWindow_ = attrs.dateWindow || null;
   this.valueRange_ = attrs.valueRange || null;
   this.wilsonInterval_ = attrs.wilsonInterval || true;
+  this.is_initial_draw_ = true;
 
   // Clear the div. This ensure that, if multiple dygraphs are passed the same
   // div, then only one will be drawn.
@@ -187,6 +188,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
     this.height_ = (this.height_ * self.innerHeight / 100) - 10;
   }
 
+  // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
   if (attrs['stackedGraph']) {
     attrs['fillGraph'] = true;
     // TODO(nikhilk): Add any other stackedGraph checks here.
@@ -263,6 +265,70 @@ Dygraph.prototype.rollPeriod = function() {
   return this.rollPeriod_;
 };
 
+/**
+ * Returns the currently-visible x-range. This can be affected by zooming,
+ * panning or a call to updateOptions.
+ * Returns a two-element array: [left, right].
+ * If the Dygraph has dates on the x-axis, these will be millis since epoch.
+ */
+Dygraph.prototype.xAxisRange = function() {
+  if (this.dateWindow_) return this.dateWindow_;
+
+  // The entire chart is visible.
+  var left = this.rawData_[0][0];
+  var right = this.rawData_[this.rawData_.length - 1][0];
+  return [left, right];
+};
+
+/**
+ * Returns the currently-visible y-range. This can be affected by zooming,
+ * panning or a call to updateOptions.
+ * Returns a two-element array: [bottom, top].
+ */
+Dygraph.prototype.yAxisRange = function() {
+  return this.displayedYRange_;
+};
+
+/**
+ * Convert from data coordinates to canvas/div X/Y coordinates.
+ * Returns a two-element array: [X, Y]
+ */
+Dygraph.prototype.toDomCoords = function(x, y) {
+  var ret = [null, null];
+  var area = this.plotter_.area;
+  if (x !== null) {
+    var xRange = this.xAxisRange();
+    ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
+  }
+
+  if (y !== null) {
+    var yRange = this.yAxisRange();
+    ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
+  }
+
+  return ret;
+};
+
+/**
+ * Convert from canvas/div coords to data coordinates.
+ * Returns a two-element array: [X, Y]
+ */
+Dygraph.prototype.toDataCoords = function(x, y) {
+  var ret = [null, null];
+  var area = this.plotter_.area;
+  if (x !== null) {
+    var xRange = this.xAxisRange();
+    ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+  }
+
+  if (y !== null) {
+    var yRange = this.yAxisRange();
+    ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
+  }
+
+  return ret;
+};
+
 Dygraph.addEvent = function(el, evt, fn) {
   var normed_fn = function(e) {
     if (!e) var e = window.event;
@@ -275,6 +341,13 @@ Dygraph.addEvent = function(el, evt, fn) {
   }
 };
 
+Dygraph.clipCanvas_ = function(cnv, clip) {
+  var ctx = cnv.getContext("2d");
+  ctx.beginPath();
+  ctx.rect(clip.left, clip.top, clip.width, clip.height);
+  ctx.clip();
+};
+
 /**
  * Generates interface elements for the Dygraph: a containing div, a div to
  * display the current point, and a textbox to adjust the rolling average
@@ -290,8 +363,16 @@ Dygraph.prototype.createInterface_ = function() {
   this.graphDiv.style.height = this.height_ + "px";
   enclosing.appendChild(this.graphDiv);
 
+  var clip = {
+    top: 0,
+    left: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
+  };
+  clip.width = this.width_ - clip.left - this.attr_("rightGap");
+  clip.height = this.height_ - this.attr_("axisLabelFontSize")
+      - 2 * this.attr_("axisTickSize");
+  this.clippingArea_ = clip;
+
   // Create the canvas for interactive parts of the chart.
-  // this.canvas_ = document.createElement("canvas");
   this.canvas_ = Dygraph.createCanvas();
   this.canvas_.style.position = "absolute";
   this.canvas_.width = this.width_;
@@ -303,6 +384,10 @@ Dygraph.prototype.createInterface_ = function() {
   // ... and for static parts of the chart.
   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
 
+  // Make sure we don't overdraw.
+  Dygraph.clipCanvas_(this.hidden_, this.clippingArea_);
+  Dygraph.clipCanvas_(this.canvas_, this.clippingArea_);
+
   var dygraph = this;
   Dygraph.addEvent(this.hidden_, 'mousemove', function(e) {
     dygraph.mouseMove_(e);
@@ -334,7 +419,35 @@ Dygraph.prototype.createInterface_ = function() {
   this.createStatusMessage_();
   this.createRollInterface_();
   this.createDragInterface_();
-}
+};
+
+/**
+ * Detach DOM elements in the dygraph and null out all data references.
+ * Calling this when you're done with a dygraph can dramatically reduce memory
+ * usage. See, e.g., the tests/perf.html example.
+ */
+Dygraph.prototype.destroy = function() {
+  var removeRecursive = function(node) {
+    while (node.hasChildNodes()) {
+      removeRecursive(node.firstChild);
+      node.removeChild(node.firstChild);
+    }
+  };
+  removeRecursive(this.maindiv_);
+
+  var nullOut = function(obj) {
+    for (var n in obj) {
+      if (typeof(obj[n]) === 'object') {
+        obj[n] = null;
+      }
+    }
+  };
+
+  // These may not all be necessary, but it can't hurt...
+  nullOut(this.layout_);
+  nullOut(this.plotter_);
+  nullOut(this);
+};
 
 /**
  * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
@@ -344,9 +457,11 @@ Dygraph.prototype.createInterface_ = function() {
  * @private
  */
 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
-  // var h = document.createElement("canvas");
   var h = Dygraph.createCanvas();
   h.style.position = "absolute";
+  // TODO(danvk): h should be offset from canvas. canvas needs to include
+  // some extra area to make it easier to zoom in on the far left and far
+  // right. h needs to be precisely the plot area, so that clipping occurs.
   h.style.top = canvas.style.top;
   h.style.left = canvas.style.left;
   h.width = this.width_;
@@ -436,34 +551,41 @@ Dygraph.prototype.getColors = function() {
   return this.colors_;
 };
 
-// The following functions are from quirksmode.org
+// The following functions are from quirksmode.org with a modification for Safari from
+// http://blog.firetree.net/2005/07/04/javascript-find-position/
 // http://www.quirksmode.org/js/findpos.html
 Dygraph.findPosX = function(obj) {
   var curleft = 0;
-  if (obj.offsetParent) {
-    while (obj.offsetParent) {
+  if(obj.offsetParent)
+    while(1) 
+    {
       curleft += obj.offsetLeft;
+      if(!obj.offsetParent)
+        break;
       obj = obj.offsetParent;
     }
-  }
-  else if (obj.x)
+  else if(obj.x)
     curleft += obj.x;
   return curleft;
 };
 
 Dygraph.findPosY = function(obj) {
   var curtop = 0;
-  if (obj.offsetParent) {
-    while (obj.offsetParent) {
+  if(obj.offsetParent)
+    while(1)
+    {
       curtop += obj.offsetTop;
+      if(!obj.offsetParent)
+        break;
       obj = obj.offsetParent;
     }
-  }
-  else if (obj.y)
+  else if(obj.y)
     curtop += obj.y;
   return curtop;
 };
 
+
+
 /**
  * Create the div that contains information on the selected point(s)
  * This goes in the top right of the canvas, unless an external div has already
@@ -787,18 +909,24 @@ Dygraph.prototype.mouseMove_ = function(event) {
   }
 
   if (this.attr_("highlightCallback")) {
-    var callbackPoints = this.selPoints_.map(
-        function(p) { return {xval: p.xval, yval: p.yval, name: p.name} });
-    if (this.attr_("stackedGraph")) {
-      // "unstack" the points.
-      var cumulative_sum = 0;
-      for (var j = callbackPoints.length - 1; j >= 0; j--) {
-        callbackPoints[j].yval -= cumulative_sum;
-        cumulative_sum += callbackPoints[j].yval;
+    var px = this.lastHighlightCallbackX;
+    if (px !== null && lastx != px) {
+      // only fire if the selected point has changed.
+      this.lastHighlightCallbackX = lastx;
+      if (!this.attr_("stackedGraph")) {
+        this.attr_("highlightCallback")(event, lastx, this.selPoints_);
+      } else {
+        // "unstack" the points.
+        var callbackPoints = this.selPoints_.map(
+            function(p) { return {xval: p.xval, yval: p.yval, name: p.name} });
+        var cumulative_sum = 0;
+        for (var j = callbackPoints.length - 1; j >= 0; j--) {
+          callbackPoints[j].yval -= cumulative_sum;
+          cumulative_sum += callbackPoints[j].yval;
+        }
+        this.attr_("highlightCallback")(event, lastx, callbackPoints);
       }
     }
-
-    this.attr_("highlightCallback")(event, lastx, callbackPoints);
   }
 
   // Clear the previously drawn vertical, if there is one
@@ -880,10 +1008,8 @@ Dygraph.prototype.hmsString_ = function(date) {
     return zeropad(d.getHours()) + ":" +
            zeropad(d.getMinutes()) + ":" +
            zeropad(d.getSeconds());
-  } else if (d.getMinutes()) {
-    return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
   } else {
-    return zeropad(d.getHours());
+    return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
   }
 }
 
@@ -1271,6 +1397,10 @@ Dygraph.prototype.extremeValues_ = function(series) {
  * @private
  */
 Dygraph.prototype.drawGraph_ = function(data) {
+  // This is used to set the second parameter to drawCallback, below.
+  var is_initial_draw = this.is_initial_draw_;
+  this.is_initial_draw_ = false;
+
   var minY = null, maxY = null;
   this.layout_.removeAllDatasets();
   this.setColors_();
@@ -1278,10 +1408,9 @@ Dygraph.prototype.drawGraph_ = function(data) {
 
   // For stacked series.
   var cumulative_y = [];
-  var datasets = [];
+  var stacked_datasets = [];
 
   // Loop over all fields in the dataset
-
   for (var i = 1; i < data[0].length; i++) {
     if (!this.visibility()[i - 1]) continue;
 
@@ -1293,16 +1422,31 @@ Dygraph.prototype.drawGraph_ = function(data) {
     series = this.rollingAverage(series, this.rollPeriod_);
 
     // Prune down to the desired range, if necessary (for zooming)
+    // Because there can be lines going to points outside of the visible area,
+    // we actually prune to visible points, plus one on either side.
     var bars = this.attr_("errorBars") || this.attr_("customBars");
     if (this.dateWindow_) {
       var low = this.dateWindow_[0];
       var high= this.dateWindow_[1];
       var pruned = [];
+      // TODO(danvk): do binary search instead of linear search.
+      // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+      var firstIdx = null, lastIdx = null;
       for (var k = 0; k < series.length; k++) {
-        if (series[k][0] >= low && series[k][0] <= high) {
-          pruned.push(series[k]);
+        if (series[k][0] >= low && firstIdx === null) {
+          firstIdx = k;
+        }
+        if (series[k][0] <= high) {
+          lastIdx = k;
         }
       }
+      if (firstIdx === null) firstIdx = 0;
+      if (firstIdx > 0) firstIdx--;
+      if (lastIdx === null) lastIdx = series.length - 1;
+      if (lastIdx < series.length - 1) lastIdx++;
+      for (var k = firstIdx; k <= lastIdx; k++) {
+        pruned.push(series[k]);
+      }
       series = pruned;
     }
 
@@ -1334,16 +1478,16 @@ Dygraph.prototype.drawGraph_ = function(data) {
         if (!maxY || cumulative_y[series[j][0]] > maxY)
           maxY = cumulative_y[series[j][0]];
       }
-      datasets.push([this.attr_("labels")[i], vals]);
+      stacked_datasets.push([this.attr_("labels")[i], vals]);
       //this.layout_.addDataset(this.attr_("labels")[i], vals);
     } else {
       this.layout_.addDataset(this.attr_("labels")[i], series);
     }
   }
 
-  if (datasets.length > 0) {
-    for (var i = (datasets.length - 1); i >= 0; i--) {
-      this.layout_.addDataset(datasets[i][0], datasets[i][1]);
+  if (stacked_datasets.length > 0) {
+    for (var i = (stacked_datasets.length - 1); i >= 0; i--) {
+      this.layout_.addDataset(stacked_datasets[i][0], stacked_datasets[i][1]);
     }
   }
 
@@ -1351,6 +1495,7 @@ Dygraph.prototype.drawGraph_ = function(data) {
   // set explicitly by the user.
   if (this.valueRange_ != null) {
     this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
+    this.displayedYRange_ = this.valueRange_;
   } else {
     // This affects the calculation of span, below.
     if (this.attr_("includeZero") && minY > 0) {
@@ -1374,6 +1519,7 @@ Dygraph.prototype.drawGraph_ = function(data) {
     }
 
     this.addYTicks_(minAxisY, maxAxisY);
+    this.displayedYRange_ = [minAxisY, maxAxisY];
   }
 
   this.addXTicks_();
@@ -1385,6 +1531,10 @@ Dygraph.prototype.drawGraph_ = function(data) {
   this.plotter_.render();
   this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
                                          this.canvas_.height);
+
+  if (this.attr_("drawCallback") !== null) {
+    this.attr_("drawCallback")(this, is_initial_draw);
+  }
 };
 
 /**
@@ -1533,7 +1683,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
 Dygraph.dateParser = function(dateStr, self) {
   var dateStrSlashed;
   var d;
-  if (dateStr.length == 10 && dateStr.search("-") != -1) {  // e.g. '2009-07-12'
+  if (dateStr.search("-") != -1) {  // e.g. '2009-7-12' or '2009-07-12'
     dateStrSlashed = dateStr.replace("-", "/", "g");
     while (dateStrSlashed.search("-") != -1) {
       dateStrSlashed = dateStrSlashed.replace("-", "/");
@@ -1722,8 +1872,9 @@ Dygraph.prototype.parseArray_ = function(data) {
         return null;
       }
       if (parsedData[i][0] == null
-          || typeof(parsedData[i][0].getTime) != 'function') {
-        this.error("x value in row " << (1 + i) << " is not a Date");
+          || typeof(parsedData[i][0].getTime) != 'function'
+          || isNaN(parsedData[i][0].getTime())) {
+        this.error("x value in row " + (1 + i) + " is not a Date");
         return null;
       }
       parsedData[i][0] = parsedData[i][0].getTime();