Fix problems with multiple null values in a row for stepped graphs.
[dygraphs.git] / dygraph.js
index 1bf5e05..ee08802 100644 (file)
@@ -102,6 +102,7 @@ Dygraph.DEFAULT_ATTRS = {
   axisLabelFontSize: 14,
   xAxisLabelWidth: 50,
   yAxisLabelWidth: 50,
+  xAxisLabelFormatter: Dygraph.dateAxisFormatter,
   rightGap: 5,
 
   showRoller: false,
@@ -122,7 +123,9 @@ Dygraph.DEFAULT_ATTRS = {
   connectSeparatedPoints: false,
 
   stackedGraph: false,
-  hideOverlayOnMouseOut: true
+  hideOverlayOnMouseOut: true,
+
+  stepPlot: false
 };
 
 // Various logging levels.
@@ -185,11 +188,17 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // The div might have been specified as percent of the current window size,
   // convert that to an appropriate number of pixels.
   if (div.style.width.indexOf("%") == div.style.width.length - 1) {
-    // Minus ten pixels  keeps scrollbars from showing up for a 100% width div.
-    this.width_ = (this.width_ * self.innerWidth / 100) - 10;
+    this.width_ = div.offsetWidth;
   }
   if (div.style.height.indexOf("%") == div.style.height.length - 1) {
-    this.height_ = (this.height_ * self.innerHeight / 100) - 10;
+    this.height_ = div.offsetHeight;
+  }
+
+  if (this.width_ == 0) {
+    this.error("dygraph has zero width. Please specify a width in pixels.");
+  }
+  if (this.height_ == 0) {
+    this.error("dygraph has zero height. Please specify a height in pixels.");
   }
 
   // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
@@ -386,20 +395,24 @@ Dygraph.prototype.createInterface_ = function() {
   this.canvas_.height = this.height_;
   this.canvas_.style.width = this.width_ + "px";    // for IE
   this.canvas_.style.height = this.height_ + "px";  // for IE
-  this.graphDiv.appendChild(this.canvas_);
 
   // ... and for static parts of the chart.
   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
 
+  // The interactive parts of the graph are drawn on top of the chart.
+  this.graphDiv.appendChild(this.hidden_);
+  this.graphDiv.appendChild(this.canvas_);
+  this.mouseEventElement_ = 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.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
     dygraph.mouseMove_(e);
   });
-  Dygraph.addEvent(this.hidden_, 'mouseout', function(e) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
     dygraph.mouseOut_(e);
   });
 
@@ -475,7 +488,6 @@ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
   h.height = this.height_;
   h.style.width = this.width_ + "px";    // for IE
   h.style.height = this.height_ + "px";  // for IE
-  this.graphDiv.appendChild(h);
   return h;
 };
 
@@ -706,7 +718,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   var getY = function(e) { return Dygraph.pageX(e) - py };
 
   // Draw zoom rectangles when the mouse is down and the user moves around
-  Dygraph.addEvent(this.hidden_, 'mousemove', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
     if (isZooming) {
       dragEndX = getX(event);
       dragEndY = getY(event);
@@ -728,7 +740,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   });
 
   // Track the beginning of drag events
-  Dygraph.addEvent(this.hidden_, 'mousedown', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
     px = Dygraph.findPosX(self.canvas_);
     py = Dygraph.findPosY(self.canvas_);
     dragStartX = getX(event);
@@ -762,7 +774,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   });
 
   // Temporarily cancel the dragging event when the mouse leaves the graph
