Merge branch 'master' of git://github.com/danvk/dygraphs
authorRobert Konigsberg <konigsberg@google.com>
Fri, 8 Oct 2010 14:21:30 +0000 (10:21 -0400)
committerRobert Konigsberg <konigsberg@google.com>
Fri, 8 Oct 2010 14:21:30 +0000 (10:21 -0400)
1  2 
dygraph-canvas.js
dygraph.js

diff --combined dygraph-canvas.js
@@@ -19,7 -19,7 +19,7 @@@ DygraphLayout = function(dygraph, optio
    this.options = {};  // TODO(danvk): remove, use attr_ instead.
    Dygraph.update(this.options, options ? options : {});
    this.datasets = new Array();
 -  this.annotations = new Array()
 +  this.annotations = new Array();
  };
  
  DygraphLayout.prototype.attr_ = function(name) {
@@@ -103,6 -103,13 +103,6 @@@ DygraphLayout.prototype._evaluateLineCh
          name: setName
        };
  
 -      // limit the x, y values so they do not overdraw
 -      if (point.y <= 0.0) {
 -        point.y = 0.0;
 -      }
 -      if (point.y >= 1.0) {
 -        point.y = 1.0;
 -      }
        this.points.push(point);
      }
    }
@@@ -195,6 -202,35 +195,35 @@@ DygraphLayout.prototype.updateOptions 
    Dygraph.update(this.options, new_options ? new_options : {});
  };
  
+ /**
+  * Return a copy of the point at the indicated index, with its yval unstacked.
+  * @param int index of point in layout_.points
+  */
+ DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
+   var point = this.points[idx];
+   
+   // Clone the point since we modify it
+   var unstackedPoint = {};  
+   for (var i in point) {
+     unstackedPoint[i] = point[i];
+   }
+   
+   if (!this.attr_("stackedGraph")) {
+     return unstackedPoint;
+   }
+   
+   // The unstacked yval is equal to the current yval minus the yval of the 
+   // next point at the same xval.
+   for (var i = idx+1; i < this.points.length; i++) {
+     if (this.points[i].xval == point.xval) {
+       unstackedPoint.yval -= this.points[i].yval; 
+       break;
+     }
+   }
+   
+   return unstackedPoint;
+ }  
  // Subclass PlotKit.CanvasRenderer to add:
  // 1. X/Y grid overlay
  // 2. Ability to draw error bars (if required)
