Add test for stacked graph.
[dygraphs.git] / dygraph.js
index ff70bcb..8cfa678 100644 (file)
@@ -92,6 +92,9 @@ Dygraph.DEFAULT_ATTRS = {
   labelsSeparateLines: false,
   labelsKMB: false,
   labelsKMG2: false,
+  staticLabels: false,
+
+  yValueFormatter: null,
 
   strokeWidth: 1.0,
 
@@ -108,12 +111,17 @@ Dygraph.DEFAULT_ATTRS = {
 
   delimiter: ',',
 
+  logScale: false,
   sigma: 2.0,
   errorBars: false,
   fractions: false,
   wilsonInterval: true,  // only relevant if fractions is true
   customBars: false,
-  fillGraph: false
+  fillGraph: false,
+  fillAlpha: 0.15,
+
+  stackedGraph: false,
+  hideOverlayOnMouseOut: true
 };
 
 // Various logging levels.
@@ -138,8 +146,8 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
  * and interaction <canvas> inside of it. See the constructor for details
  * on the parameters.
+ * @param {Element} div the Element to render the graph into.
  * @param {String | Function} file Source data
- * @param {Array.<String>} labels Names of the data series
  * @param {Object} attrs Miscellaneous other options
  * @private
  */
@@ -162,20 +170,35 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // div, then only one will be drawn.
   div.innerHTML = "";
 
-  // If the div isn't already sized then give it a default size.
+  // If the div isn't already sized then inherit from our attrs or
+  // give it a default size.
   if (div.style.width == '') {
-    div.style.width = Dygraph.DEFAULT_WIDTH + "px";
+    div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
   }
   if (div.style.height == '') {
-    div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
+    div.style.height = attrs.height || Dygraph.DEFAULT_HEIGHT + "px";
   }
   this.width_ = parseInt(div.style.width, 10);
   this.height_ = parseInt(div.style.height, 10);
+  // 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;
+  }
+  if (div.style.height.indexOf("%") == div.style.height.length - 1) {
+    this.height_ = (this.height_ * self.innerHeight / 100) - 10;
+  }
+
+  if (attrs['stackedGraph']) {
+    attrs['fillGraph'] = true;
+    // TODO(nikhilk): Add any other stackedGraph checks here.
+  }
 
   // Dygraphs has many options, some of which interact with one another.
   // To keep track of everything, we maintain two sets of options:
   //
-  //  this.user_attrs_   only options explicitly set by the user. 
+  //  this.user_attrs_   only options explicitly set by the user.
   //  this.attrs_        defaults, options derived from user_attrs_, data.
   //
   // Options are then accessed this.attr_('attr'), which first looks at
@@ -386,23 +409,36 @@ Dygraph.prototype.setColors_ = function() {
     var sat = this.attr_('colorSaturation') || 1.0;
     var val = this.attr_('colorValue') || 0.5;
     for (var i = 1; i <= num; i++) {
-      var hue = (1.0*i/(1+num));
-      this.colors_.push( Dygraph.hsvToRGB(hue, sat, val) );
+      if (!this.visibility()[i-1]) continue;
+      // alternate colors for high contrast.
+      var idx = i - parseInt(i % 2 ? i / 2 : (i - num)/2, 10);
+      var hue = (1.0 * idx/ (1 + num));
+      this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
     }
   } else {
     for (var i = 0; i < num; i++) {
+      if (!this.visibility()[i]) continue;
       var colorStr = colors[i % colors.length];
       this.colors_.push(colorStr);
     }
   }
 
-  // TODO(danvk): update this w/r/t/ the new options system. 
+  // TODO(danvk): update this w/r/t/ the new options system.
   this.renderOptions_.colorScheme = this.colors_;
   Dygraph.update(this.plotter_.options, this.renderOptions_);
   Dygraph.update(this.layoutOptions_, this.user_attrs_);
   Dygraph.update(this.layoutOptions_, this.attrs_);
 }
 