-  Dygraph.addEvent(this.hidden_, 'mouseout', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
     if (isZooming) {
       dragEndX = null;
       dragEndY = null;
@@ -771,7 +783,7 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   // If the mouse is released on the canvas during a drag event, then it's a
   // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
-  Dygraph.addEvent(this.hidden_, 'mouseup', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
     if (isZooming) {
       isZooming = false;
       dragEndX = getX(event);
@@ -807,7 +819,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   });
 
   // Double-clicking zooms back out
-  Dygraph.addEvent(this.hidden_, 'dblclick', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
     if (self.dateWindow_ == null) return;
     self.dateWindow_ = null;
     self.drawGraph_(self.rawData_);
@@ -878,7 +890,7 @@ Dygraph.prototype.doZoom_ = function(lowX, highX) {
  * @private
  */
 Dygraph.prototype.mouseMove_ = function(event) {
-  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.hidden_);
+  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
   var points = this.layout_.points;
 
   var lastx = -1;
@@ -901,9 +913,7 @@ Dygraph.prototype.mouseMove_ = function(event) {
 
   // Extract the points we've selected
   this.selPoints_ = [];
-  var cumulative_sum = 0;  // used only if we have a stackedGraph.
   var l = points.length;
-  var isStacked = this.attr_("stackedGraph");
   if (!this.attr_("stackedGraph")) {
     for (var i = 0; i < l; i++) {
       if (points[i].xval == lastx) {
@@ -911,11 +921,11 @@ Dygraph.prototype.mouseMove_ = function(event) {
       }
     }
   } else {
-    // Stacked points need to be examined in reverse order.
+    // Need to 'unstack' points starting from the bottom
+    var cumulative_sum = 0;
     for (var i = l - 1; i >= 0; i--) {
       if (points[i].xval == lastx) {
-        // Clone the point, since we need to 'unstack' it below.
-        var p = {};
+        var p = {};  // Clone the point since we modify it
         for (var k in points[i]) {
           p[k] = points[i][k];
         }
@@ -924,13 +934,13 @@ Dygraph.prototype.mouseMove_ = function(event) {
         this.selPoints_.push(p);
       }
     }
+    this.selPoints_.reverse();
   }
 
   if (this.attr_("highlightCallback")) {
-    var px = this.lastHighlightCallbackX;
+    var px = this.lastx_;
     if (px !== null && lastx != px) {
       // only fire if the selected point has changed.
-      this.lastHighlightCallbackX = lastx;
       this.attr_("highlightCallback")(event, lastx, this.selPoints_);
     }
   }
@@ -1039,6 +1049,10 @@ Dygraph.prototype.setSelection = function(row) {
  * @private
  */
 Dygraph.prototype.mouseOut_ = function(event) {
+  if (this.attr_("unhighlightCallback")) {
+    this.attr_("unhighlightCallback")(event);
+  }
+
   if (this.attr_("hideOverlayOnMouseOut")) {
     this.clearSelection();
   }
@@ -1085,7 +1099,7 @@ Dygraph.zeropad = function(x) {
  * @return {String} A time of the form "HH:MM:SS"
  * @private
  */
-Dygraph.prototype.hmsString_ = function(date) {
+Dygraph.hmsString_ = function(date) {
   var zeropad = Dygraph.zeropad;
   var d = new Date(date);
   if (d.getSeconds()) {
@@ -1098,11 +1112,31 @@ Dygraph.prototype.hmsString_ = function(date) {
 }
 
 /**
+ * Convert a JS date to a string appropriate to display on an axis that
+ * is displaying values at the stated granularity.
+ * @param {Date} date The date to format
+ * @param {Number} granularity One of the Dygraph granularity constants
+ * @return {String} The formatted date
+ * @private
+ */
+Dygraph.dateAxisFormatter = function(date, granularity) {
+  if (granularity >= Dygraph.MONTHLY) {
+    return date.strftime('%b %y');
+  } else {
+    var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
+    if (frac == 0 || granularity >= Dygraph.DAILY) {
+      return new Date(date.getTime() + 3600*1000).strftime('%d%b');
+    } else {
+      return Dygraph.hmsString_(date.getTime());
+    }
+  }
+}
+
+/**
  * Convert a JS date (millis since epoch) to YYYY/MM/DD
  * @param {Number} date The JavaScript date (ms since epoch)
  * @return {String} A date of the form "YYYY/MM/DD"
  * @private
- * TODO(danvk): why is this part of the prototype?
  */
 Dygraph.dateString_ = function(date, self) {
   var zeropad = Dygraph.zeropad;
@@ -1117,7 +1151,7 @@ Dygraph.dateString_ = function(date, self) {
 
   var ret = "";
   var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-  if (frac) ret = " " + self.hmsString_(date);
+  if (frac) ret = " " + Dygraph.hmsString_(date);
 
   return year + "/" + month + "/" + day + ret;
 };
@@ -1239,6 +1273,7 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
 //   Returns an array containing {v: millis, label: label} dictionaries.
 //
 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
+  var formatter = this.attr_("xAxisLabelFormatter");
   var ticks = [];
   if (granularity < Dygraph.MONTHLY) {
     // Generate one tick mark for every fixed interval of time.
@@ -1275,14 +1310,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
     start_time = d.getTime();
 
     for (var t = start_time; t <= end_time; t += spacing) {
-      var d = new Date(t);
-      var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-      if (frac == 0 || granularity >= Dygraph.DAILY) {
-        // the extra hour covers DST problems.
-        ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) });
-      } else {
-        ticks.push({ v:t, label: this.hmsString_(t) });
-      }
+      ticks.push({ v:t, label: formatter(new Date(t), granularity) });
     }
   } else {
     // Display a tick mark on the first of a set of months of each year.
@@ -1313,7 +1341,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
         var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
         var t = Date.parse(date_str);
         if (t < start_time || t > end_time) continue;
-        ticks.push({ v:t, label: new Date(t).strftime('%b %y') });
+        ticks.push({ v:t, label: formatter(new Date(t), granularity) });
       }
     }
   }
@@ -1350,10 +1378,12 @@ Dygraph.dateTicker = function(startDate, endDate, self) {
  * Add ticks when the x axis has numbers on it (instead of dates)
  * @param {Number} startDate Start of the date window (millis since epoch)
  * @param {Number} endDate End of the date window (millis since epoch)
+ * @param self
+ * @param {function} formatter: Optional formatter to use for each tick value
  * @return {Array.<Object>} Array of {label, value} tuples.
  * @public
  */
-Dygraph.numericTicks = function(minV, maxV, self) {
+Dygraph.numericTicks = function(minV, maxV, self, formatter) {
   // Basic idea:
   // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
   // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
@@ -1405,7 +1435,12 @@ Dygraph.numericTicks = function(minV, maxV, self) {
   for (var i = 0; i < nTicks; i++) {
     var tickV = low_val + i * scale;
     var absTickV = Math.abs(tickV);
-    var label = Dygraph.round_(tickV, 2);
+    var label;
+    if (formatter != undefined) {
+      label = formatter(tickV);
+    } else {
+      label = Dygraph.round_(tickV, 2);
+    }
     if (k_labels.length) {
       // Round up to an appropriate unit.
       var n = k*k*k*k;
@@ -1430,7 +1465,8 @@ Dygraph.numericTicks = function(minV, maxV, self) {
 Dygraph.prototype.addYTicks_ = function(minY, maxY) {
   // Set the number of ticks so that the labels are human-friendly.
   // TODO(danvk): make this an attribute as well.
-  var ticks = Dygraph.numericTicks(minY, maxY, this);
+  var formatter = this.attr_('yAxisLabelFormatter') ? this.attr_('yAxisLabelFormatter') : this.attr_('yValueFormatter');
+  var ticks = Dygraph.numericTicks(minY, maxY, this, formatter);
   this.layout_.updateOptions( { yAxis: [minY, maxY],
                                 yTicks: ticks } );
 };
@@ -1495,17 +1531,19 @@ Dygraph.prototype.drawGraph_ = function(data) {
 
   var connectSeparatedPoints = this.attr_('connectSeparatedPoints');
 
-  // For stacked series.
-  var cumulative_y = [];
-  var stacked_datasets = [];
+  // Loop over the fields (series).  Go from the last to the first,
+  // because if they're stacked that's how we accumulate the values.
+
+  var cumulative_y = [];  // For stacked series.
+  var datasets = [];
 
-  // Loop over all fields in the dataset
-  for (var i = 1; i < data[0].length; i++) {
+  // Loop over all fields and create datasets
+  for (var i = data[0].length - 1; i >= 1; i--) {
     if (!this.visibility()[i - 1]) continue;
 
     var series = [];
     for (var j = 0; j < data.length; j++) {
-      if (data[j][i] || !connectSeparatedPoints) {
+      if (data[j][i] != null || !connectSeparatedPoints) {
         var date = data[j][0];
         series.push([date, data[j][i]]);
       }
@@ -1547,42 +1585,39 @@ Dygraph.prototype.drawGraph_ = function(data) {
     var extremes = this.extremeValues_(series);
     var thisMinY = extremes[0];
     var thisMaxY = extremes[1];
-    if (!minY || thisMinY < minY) minY = thisMinY;
-    if (!maxY || thisMaxY > maxY) maxY = thisMaxY;
+    if (minY === null || thisMinY < minY) minY = thisMinY;
+    if (maxY === null || thisMaxY > maxY) maxY = thisMaxY;
 
     if (bars) {
-      var vals = [];
-      for (var j=0; j<series.length; j++)
-        vals[j] = [series[j][0],
-                   series[j][1][0], series[j][1][1], series[j][1][2]];
-      this.layout_.addDataset(this.attr_("labels")[i], vals);
+      for (var j=0; j<series.length; j++) {
+        val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
+        series[j] = val;
+      }
     } else if (this.attr_("stackedGraph")) {
-      var vals = [];
       var l = series.length;
       var actual_y;
       for (var j = 0; j < l; j++) {
-        if (cumulative_y[series[j][0]] === undefined)
-          cumulative_y[series[j][0]] = 0;
+        // If one data set has a NaN, let all subsequent stacked
+        // sets inherit the NaN -- only start at 0 for the first set.
+        var x = series[j][0];
+        if (cumulative_y[x] === undefined)
+          cumulative_y[x] = 0;
 
         actual_y = series[j][1];
-        cumulative_y[series[j][0]] += actual_y;
+        cumulative_y[x] += actual_y;
 
-        vals[j] = [series[j][0], cumulative_y[series[j][0]]]
+        series[j] = [x, cumulative_y[x]]
 
-        if (!maxY || cumulative_y[series[j][0]] > maxY)
-          maxY = cumulative_y[series[j][0]];
+        if (!maxY || cumulative_y[x] > maxY)
+          maxY = cumulative_y[x];
       }
-      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);
     }
+
+    datasets[i] = series;
   }
 
-  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]);
-    }
+  for (var i = 1; i < datasets.length; i++) {
+    this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
   }
 
   // Use some heuristics to come up with a good maxY value, unless it's been
@@ -1821,10 +1856,12 @@ Dygraph.prototype.detectTypeFromString_ = function(str) {
     this.attrs_.xValueFormatter = Dygraph.dateString_;
     this.attrs_.xValueParser = Dygraph.dateParser;
     this.attrs_.xTicker = Dygraph.dateTicker;
+    this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
   } else {
     this.attrs_.xValueFormatter = function(x) { return x; };
     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
     this.attrs_.xTicker = Dygraph.numericTicks;
+    this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
   }
 };
 
@@ -1956,6 +1993,7 @@ Dygraph.prototype.parseArray_ = function(data) {
   if (Dygraph.isDateLike(data[0][0])) {
     // Some intelligent defaults for a date x-axis.
     this.attrs_.xValueFormatter = Dygraph.dateString_;
+    this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
     this.attrs_.xTicker = Dygraph.dateTicker;
 
     // Assume they're all dates.
@@ -2009,10 +2047,12 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     this.attrs_.xValueFormatter = Dygraph.dateString_;
     this.attrs_.xValueParser = Dygraph.dateParser;
     this.attrs_.xTicker = Dygraph.dateTicker;
+    this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
   } else if (indepType == 'number') {
     this.attrs_.xValueFormatter = function(x) { return x; };
     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
     this.attrs_.xTicker = Dygraph.numericTicks;
+    this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
   } else {
     this.error("only 'date', 'datetime' and 'number' types are supported for " +
                "column 1 of DataTable input (Got '" + indepType + "')");
@@ -2190,6 +2230,11 @@ Dygraph.prototype.updateOptions = function(attrs) {
  * @param {Number} height Height (in pixels)
  */
 Dygraph.prototype.resize = function(width, height) {
+  if (this.resize_lock) {
+    return;
+  }
+  this.resize_lock = true;
+
   if ((width === null) != (height === null)) {
     this.warn("Dygraph.resize() should be called with zero parameters or " +
               "two non-NULL parameters. Pretending it was zero.");
@@ -2212,6 +2257,8 @@ Dygraph.prototype.resize = function(width, height) {
 
   this.createInterface_();
   this.drawGraph_(this.rawData_);
+
+  this.resize_lock = false;
 };
 
 /**