@@@ -748,13 -784,14 +777,14 @@@ DygraphCanvasRenderer.prototype._render
    for (var i = 0; i < setCount; i++) {
      var setName = setNames[i];
      var color = this.colors[setName];
+     var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
  
      // setup graphics context
      context.save();
      var point = this.layout.points[0];
-     var pointSize = this.dygraph_.attr_("pointSize");
+     var pointSize = this.dygraph_.attr_("pointSize", setName);
      var prevX = null, prevY = null;
-     var drawPoints = this.dygraph_.attr_("drawPoints");
+     var drawPoints = this.dygraph_.attr_("drawPoints", setName);
      var points = this.layout.points;
      for (var j = 0; j < points.length; j++) {
        var point = points[j];
              prevX = point.canvasx;
              prevY = point.canvasy;
            } else {
-             ctx.beginPath();
-             ctx.strokeStyle = color;
-             ctx.lineWidth = this.options.strokeWidth;
-             ctx.moveTo(prevX, prevY);
-             if (stepPlot) {
-               ctx.lineTo(point.canvasx, prevY);
+             // TODO(danvk): figure out why this conditional is necessary.
+             if (strokeWidth) {
+               ctx.beginPath();
+               ctx.strokeStyle = color;
+               ctx.lineWidth = strokeWidth;
+               ctx.moveTo(prevX, prevY);
+               if (stepPlot) {
+                 ctx.lineTo(point.canvasx, prevY);
+               }
+               prevX = point.canvasx;
+               prevY = point.canvasy;
+               ctx.lineTo(prevX, prevY);
+               ctx.stroke();
              }
-             prevX = point.canvasx;
-             prevY = point.canvasy;
-             ctx.lineTo(prevX, prevY);
-             ctx.stroke();
            }
  
            if (drawPoints || isIsolated) {
diff --combined dygraph.js
@@@ -135,11 -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;
  
@@@ -176,13 -171,7 +176,13 @@@ Dygraph.prototype.__init__ = function(d
    this.previousVerticalX_ = -1;
    this.fractions_ = attrs.fractions || false;
    this.dateWindow_ = attrs.dateWindow || null;
 +  // valueRange and valueWindow are similar, but not the same. valueRange is a
 +  // 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.
    this.valueRange_ = attrs.valueRange || null;
 +  this.valueWindow_ = this.valueRange_;
 +
    this.wilsonInterval_ = attrs.wilsonInterval || true;
    this.is_initial_draw_ = true;
    this.annotations_ = [];
    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];
@@@ -757,20 -751,8 +762,20 @@@ Dygraph.prototype.createDragInterface_ 
    var dragEndX = null;
    var dragEndY = 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;
        dragEndX = getX(event);
        dragEndY = getY(event);
  
 -      self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
 +      var xDelta = Math.abs(dragStartX - dragEndX);
 +      var yDelta = Math.abs(dragStartY - dragEndY);
 +
 +      // drag direction threshold for y axis is twice as large as x axis
 +      var dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
 +
 +      self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY,
 +                         prevDragDirection, prevEndX, prevEndY);
 +
        prevEndX = dragEndX;
 +      prevEndY = dragEndY;
 +      prevDragDirection = dragDirection;
      } else if (isPanning) {
        dragEndX = getX(event);
        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.
 +
 +      var minDate = draggingDate - (dragEndX / self.width_) * dateRange;
 +      var maxDate = minDate + dateRange;
 +      self.dateWindow_ = [minDate, maxDate];
 +
  
 -      self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
 -      self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
 +      // y-axis scaling is automatic unless a valueRange is defiend 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_);
      }
    });
      dragStartY = getY(event);
  
      if (event.altKey || event.shiftKey) {
 -      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;
      }
      if (isPanning) {
        isPanning = false;
        draggingDate = null;
 +      draggingValue = null;
        dateRange = null;
 +      valueRange = null;
      }
    });
  
          }
        }
  
 -      if (regionWidth >= 10) {
 -        self.doZoom_(Math.min(dragStartX, dragEndX),
 +      if (regionWidth >= 10 && regionWidth > regionHeight) {
 +        self.doZoomX_(Math.min(dragStartX, dragEndX),
                       Math.max(dragStartX, dragEndX));
 +      } else if (regionHeight >= 10 && regionHeight > regionWidth){
 +        self.doZoomY_(Math.min(dragStartY, dragEndY),
 +                      Math.max(dragStartY, dragEndY));
        } else {
          self.canvas_.getContext("2d").clearRect(0, 0,
                                             self.canvas_.width,
      if (isPanning) {
        isPanning = false;
        draggingDate = null;
 +      draggingValue = null;
        dateRange = null;
 +      valueRange = null;
      }
    });
  
    // Double-clicking zooms back out
    Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
 -    if (self.dateWindow_ == null) return;
 -    self.dateWindow_ = null;
 -    self.drawGraph_(self.rawData_);
 -    var minDate = self.rawData_[0][0];
 -    var maxDate = self.rawData_[self.rawData_.length - 1][0];
 -    if (self.attr_("zoomCallback")) {
 -      self.attr_("zoomCallback")(minDate, maxDate);
 -    }
 +    // Disable zooming out if panning.
 +    if (event.altKey || event.shiftKey) return;
 +
 +    self.doUnzoom_();
    });
  };
  
   * up any previous zoom rectangles that were drawn. This could be optimized to
   * avoid extra redrawing, but it's tricky to avoid interactions with the status
   * dots.
 + * 
 + * @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 {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
 + * @param {Number} prevEndY The value of endY on the previous call to this
 + * function. Used to avoid excess redrawing
   * @private
   */
 -Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
 +Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY,
 +                                           prevDirection, prevEndX, prevEndY) {
    var ctx = this.canvas_.getContext("2d");
  
    // Clean up from the previous rect if necessary
 -  if (prevEndX) {
 +  if (prevDirection == Dygraph.HORIZONTAL) {
      ctx.clearRect(Math.min(startX, prevEndX), 0,
                    Math.abs(startX - prevEndX), this.height_);
 +  } 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 (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 == 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 == Dygraph.VERTICAL) {
 +    if (endY && startY) {
 +      ctx.fillStyle = "rgba(128,128,128,0.33)";
 +      ctx.fillRect(0, Math.min(startY, endY),
 +                   this.width_, Math.abs(endY - startY));
 +    }
    }
  };
  
  /**
 - * Zoom to something containing [lowX, highX]. These are pixel coordinates
 - * in the canvas. The exact zoom window may be slightly larger if there are no
 - * data points near lowX or highX. This function redraws the graph.
 + * Zoom to something containing [lowX, highX]. These are pixel coordinates in
 + * the canvas. The exact zoom window may be slightly larger if there are no data
 + * points near lowX or highX. Don't confuse this function with doZoomXDates,
 + * which accepts dates that match the raw data. This function redraws the graph.
 + * 
   * @param {Number} lowX The leftmost pixel value that should be visible.
   * @param {Number} highX The rightmost pixel value that should be visible.
   * @private
   */
 -Dygraph.prototype.doZoom_ = function(lowX, highX) {
 +Dygraph.prototype.doZoomX_ = function(lowX, highX) {
    // Find the earliest and latest dates contained in this canvasx range.
 +  // Convert the call to date ranges of the raw data.
    var r = this.toDataCoords(lowX, null);
    var minDate = r[0];
    r = this.toDataCoords(highX, null);
    var maxDate = r[0];
 +  this.doZoomXDates_(minDate, maxDate);
 +};
  
 +/**
 + * Zoom to something containing [minDate, maxDate] values. Don't confuse this
 + * method with doZoomX which accepts pixel coordinates. This function redraws
 + * the graph.
 + * 
 + * @param {Number} minDate The minimum date that should be visible.
 + * @param {Number} maxDate The maximum date that should be visible.
 + * @private
 + */
 +Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
    this.dateWindow_ = [minDate, maxDate];
    this.drawGraph_(this.rawData_);
    if (this.attr_("zoomCallback")) {
 -    this.attr_("zoomCallback")(minDate, maxDate);
 +    var yRange = this.yAxisRange();
 +    this.attr_("zoomCallback")(minDate, maxDate, yRange[0], yRange[1]);
 +  }
 +};
 +
 +/**
 + * Zoom to something containing [lowY, highY]. These are pixel coordinates in
 + * the canvas. The exact zoom window may be slightly larger if there are no
 + * data points near lowY or highY.  Don't confuse this function with
 + * doZoomYValues, which accepts parameters that match the raw data. This
 + * function redraws the graph.
 + * 
 + * @param {Number} lowY The topmost pixel value that should be visible.
 + * @param {Number} highY The lowest pixel value that should be visible.
 + * @private
 + */
 +Dygraph.prototype.doZoomY_ = function(lowY, highY) {
 +  // Find the highest and lowest values in pixel range.
 +  var r = this.toDataCoords(null, lowY);
 +  var maxValue = r[1];
 +  r = this.toDataCoords(null, highY);
 +  var minValue = r[1];
 +
 +  this.doZoomYValues_(minValue, maxValue);
 +};
 +
 +/**
 + * Zoom to something containing [minValue, maxValue] values. Don't confuse this
 + * method with doZoomY which accepts pixel coordinates. This function redraws
 + * the graph.
 + * 
 + * @param {Number} minValue The minimum Value that should be visible.
 + * @param {Number} maxValue The maximum value that should be visible.
 + * @private
 + */
 +Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) {
 +  this.valueWindow_ = [minValue, maxValue];
 +  this.drawGraph_(this.rawData_);
 +  if (this.attr_("zoomCallback")) {
 +    var xRange = this.xAxisRange(); 
 +    this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue);
 +  }
 +};
 +
 +/**
 + * Reset the zoom to the original view coordinates. This is the same as
 + * double-clicking on the graph.
 + * 
 + * @private
 + */
 +Dygraph.prototype.doUnzoom_ = function() {
 +  var dirty = null;
 +  if (this.dateWindow_ != null) {
 +    dirty = 1;
 +    this.dateWindow_ = null;
 +  }
 +  if (this.valueWindow_ != null) {
 +    dirty = 1;
 +    this.valueWindow_ = this.valueRange_;
 +  }
 +
 +  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.yAxisRange()[0];
 +      var maxValue = this.yAxisRange()[1];
 +      this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue);
 +    }
    }
  };
  