+/**
+ * Return the list of colors. This is either the list of colors passed in the
+ * attributes, or the autogenerated list of rgb(r,g,b) strings.
+ * @return {Array<string>} The list of colors.
+ */
+Dygraph.prototype.getColors = function() {
+  return this.colors_;
+};
+
 // The following functions are from quirksmode.org
 // http://www.quirksmode.org/js/findpos.html
 Dygraph.findPosX = function(obj) {
@@ -417,7 +453,7 @@ Dygraph.findPosX = function(obj) {
     curleft += obj.x;
   return curleft;
 };
-                   
+
 Dygraph.findPosY = function(obj) {
   var curtop = 0;
   if (obj.offsetParent) {
@@ -749,7 +785,21 @@ Dygraph.prototype.mouseMove_ = function(event) {
   this.selPoints_ = [];
   for (var i = 0; i < points.length; i++) {
     if (points[i].xval == lastx) {
-      this.selPoints_.push(points[i]);
+      // Clone the point.
+      var p = {};
+      for (var k in points[i]) {
+        p[k] = points[i][k];
+      }
+      this.selPoints_.push(p);
+    }
+  }
+
+  if (this.attr_("stackedGraph")) {
+    // "unstack" the points.
+    var cumulative_sum = 0;
+    for (var j = this.selPoints_.length - 1; j >= 0; j--) {
+      this.selPoints_[j].yval -= cumulative_sum;
+      cumulative_sum += this.selPoints_[j].yval;
     }
   }
 
@@ -767,11 +817,12 @@ Dygraph.prototype.mouseMove_ = function(event) {
 
   var isOK = function(x) { return x && !isNaN(x); };
 
-  if (this.selPoints_.length > 0) {
+  if (this.selPoints_.length > 0 && !this.attr_('staticLabels')) {
     var canvasx = this.selPoints_[0].canvasx;
 
     // Set the status message to indicate the selected point(s)
     var replace = this.attr_('xValueFormatter')(lastx, this) + ":";
+    var fmtFunc = this.attr_('yValueFormatter');
     var clen = this.colors_.length;
     for (var i = 0; i < this.selPoints_.length; i++) {
       if (!isOK(this.selPoints_[i].canvasy)) continue;
@@ -780,9 +831,10 @@ Dygraph.prototype.mouseMove_ = function(event) {
       }
       var point = this.selPoints_[i];
       var c = new RGBColor(this.colors_[i%clen]);
+      var yval = fmtFunc ? fmtFunc(point.yval) : this.round_(point.yval, 2);
       replace += " <b><font color='" + c.toHex() + "'>"
               + point.name + "</font></b>:"
-              + this.round_(point.yval, 2);
+              + yval;
     }
     this.attr_("labelsDiv").innerHTML = replace;
 
@@ -790,7 +842,7 @@ Dygraph.prototype.mouseMove_ = function(event) {
     this.lastx_ = lastx;
 
     // Draw colored circles over the center of each selected point
-    ctx.save()
+    ctx.save();
     for (var i = 0; i < this.selPoints_.length; i++) {
       if (!isOK(this.selPoints_[i%clen].canvasy)) continue;
       ctx.beginPath();
@@ -811,10 +863,12 @@ Dygraph.prototype.mouseMove_ = function(event) {
  * @private
  */
 Dygraph.prototype.mouseOut_ = function(event) {
-  // Get rid of the overlay data
-  var ctx = this.canvas_.getContext("2d");
-  ctx.clearRect(0, 0, this.width_, this.height_);
-  this.attr_("labelsDiv").innerHTML = "";
+  if (this.attr_("hideOverlayOnMouseOut")) {
+    // Get rid of the overlay data
+    var ctx = this.canvas_.getContext("2d");
+    ctx.clearRect(0, 0, this.width_, this.height_);
+    this.attr_("labelsDiv").innerHTML = "";
+  }
 };
 
 Dygraph.zeropad = function(x) {
@@ -1102,6 +1156,7 @@ Dygraph.numericTicks = function(minV, maxV, self) {
   // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
   // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
   // The first spacing greater than pixelsPerYLabel is what we use.
+  // TODO(danvk): version that works on a log scale.
   if (self.attr_("labelsKMG2")) {
     var mults = [1, 2, 4, 8];
   } else {
@@ -1229,7 +1284,12 @@ Dygraph.prototype.drawGraph_ = function(data) {
   this.setColors_();
   this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
 
+  // For stacked series.
+  var cumulative_y = [];
+  var datasets = [];
+
   // Loop over all fields in the dataset
+
   for (var i = 1; i < data[0].length; i++) {
     if (!this.visibility()[i - 1]) continue;
 
@@ -1266,18 +1326,49 @@ Dygraph.prototype.drawGraph_ = function(data) {
         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);
+    } 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;
+
+        actual_y = series[j][1];
+        cumulative_y[series[j][0]] += actual_y;
+
+        vals[j] = [series[j][0], cumulative_y[series[j][0]]]
+
+        if (!maxY || cumulative_y[series[j][0]] > maxY)
+          maxY = cumulative_y[series[j][0]];
+      }
+      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]);
+    }
+  }
+
   // Use some heuristics to come up with a good maxY value, unless it's been
   // set explicitly by the user.
   if (this.valueRange_ != null) {
     this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
   } else {
+    // This affects the calculation of span, below.
+    if (this.attr_("includeZero") && minY > 0) {
+      minY = 0;
+    }
+
     // Add some padding and round up to an integer to be human-friendly.
     var span = maxY - minY;
+    // special case: if we have no sense of scale, use +/-10% of the sole value.
+    if (span == 0) { span = maxY; }
     var maxAxisY = maxY + 0.1 * span;
     var minAxisY = minY - 0.1 * span;
 
@@ -1537,6 +1628,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
   var xParser;
   var defaultParserSet = false;  // attempt to auto-detect x value type
   var expectedCols = this.attr_("labels").length;
+  var outOfOrder = false;
   for (var i = start; i < lines.length; i++) {
     var line = lines[i];
     if (line.length == 0) continue;  // skip blank lines
@@ -1578,6 +1670,9 @@ Dygraph.prototype.parseCSV_ = function(data) {
         fields[j] = parseFloat(inFields[j]);
       }
     }
+    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
     ret.push(fields);
 
     if (fields.length != expectedCols) {
@@ -1586,6 +1681,12 @@ Dygraph.prototype.parseCSV_ = function(data) {
                  ") " + line);
     }
   }
+
+  if (outOfOrder) {
+    this.warn("CSV is out of order; order it correctly to speed loading.");
+    ret.sort(function(a,b) { return a[0] - b[0] });
+  }
+
   return ret;
 };
 
@@ -1667,7 +1768,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   cols = labels.length;
 
   var indepType = data.getColumnType(0);
-  if (indepType == 'date') {
+  if (indepType == 'date' || indepType == 'datetime') {
     this.attrs_.xValueFormatter = Dygraph.dateString_;
     this.attrs_.xValueParser = Dygraph.dateParser;
     this.attrs_.xTicker = Dygraph.dateTicker;
@@ -1676,12 +1777,13 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
     this.attrs_.xTicker = Dygraph.numericTicks;
   } else {
-    this.error("only 'date' and 'number' types are supported for column 1 " +
-               "of DataTable input (Got '" + indepType + "')");
+    this.error("only 'date', 'datetime' and 'number' types are supported for " +
+               "column 1 of DataTable input (Got '" + indepType + "')");
     return null;
   }
 
   var ret = [];
+  var outOfOrder = false;
   for (var i = 0; i < rows; i++) {
     var row = [];
     if (typeof(data.getValue(i, 0)) === 'undefined' ||
@@ -1691,7 +1793,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
       continue;
     }
 
-    if (indepType == 'date') {
+    if (indepType == 'date' || indepType == 'datetime') {
       row.push(data.getValue(i, 0).getTime());
     } else {
       row.push(data.getValue(i, 0));
@@ -1705,8 +1807,16 @@ Dygraph.prototype.parseDataTable_ = function(data) {
         row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
       }
     }
+    if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
     ret.push(row);
   }
+
+  if (outOfOrder) {
+    this.warn("DataTable is out of order; order it correctly to speed loading.");
+    ret.sort(function(a,b) { return a[0] - b[0] });
+  }
   return ret;
 }
 
@@ -1725,7 +1835,7 @@ Dygraph.update = function (self, o) {
 Dygraph.isArrayLike = function (o) {
   var typ = typeof(o);
   if (
-      (typ != 'object' && !(typ == 'function' && 
+      (typ != 'object' && !(typ == 'function' &&
         typeof(o.item) == 'function')) ||
       o === null ||
       typeof(o.length) != 'number' ||
@@ -1884,10 +1994,10 @@ Dygraph.prototype.visibility = function() {
   // Do lazy-initialization, so that this happens after we know the number of
   // data series.
   if (!this.attr_("visibility")) {
-    this.attr_["visibility"] = [];
+    this.attrs_["visibility"] = [];
   }
   while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
-    this.attr_("visibility").push(false);
+    this.attr_("visibility").push(true);
   }
   return this.attr_("visibility");
 };