nit: s/undefined/null
[dygraphs.git] / dygraph.js
index 2ae8cd9..375f9c9 100644 (file)
@@ -135,6 +135,11 @@ Dygraph.INFO = 2;
 Dygraph.WARNING = 3;
 Dygraph.ERROR = 3;
 
+// Directions for panning and zooming. Use bit operations when combined
+// values are possible.
+Dygraph.HORIZONTAL = 1;
+Dygraph.VERTICAL = 2;
+
 // Used for initializing annotation CSS rules only once.
 Dygraph.addedAnnotationCSS = false;
 
@@ -175,8 +180,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // locally-stored copy of the attribute. valueWindow starts off the same as
   // valueRange but is impacted by zoom or pan effects. valueRange is kept
   // around to restore the original value back to valueRange.
-  // TODO(konigsberg): There are no vertical pan effects yet, but valueWindow
-  // would change accordingly.
   this.valueRange_ = attrs.valueRange || null;
   this.valueWindow_ = this.valueRange_;
 
@@ -248,8 +251,13 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.start_();
 };
 
-Dygraph.prototype.attr_ = function(name) {
-  if (typeof(this.user_attrs_[name]) != 'undefined') {
+Dygraph.prototype.attr_ = function(name, seriesName) {
+  if (seriesName &&
+      typeof(this.user_attrs_[seriesName]) != 'undefined' &&
+      this.user_attrs_[seriesName] != null &&
+      typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
+    return this.user_attrs_[seriesName][name];
+  } else if (typeof(this.user_attrs_[name]) != 'undefined') {
     return this.user_attrs_[name];
   } else if (typeof(this.attrs_[name]) != 'undefined') {
     return this.attrs_[name];
@@ -753,11 +761,22 @@ Dygraph.prototype.createDragInterface_ = function() {
   var dragStartY = null;
   var dragEndX = null;
   var dragEndY = null;
+  var dragDirection = null;
   var prevEndX = null;
   var prevEndY = null;
   var prevDragDirection = null;
+
+  // draggingDate and draggingValue represent the [date,value] point on the
+  // graph at which the mouse was pressed. As the mouse moves while panning,
+  // the viewport must pan so that the mouse position points to
+  // [draggingDate, draggingValue]
   var draggingDate = null;
+  var draggingValue = null;
+
+  // The range in second/value units that the viewport encompasses during a
+  // panning operation.
   var dateRange = null;
+  var valueRange = null;
 
   // Utility function to convert page-wide coordinates to canvas coords
   var px = 0;
@@ -773,7 +792,9 @@ Dygraph.prototype.createDragInterface_ = function() {
 
       var xDelta = Math.abs(dragStartX - dragEndX);
       var yDelta = Math.abs(dragStartY - dragEndY);
-      var dragDirection = (xDelta < yDelta) ? "V" : "H";
+
+      // drag direction threshold for y axis is twice as large as x axis
+      dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
 
       self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY,
                          prevDragDirection, prevEndX, prevEndY);
@@ -786,11 +807,24 @@ Dygraph.prototype.createDragInterface_ = function() {
       dragEndY = getY(event);
 
       // Want to have it so that:
-      // 1. draggingDate appears at dragEndX
+      // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
       // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
+      // 3. draggingValue appears at dragEndY.
+      // 4. valueRange is unaltered.
 
-      self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
-      self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
+      var minDate = draggingDate - (dragEndX / self.width_) * dateRange;
+      var maxDate = minDate + dateRange;
+      self.dateWindow_ = [minDate, maxDate];
+
+
+      // y-axis scaling is automatic unless a valueRange is defined or
+      // if the user zooms in on the y-axis. If neither is true, valueWindow_
+      // will be null.
+      if (self.valueWindow_) {
+        var maxValue = draggingValue + (dragEndY / self.height_) * valueRange;
+        var minValue = maxValue - valueRange;
+        self.valueWindow_ = [ minValue, maxValue ];
+      }
       self.drawGraph_(self.rawData_);
     }
   });
@@ -803,12 +837,21 @@ Dygraph.prototype.createDragInterface_ = function() {
     dragStartY = getY(event);
 
     if (event.altKey || event.shiftKey) {
-      // TODO(konigsberg): Support vertical panning.
-      if (!self.dateWindow_) return;  // have to be zoomed in to pan.
+      // have to be zoomed in to pan.
+      if (!self.dateWindow_ && !self.valueWindow_) return;
+
       isPanning = true;
-      dateRange = self.dateWindow_[1] - self.dateWindow_[0];
+      var xRange = self.xAxisRange();
+      dateRange = xRange[1] - xRange[0];
+      var yRange = self.yAxisRange();
+      valueRange = yRange[1] - yRange[0];
+
+      // TODO(konigsberg): Switch from all this math to toDataCoords?
+      // Seems to work for the dragging value.
       draggingDate = (dragStartX / self.width_) * dateRange +
-        self.dateWindow_[0];
+        xRange[0];
+      var r = self.toDataCoords(null, dragStartY);
+      draggingValue = r[1];
     } else {
       isZooming = true;
     }
@@ -826,7 +869,9 @@ Dygraph.prototype.createDragInterface_ = function() {
     if (isPanning) {
       isPanning = false;
       draggingDate = null;
+      draggingValue = null;
       dateRange = null;
+      valueRange = null;
     }
   });
 
@@ -876,10 +921,10 @@ Dygraph.prototype.createDragInterface_ = function() {
         }
       }
 
-      if (regionWidth >= 10 && regionWidth > regionHeight) {
+      if (regionWidth >= 10 && dragDirection == Dygraph.HORIZONTAL) {
         self.doZoomX_(Math.min(dragStartX, dragEndX),
                      Math.max(dragStartX, dragEndX));
-      } else if (regionHeight >= 10 && regionHeight > regionWidth){
+      } else if (regionHeight >= 10 && dragDirection == Dygraph.VERTICAL){
         self.doZoomY_(Math.min(dragStartY, dragEndY),
                       Math.max(dragStartY, dragEndY));
       } else {
@@ -895,7 +940,9 @@ Dygraph.prototype.createDragInterface_ = function() {
     if (isPanning) {
       isPanning = false;
       draggingDate = null;
+      draggingValue = null;
       dateRange = null;
+      valueRange = null;
     }
   });
 
@@ -914,15 +961,15 @@ Dygraph.prototype.createDragInterface_ = function() {
  * avoid extra redrawing, but it's tricky to avoid interactions with the status
  * dots.
  * 
- * @param {String} direction the direction of the zoom rectangle. "H" and "V"
- * for Horizontal and Vertical.
+ * @param {Number} direction the direction of the zoom rectangle. Acceptable
+ * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
  * @param {Number} startX The X position where the drag started, in canvas
  * coordinates.
  * @param {Number} endX The current X position of the drag, in canvas coords.
  * @param {Number} startY The Y position where the drag started, in canvas
  * coordinates.
  * @param {Number} endY The current Y position of the drag, in canvas coords.
- * @param {String} prevDirection the value of direction on the previous call to
+ * @param {Number} prevDirection the value of direction on the previous call to
  * this function. Used to avoid excess redrawing
  * @param {Number} prevEndX The value of endX on the previous call to this
  * function. Used to avoid excess redrawing
@@ -935,23 +982,23 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY
   var ctx = this.canvas_.getContext("2d");
 
   // Clean up from the previous rect if necessary
-  if (prevDirection == "H") {
+  if (prevDirection == Dygraph.HORIZONTAL) {
     ctx.clearRect(Math.min(startX, prevEndX), 0,
                   Math.abs(startX - prevEndX), this.height_);
-  } else if (prevDirection == "V"){
+  } else if (prevDirection == Dygraph.VERTICAL){
     ctx.clearRect(0, Math.min(startY, prevEndY),
                   this.width_, Math.abs(startY - prevEndY));
   }
 
   // Draw a light-grey rectangle to show the new viewing area
-  if (direction == "H") {
+  if (direction == Dygraph.HORIZONTAL) {
     if (endX && startX) {
       ctx.fillStyle = "rgba(128,128,128,0.33)";
       ctx.fillRect(Math.min(startX, endX), 0,
                    Math.abs(endX - startX), this.height_);
     }
   }
-  if (direction == "V") {
+  if (direction == Dygraph.VERTICAL) {
     if (endY && startY) {
       ctx.fillStyle = "rgba(128,128,128,0.33)";
       ctx.fillRect(0, Math.min(startY, endY),
@@ -1012,9 +1059,9 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
   // Find the highest and lowest values in pixel range.
   var r = this.toDataCoords(null, lowY);
-  var minValue = r[1];
-  r = this.toDataCoords(null, highY);
   var maxValue = r[1];
+  r = this.toDataCoords(null, highY);
+  var minValue = r[1];
 
   this.doZoomYValues_(minValue, maxValue);
 };
@@ -1029,7 +1076,7 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) {
  * @private
  */
 Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) {
-  this.valueWindow_ = [maxValue, minValue];
+  this.valueWindow_ = [minValue, maxValue];
   this.drawGraph_(this.rawData_);
   if (this.attr_("zoomCallback")) {
     var xRange = this.xAxisRange(); 
@@ -1055,14 +1102,16 @@ Dygraph.prototype.doUnzoom_ = function() {
   }
 
   if (dirty) {
+    // Putting the drawing operation before the callback because it resets
+    // yAxisRange.
+    this.drawGraph_(this.rawData_);
     if (this.attr_("zoomCallback")) {
       var minDate = this.rawData_[0][0];
       var maxDate = this.rawData_[this.rawData_.length - 1][0];
-      var minValue = this.xAxisRange()[0];
-      var maxValue = this.xAxisRange()[1];
+      var minValue = this.yAxisRange()[0];
+      var maxValue = this.yAxisRange()[1];
       this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue);
     }
-    this.drawGraph_(this.rawData_);
   }
 };
 
@@ -1142,11 +1191,18 @@ Dygraph.prototype.mouseMove_ = function(event) {
  */
 Dygraph.prototype.updateSelection_ = function() {
   // Clear the previously drawn vertical, if there is one
-  var circleSize = this.attr_('highlightCircleSize');
   var ctx = this.canvas_.getContext("2d");
   if (this.previousVerticalX_ >= 0) {
+    // Determine the maximum highlight circle size.
+    var maxCircleSize = 0;
+    var labels = this.attr_('labels');
+    for (var i = 1; i < labels.length; i++) {
+      var r = this.attr_('highlightCircleSize', labels[i]);
+      if (r > maxCircleSize) maxCircleSize = r;
+    }
     var px = this.previousVerticalX_;
-    ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
+    ctx.clearRect(px - maxCircleSize - 1, 0,
+                  2 * maxCircleSize + 2, this.height_);
   }
 
   var isOK = function(x) { return x && !isNaN(x); };
@@ -1162,7 +1218,7 @@ Dygraph.prototype.updateSelection_ = function() {
     if (this.attr_('showLabelsOnHighlight')) {
       // Set the status message to indicate the selected point(s)
       for (var i = 0; i < this.selPoints_.length; i++) {
-       if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;        
+        if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
         if (!isOK(this.selPoints_[i].canvasy)) continue;
         if (this.attr_("labelsSeparateLines")) {
           replace += "<br/>";
@@ -1182,6 +1238,8 @@ Dygraph.prototype.updateSelection_ = function() {
     ctx.save();
     for (var i = 0; i < this.selPoints_.length; i++) {
       if (!isOK(this.selPoints_[i].canvasy)) continue;
+      var circleSize =
+        this.attr_('highlightCircleSize', this.selPoints_[i].name);
       ctx.beginPath();
       ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
       ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
@@ -1212,7 +1270,13 @@ Dygraph.prototype.setSelection = function(row) {
   if (row !== false && row >= 0) {
     for (var i in this.layout_.datasets) {
       if (row < this.layout_.datasets[i].length) {
-        this.selPoints_.push(this.layout_.points[pos+row]);
+        var point = this.layout_.points[pos+row];
+        
+        if (this.attr_("stackedGraph")) {
+          point = this.layout_.unstackPointAtIndex(pos+row);
+        }
+        
+        this.selPoints_.push(point);
       }
       pos += this.layout_.datasets[i].length;
     }
@@ -1706,8 +1770,6 @@ Dygraph.prototype.drawGraph_ = function(data) {
   this.setColors_();
   this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
 
-  var connectSeparatedPoints = this.attr_('connectSeparatedPoints');
-
   // Loop over the fields (series).  Go from the last to the first,
   // because if they're stacked that's how we accumulate the values.
 
@@ -1718,6 +1780,8 @@ Dygraph.prototype.drawGraph_ = function(data) {
   for (var i = data[0].length - 1; i >= 1; i--) {
     if (!this.visibility()[i - 1]) continue;
 
+    var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
+
     var series = [];
     for (var j = 0; j < data.length; j++) {
       if (data[j][i] != null || !connectSeparatedPoints) {
@@ -2262,6 +2326,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   var labels = [data.getColumnLabel(0)];
   for (var i = 0; i < colIdx.length; i++) {
     labels.push(data.getColumnLabel(colIdx[i]));
+    if (this.attr_("errorBars")) i += 1;
   }
   this.attrs_.labels = labels;
   cols = labels.length;
@@ -2273,8 +2338,8 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     var row = [];
     if (typeof(data.getValue(i, 0)) === 'undefined' ||
         data.getValue(i, 0) === null) {
-      this.warning("Ignoring row " + i +
-                   " of DataTable because of undefined or null first column.");
+      this.warn("Ignoring row " + i +
+                " of DataTable because of undefined or null first column.");
       continue;
     }
 
@@ -2422,15 +2487,24 @@ Dygraph.prototype.start_ = function() {
  */
 Dygraph.prototype.updateOptions = function(attrs) {
   // TODO(danvk): this is a mess. Rethink this function.
-  if (attrs.rollPeriod) {
+  if ('rollPeriod' in attrs) {
     this.rollPeriod_ = attrs.rollPeriod;
   }
-  if (attrs.dateWindow) {
+  if ('dateWindow' in attrs) {
     this.dateWindow_ = attrs.dateWindow;
   }
-  if (attrs.valueRange) {
+  if ('valueRange' in attrs) {
     this.valueRange_ = attrs.valueRange;
+    this.valueWindow_ = attrs.valueRange;
   }
+
+  // TODO(danvk): validate per-series options.
+  // Supported:
+  // strokeWidth
+  // pointSize
+  // drawPoints
+  // highlightCircleSize
+
   Dygraph.update(this.user_attrs_, attrs);
   Dygraph.update(this.renderOptions_, attrs);
 
@@ -2545,6 +2619,18 @@ Dygraph.prototype.annotations = function() {
   return this.annotations_;
 };
 
+/**
+ * Get the index of a series (column) given its name. The first column is the
+ * x-axis, so the data series start with index 1.
+ */
+Dygraph.prototype.indexFromSetName = function(name) {
+  var labels = this.attr_("labels");
+  for (var i = 0; i < labels.length; i++) {
+    if (labels[i] == name) return i;
+  }
+  return null;
+};
+
 Dygraph.addAnnotationRule = function() {
   if (Dygraph.addedAnnotationCSS) return;