@@@ -1185,11 -1022,18 +1190,18 @@@ Dygraph.prototype.mouseMove_ = function
   */
  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); };
      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/>";
      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,
@@@ -1255,7 -1101,13 +1269,13 @@@ Dygraph.prototype.setSelection = functi
    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;
      }
@@@ -1749,8 -1601,6 +1769,6 @@@ Dygraph.prototype.drawGraph_ = function
    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.
  
    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) {
    }
  
    // 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]);
 -    this.displayedYRange_ = this.valueRange_;
 +  // set explicitly by the developer or end-user (via drag)
 +  if (this.valueWindow_ != null) {
 +    this.addYTicks_(this.valueWindow_[0], this.valueWindow_[1]);
 +    this.displayedYRange_ = this.valueWindow_;
    } else {
      // This affects the calculation of span, below.
      if (this.attr_("includeZero") && minY > 0) {
@@@ -2305,6 -2157,7 +2325,7 @@@ Dygraph.prototype.parseDataTable_ = fun
    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;
      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;
      }
  
@@@ -2474,6 -2327,14 +2495,14 @@@ Dygraph.prototype.updateOptions = funct
    if (attrs.valueRange) {
      this.valueRange_ = attrs.valueRange;
    }
+   // TODO(danvk): validate per-series options.
+   // Supported:
+   // strokeWidth
+   // pointSize
+   // drawPoints
+   // highlightCircleSize
    Dygraph.update(this.user_attrs_, attrs);
    Dygraph.update(this.renderOptions_, attrs);
  
@@@ -2588,6 -2449,18 +2617,18 @@@ Dygraph.prototype.annotations = functio
    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;