Merge branch 'master' of https://github.com/danvk/dygraphs
authorJeremy Brewer <jeremy.d.brewer@gmail.com>
Tue, 21 Dec 2010 22:27:17 +0000 (17:27 -0500)
committerJeremy Brewer <jeremy.d.brewer@gmail.com>
Tue, 21 Dec 2010 22:27:17 +0000 (17:27 -0500)
1  2 
dygraph.js

diff --combined dygraph.js
@@@ -79,6 -79,7 +79,7 @@@ Dygraph.DEFAULT_WIDTH = 480
  Dygraph.DEFAULT_HEIGHT = 320;
  Dygraph.AXIS_LINE_WIDTH = 0.3;
  
  // Default attribute values.
  Dygraph.DEFAULT_ATTRS = {
    highlightCircleSize: 3,
@@@ -96,7 -97,7 +97,7 @@@
    showLabelsOnHighlight: true,
  
    yValueFormatter: function(x, opt_numDigits) {
 -    return x.toPrecision(opt_numDigits || 2);
 +    return x.toPrecision(Math.min(21, Math.max(1, opt_numDigits || 2)));
    },
  
    strokeWidth: 1.0,
    hideOverlayOnMouseOut: true,
  
    stepPlot: false,
-   avoidMinZero: false
+   avoidMinZero: false,
+   interactionModel: null  // will be set to Dygraph.defaultInteractionModel.
  };
  
  // Various logging levels.
@@@ -160,7 -163,7 +163,7 @@@ Dygraph.prototype.__old_init__ = functi
  
  /**
   * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
-  * and interaction &lt;canvas&gt; inside of it. See the constructor for details
+  * and context &lt;canvas&gt; 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
@@@ -193,20 -196,7 +196,20 @@@ Dygraph.prototype.__init__ = function(d
    this.wilsonInterval_ = attrs.wilsonInterval || true;
    this.is_initial_draw_ = true;
    this.annotations_ = [];
 -  this.numDigits_ = 2;
 +  
 +  // Number of digits to use when labeling the x (if numeric) and y axis
 +  // ticks.
 +  this.numXDigits_ = 2;
 +  this.numYDigits_ = 2;
 +
 +  // When labeling x (if numeric) or y values in the legend, there are
 +  // numDigits + numExtraDigits of precision used.  For axes labels with N
 +  // digits of precision, the data should be displayed with at least N+1 digits
 +  // of precision. The reason for this is to divide each interval between
 +  // successive ticks into tenths (for 1) or hundredths (for 2), etc.  For
 +  // example, if the labels are [0, 1, 2], we want data to be displayed as
 +  // 0.1, 1.3, etc.
 +  this.numExtraDigits_ = 1;
  
    // Clear the div. This ensure that, if multiple dygraphs are passed the same
    // div, then only one will be drawn.
@@@ -446,6 -436,23 +449,23 @@@ Dygraph.addEvent = function(el, evt, fn
    }
  };
  
+ // Based on the article at
+ // http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
+ Dygraph.cancelEvent = function(e) {
+   e = e ? e : window.event;
+   if (e.stopPropagation) {
+     e.stopPropagation();
+   }
+   if (e.preventDefault) {
+     e.preventDefault();
+   }
+   e.cancelBubble = true;
+   e.cancel = true;
+   e.returnValue = false;
+   return false;
+ }
  /**
   * Generates interface elements for the Dygraph: a containing div, a div to
   * display the current point, and a textbox to adjust the rolling average
@@@ -778,242 -785,347 +798,347 @@@ Dygraph.pageY = function(e) 
    }
  };
  
- /**
-  * Set up all the mouse handlers needed to capture dragging behavior for zoom
-  * events.
-  * @private
-  */
- Dygraph.prototype.createDragInterface_ = function() {
-   var self = this;
-   // Tracks whether the mouse is down right now
-   var isZooming = false;
-   var isPanning = false;  // is this drag part of a pan?
-   var is2DPan = false;    // if so, is that pan 1- or 2-dimensional?
-   var dragStartX = null;
-   var dragStartY = null;
-   var dragEndX = null;
-   var dragEndY = null;
-   var dragDirection = null;
-   var prevEndX = null;
-   var prevEndY = null;
-   var prevDragDirection = null;
+ Dygraph.prototype.dragGetX_ = function(e, context) {
+   return Dygraph.pageX(e) - context.px
+ };
  
-   // TODO(danvk): update this comment
-   // 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;
+ Dygraph.prototype.dragGetY_ = function(e, context) {
+   return Dygraph.pageY(e) - context.py
+ };
  
-   // TODO(danvk): update this comment
-   // The range in second/value units that the viewport encompasses during a
-   // panning operation.
-   var dateRange = null;
+ // Called in response to an interaction model operation that
+ // should start the default panning behavior.
+ //
+ // It's used in the default callback for "mousedown" operations.
+ // Custom interaction model builders can use it to provide the default
+ // panning behavior.
+ //
+ Dygraph.startPan = function(event, g, context) {
+   // have to be zoomed in to pan.
+   // TODO(konigsberg): Let's loosen this zoom-to-pan restriction, also
+   // perhaps create panning boundaries? A more flexible pan would make it,
+   // ahem, 'pan-useful'.
+   var zoomedY = false;
+   for (var i = 0; i < g.axes_.length; i++) {
+     if (g.axes_[i].valueWindow || g.axes_[i].valueRange) {
+       zoomedY = true;
+       break;
+     }
+   }
+   if (!g.dateWindow_ && !zoomedY) return;
+   context.isPanning = true;
+   var xRange = g.xAxisRange();
+   context.dateRange = xRange[1] - xRange[0];
+   // Record the range of each y-axis at the start of the drag.
+   // If any axis has a valueRange or valueWindow, then we want a 2D pan.
+   context.is2DPan = false;
+   for (var i = 0; i < g.axes_.length; i++) {
+     var axis = g.axes_[i];
+     var yRange = g.yAxisRange(i);
+     axis.dragValueRange = yRange[1] - yRange[0];
+     var r = g.toDataCoords(null, context.dragStartY, i);
+     axis.draggingValue = r[1];
+     if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
+   }
  
-   // Utility function to convert page-wide coordinates to canvas coords
-   var px = 0;
-   var py = 0;
-   var getX = function(e) { return Dygraph.pageX(e) - px };
-   var getY = function(e) { return Dygraph.pageY(e) - py };
+   // TODO(konigsberg): Switch from all this math to toDataCoords?
+   // Seems to work for the dragging value.
+   context.draggingDate = (context.dragStartX / g.width_) * context.dateRange + xRange[0];
+ };
  
-   // Draw zoom rectangles when the mouse is down and the user moves around
-   Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
-     if (isZooming) {
-       dragEndX = getX(event);
-       dragEndY = getY(event);
-       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
-       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);
-       // TODO(danvk): update this comment
-       // Want to have it so that:
-       // 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];
-       // y-axis scaling is automatic unless this is a full 2D pan.
-       if (is2DPan) {
-         // Adjust each axis appropriately.
-         var y_frac = dragEndY / self.height_;
-         for (var i = 0; i < self.axes_.length; i++) {
-           var axis = self.axes_[i];
-           var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
-           var minValue = maxValue - axis.dragValueRange;
-           axis.valueWindow = [ minValue, maxValue ];
-         }
-       }
+ // Called in response to an interaction model operation that
+ // responds to an event that pans the view.
+ //
+ // It's used in the default callback for "mousemove" operations.
+ // Custom interaction model builders can use it to provide the default
+ // panning behavior.
+ //
+ Dygraph.movePan = function(event, g, context) {
+   context.dragEndX = g.dragGetX_(event, context);
+   context.dragEndY = g.dragGetY_(event, context);
  
-       self.drawGraph_();
+   // TODO(danvk): update this comment
+   // Want to have it so that:
+   // 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 = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange;
+   var maxDate = minDate + context.dateRange;
+   g.dateWindow_ = [minDate, maxDate];
+   // y-axis scaling is automatic unless this is a full 2D pan.
+   if (context.is2DPan) {
+     // Adjust each axis appropriately.
+     var y_frac = context.dragEndY / g.height_;
+     for (var i = 0; i < g.axes_.length; i++) {
+       var axis = g.axes_[i];
+       var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
+       var minValue = maxValue - axis.dragValueRange;
+       axis.valueWindow = [ minValue, maxValue ];
      }
-   });
+   }
  
-   // Track the beginning of drag events
-   Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
-     // prevents mouse drags from selecting page text.
-     if (event.preventDefault) {
-       event.preventDefault();  // Firefox, Chrome, etc.
-     } else {
-       event.returnValue = false;  // IE
-       event.cancelBubble = true;  
-     }
+   g.drawGraph_();
+ }
  
-     px = Dygraph.findPosX(self.canvas_);
-     py = Dygraph.findPosY(self.canvas_);
-     dragStartX = getX(event);
-     dragStartY = getY(event);
+ // Called in response to an interaction model operation that
+ // responds to an event that ends panning.
+ //
+ // It's used in the default callback for "mouseup" operations.
+ // Custom interaction model builders can use it to provide the default
+ // panning behavior.
+ //
+ Dygraph.endPan = function(event, g, context) {
+   context.isPanning = false;
+   context.is2DPan = false;
+   context.draggingDate = null;
+   context.dateRange = null;
+   context.valueRange = null;
+ }
  
-     if (event.altKey || event.shiftKey) {
-       // have to be zoomed in to pan.
-       var zoomedY = false;
-       for (var i = 0; i < self.axes_.length; i++) {
-         if (self.axes_[i].valueWindow || self.axes_[i].valueRange) {
-           zoomedY = true;
-           break;
+ // Called in response to an interaction model operation that
+ // responds to an event that starts zooming.
+ //
+ // It's used in the default callback for "mousedown" operations.
+ // Custom interaction model builders can use it to provide the default
+ // zooming behavior.
+ //
+ Dygraph.startZoom = function(event, g, context) {
+   context.isZooming = true;
+ }
+ // Called in response to an interaction model operation that
+ // responds to an event that defines zoom boundaries.
+ //
+ // It's used in the default callback for "mousemove" operations.
+ // Custom interaction model builders can use it to provide the default
+ // zooming behavior.
+ //
+ Dygraph.moveZoom = function(event, g, context) {
+   context.dragEndX = g.dragGetX_(event, context);
+   context.dragEndY = g.dragGetY_(event, context);
+   var xDelta = Math.abs(context.dragStartX - context.dragEndX);
+   var yDelta = Math.abs(context.dragStartY - context.dragEndY);
+   // drag direction threshold for y axis is twice as large as x axis
+   context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
+   g.drawZoomRect_(
+       context.dragDirection,
+       context.dragStartX,
+       context.dragEndX,
+       context.dragStartY,
+       context.dragEndY,
+       context.prevDragDirection,
+       context.prevEndX,
+       context.prevEndY);
+   context.prevEndX = context.dragEndX;
+   context.prevEndY = context.dragEndY;
+   context.prevDragDirection = context.dragDirection;
+ }
+ // Called in response to an interaction model operation that
+ // responds to an event that performs a zoom based on previously defined
+ // bounds..
+ //
+ // It's used in the default callback for "mouseup" operations.
+ // Custom interaction model builders can use it to provide the default
+ // zooming behavior.
+ //
+ Dygraph.endZoom = function(event, g, context) {
+   context.isZooming = false;
+   context.dragEndX = g.dragGetX_(event, context);
+   context.dragEndY = g.dragGetY_(event, context);
+   var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
+   var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
+   if (regionWidth < 2 && regionHeight < 2 &&
+       g.lastx_ != undefined && g.lastx_ != -1) {
+     // TODO(danvk): pass along more info about the points, e.g. 'x'
+     if (g.attr_('clickCallback') != null) {
+       g.attr_('clickCallback')(event, g.lastx_, g.selPoints_);
+     }
+     if (g.attr_('pointClickCallback')) {
+       // check if the click was on a particular point.
+       var closestIdx = -1;
+       var closestDistance = 0;
+       for (var i = 0; i < g.selPoints_.length; i++) {
+         var p = g.selPoints_[i];
+         var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
+                        Math.pow(p.canvasy - context.dragEndY, 2);
+         if (closestIdx == -1 || distance < closestDistance) {
+           closestDistance = distance;
+           closestIdx = i;
          }
        }
-       if (!self.dateWindow_ && !zoomedY) return;
-       isPanning = true;
-       var xRange = self.xAxisRange();
-       dateRange = xRange[1] - xRange[0];
  
-       // Record the range of each y-axis at the start of the drag.
-       // If any axis has a valueRange or valueWindow, then we want a 2D pan.
-       is2DPan = false;
-       for (var i = 0; i < self.axes_.length; i++) {
-         var axis = self.axes_[i];
-         var yRange = self.yAxisRange(i);
-         axis.dragValueRange = yRange[1] - yRange[0];
-         var r = self.toDataCoords(null, dragStartY, i);
-         axis.draggingValue = r[1];
-         if (axis.valueWindow || axis.valueRange) is2DPan = true;
+       // Allow any click within two pixels of the dot.
+       var radius = g.attr_('highlightCircleSize') + 2;
+       if (closestDistance <= 5 * 5) {
+         g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]);
        }
+     }
+   }
  
-       // TODO(konigsberg): Switch from all this math to toDataCoords?
-       // Seems to work for the dragging value.
-       draggingDate = (dragStartX / self.width_) * dateRange + xRange[0];
+   if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
+     g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
+                Math.max(context.dragStartX, context.dragEndX));
+   } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
+     g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
+                Math.max(context.dragStartY, context.dragEndY));
+   } else {
+     g.canvas_.getContext("2d").clearRect(0, 0,
+                                        g.canvas_.width,
+                                        g.canvas_.height);
+   }
+   context.dragStartX = null;
+   context.dragStartY = null;
+ }
+ Dygraph.defaultInteractionModel = {
+   // Track the beginning of drag events
+   mousedown: function(event, g, context) {
+     context.initializeMouseDown(event, g, context);
+     if (event.altKey || event.shiftKey) {
+       Dygraph.startPan(event, g, context);
      } else {
-       isZooming = true;
+       Dygraph.startZoom(event, g, context);
      }
-   });
+   },
  
-   // If the user releases the mouse button during a drag, but not over the
-   // canvas, then it doesn't count as a zooming action.
-   Dygraph.addEvent(document, 'mouseup', function(event) {
-     if (isZooming || isPanning) {
-       isZooming = false;
-       dragStartX = null;
-       dragStartY = null;
+   // Draw zoom rectangles when the mouse is down and the user moves around
+   mousemove: function(event, g, context) {
+     if (context.isZooming) {
+       Dygraph.moveZoom(event, g, context);
+     } else if (context.isPanning) {
+       Dygraph.movePan(event, g, context);
      }
+   },
  
-     if (isPanning) {
-       isPanning = false;
-       draggingDate = null;
-       dateRange = null;
-       for (var i = 0; i < self.axes_.length; i++) {
-         delete self.axes_[i].draggingValue;
-         delete self.axes_[i].dragValueRange;
-       }
+   mouseup: function(event, g, context) {
+     if (context.isZooming) {
+       Dygraph.endZoom(event, g, context);
+     } else if (context.isPanning) {
+       Dygraph.endPan(event, g, context);
      }
-   });
+   },
  
    // Temporarily cancel the dragging event when the mouse leaves the graph
-   Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
-     if (isZooming) {
-       dragEndX = null;
-       dragEndY = null;
+   mouseout: function(event, g, context) {
+     if (context.isZooming) {
+       context.dragEndX = null;
+       context.dragEndY = null;
      }
-   });
+   },
  
-   // 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.mouseEventElement_, 'mouseup', function(event) {
-     if (isZooming) {
-       isZooming = false;
-       dragEndX = getX(event);
-       dragEndY = getY(event);
-       var regionWidth = Math.abs(dragEndX - dragStartX);
-       var regionHeight = Math.abs(dragEndY - dragStartY);
-       if (regionWidth < 2 && regionHeight < 2 &&
-           self.lastx_ != undefined && self.lastx_ != -1) {
-         // TODO(danvk): pass along more info about the points, e.g. 'x'
-         if (self.attr_('clickCallback') != null) {
-           self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
-         }
-         if (self.attr_('pointClickCallback')) {
-           // check if the click was on a particular point.
-           var closestIdx = -1;
-           var closestDistance = 0;
-           for (var i = 0; i < self.selPoints_.length; i++) {
-             var p = self.selPoints_[i];
-             var distance = Math.pow(p.canvasx - dragEndX, 2) +
-                            Math.pow(p.canvasy - dragEndY, 2);
-             if (closestIdx == -1 || distance < closestDistance) {
-               closestDistance = distance;
-               closestIdx = i;
-             }
-           }
+   // Disable zooming out if panning.
+   dblclick: function(event, g, context) {
+     if (event.altKey || event.shiftKey) {
+       return;
+     }
+     // TODO(konigsberg): replace g.doUnzoom()_ with something that is
+     // friendlier to public use.
+     g.doUnzoom_();
+   }
+ };
  
-           // Allow any click within two pixels of the dot.
-           var radius = self.attr_('highlightCircleSize') + 2;
-           if (closestDistance <= 5 * 5) {
-             self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
-           }
-         }
-       }
+ Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
  
-       if (regionWidth >= 10 && dragDirection == Dygraph.HORIZONTAL) {
-         self.doZoomX_(Math.min(dragStartX, dragEndX),
-                      Math.max(dragStartX, dragEndX));
-       } else if (regionHeight >= 10 && dragDirection == Dygraph.VERTICAL){
-         self.doZoomY_(Math.min(dragStartY, dragEndY),
-                       Math.max(dragStartY, dragEndY));
+ /**
+  * Set up all the mouse handlers needed to capture dragging behavior for zoom
+  * events.
+  * @private
+  */
+ Dygraph.prototype.createDragInterface_ = function() {
+   var context = {
+     // Tracks whether the mouse is down right now
+     isZooming: false,
+     isPanning: false,  // is this drag part of a pan?
+     is2DPan: false,    // if so, is that pan 1- or 2-dimensional?
+     dragStartX: null,
+     dragStartY: null,
+     dragEndX: null,
+     dragEndY: null,
+     dragDirection: null,
+     prevEndX: null,
+     prevEndY: null,
+     prevDragDirection: null,
+     // TODO(danvk): update this comment
+     // 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]
+     draggingDate: null,
+     // TODO(danvk): update this comment
+     // The range in second/value units that the viewport encompasses during a
+     // panning operation.
+     dateRange: null,
+     // Utility function to convert page-wide coordinates to canvas coords
+     px: 0,
+     py: 0,
+     initializeMouseDown: function(event, g, context) {
+       // prevents mouse drags from selecting page text.
+       if (event.preventDefault) {
+         event.preventDefault();  // Firefox, Chrome, etc.
        } else {
-         self.canvas_.getContext("2d").clearRect(0, 0,
-                                            self.canvas_.width,
-                                            self.canvas_.height);
+         event.returnValue = false;  // IE
+         event.cancelBubble = true;
        }
  
-       dragStartX = null;
-       dragStartY = null;
+       context.px = Dygraph.findPosX(g.canvas_);
+       context.py = Dygraph.findPosY(g.canvas_);
+       context.dragStartX = g.dragGetX_(event, context);
+       context.dragStartY = g.dragGetY_(event, context);
      }
+   };
  
-     if (isPanning) {
-       isPanning = false;
-       is2DPan = false;
-       draggingDate = null;
-       dateRange = null;
-       valueRange = null;
-     }
-   });
+   var interactionModel = this.attr_("interactionModel");
+   // Self is the graph.
+   var self = this;
+   // Function that binds the graph and context to the handler.
+   var bindHandler = function(handler) {
+     return function(event) {
+       handler(event, self, context);
+     };
+   };
+   for (var eventName in interactionModel) {
+     if (!interactionModel.hasOwnProperty(eventName)) continue;
+     Dygraph.addEvent(this.mouseEventElement_, eventName,
+         bindHandler(interactionModel[eventName]));
+   }
  
-   // Double-clicking zooms back out
-   Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
-     // Disable zooming out if panning.
-     if (event.altKey || event.shiftKey) return;
+   // If the user releases the mouse button during a drag, but not over the
+   // canvas, then it doesn't count as a zooming action.
+   Dygraph.addEvent(document, 'mouseup', function(event) {
+     if (context.isZooming || context.isPanning) {
+       context.isZooming = false;
+       context.dragStartX = null;
+       context.dragStartY = null;
+     }
  
-     self.doUnzoom_();
+     if (context.isPanning) {
+       context.isPanning = false;
+       context.draggingDate = null;
+       context.dateRange = null;
+       for (var i = 0; i < self.axes_.length; i++) {
+         delete self.axes_[i].draggingValue;
+         delete self.axes_[i].dragValueRange;
+       }
+     }
    });
  };
  
@@@ -1187,7 -1299,7 +1312,7 @@@ Dygraph.prototype.mouseMove_ = function
    for (var i = 0; i < points.length; i++) {
      var point = points[i];
      if (point == null) continue;
-     var dist = Math.abs(points[i].canvasx - canvasx);
+     var dist = Math.abs(point.canvasx - canvasx);
      if (dist > minDist) continue;
      minDist = dist;
      idx = i;
@@@ -1283,8 -1395,7 +1408,8 @@@ Dygraph.prototype.updateSelection_ = fu
      var canvasx = this.selPoints_[0].canvasx;
  
      // Set the status message to indicate the selected point(s)
 -    var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
 +    var replace = this.attr_('xValueFormatter')(
 +          this.lastx_, this.numXDigits_ + this.numExtraDigits_) + ":";
      var fmtFunc = this.attr_('yValueFormatter');
      var clen = this.colors_.length;
  
          }
          var point = this.selPoints_[i];
          var c = new RGBColor(this.plotter_.colors[point.name]);
 -        var yval = fmtFunc(point.yval, this.numDigits_ + 1);  // In tenths.
 +        var yval = fmtFunc(point.yval, this.numYDigits_ + this.numExtraDigits_);
          replace += " <b><font color='" + c.toHex() + "'>"
                  + point.name + "</font></b>:"
                  + yval;
@@@ -1442,7 -1553,9 +1567,9 @@@ Dygraph.hmsString_ = function(date) 
   * @private
   */
  Dygraph.dateAxisFormatter = function(date, granularity) {
-   if (granularity >= Dygraph.MONTHLY) {
+   if (granularity >= Dygraph.DECADAL) {
+     return date.strftime('%Y');
+   } else if (granularity >= Dygraph.MONTHLY) {
      return date.strftime('%b %y');
    } else {
      var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
   * @return {String} A date of the form "YYYY/MM/DD"
   * @private
   */
 -Dygraph.dateString_ = function(date, self) {
 +Dygraph.dateString_ = function(date) {
    var zeropad = Dygraph.zeropad;
    var d = new Date(date);
  
@@@ -1498,18 -1611,21 +1625,18 @@@ Dygraph.prototype.quarters = ["Jan", "A
   */
  Dygraph.prototype.addXTicks_ = function() {
    // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
 -  var startDate, endDate;
 +  var opts = {xTicks: []};
 +  var formatter = this.attr_('xTicker');
    if (this.dateWindow_) {
 -    startDate = this.dateWindow_[0];
 -    endDate = this.dateWindow_[1];
 +    opts.xTicks = formatter(this.dateWindow_[0], this.dateWindow_[1], this);
    } else {
 -    startDate = this.rawData_[0][0];
 -    endDate   = this.rawData_[this.rawData_.length - 1][0];
 -  }
 -
 -  var ret = this.attr_('xTicker')(startDate, endDate, this);
 -  if (ret.ticks !== undefined) {  // Used numericTicks()?
 -    this.layout_.updateOptions({xTicks: ret.ticks});
 -  } else {  // Used dateTicker() instead.
 -    this.layout_.updateOptions({xTicks: ret});
 +    // numericTicks() returns multiple values.
 +    var ret = formatter(this.rawData_[0][0],
 +                        this.rawData_[this.rawData_.length - 1][0], this);
 +    opts.xTicks = ret.ticks;
 +    this.numXDigits_ = ret.numDigits;
    }
 +  this.layout_.updateOptions(opts);
  };
  
  // Time granularity enumeration
@@@ -1533,7 -1649,8 +1660,8 @@@ Dygraph.QUARTERLY = 16
  Dygraph.BIANNUAL = 17;
  Dygraph.ANNUAL = 18;
  Dygraph.DECADAL = 19;
- Dygraph.NUM_GRANULARITIES = 20;
+ Dygraph.CENTENNIAL = 20;
+ Dygraph.NUM_GRANULARITIES = 21;
  
  Dygraph.SHORT_SPACINGS = [];
  Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
@@@ -1569,6 -1686,7 +1697,7 @@@ Dygraph.prototype.NumXTicks = function(
      if (granularity == Dygraph.BIANNUAL) num_months = 2;
      if (granularity == Dygraph.ANNUAL) num_months = 1;
      if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
+     if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
  
      var msInYear = 365.2524 * 24 * 3600 * 1000;
      var num_years = 1.0 * (end_time - start_time) / msInYear;
@@@ -1641,6 -1759,11 +1770,11 @@@ Dygraph.prototype.GetXAxis = function(s
      } else if (granularity == Dygraph.DECADAL) {
        months = [ 0 ];
        year_mod = 10;
+     } else if (granularity == Dygraph.CENTENNIAL) {
+       months = [ 0 ];
+       year_mod = 100;
+     } else {
+       this.warn("Span of dates is too long");
      }
  
      var start_year = new Date(start_time).getFullYear();
@@@ -1698,7 -1821,7 +1832,7 @@@ Dygraph.dateTicker = function(startDate
  Dygraph.significantFigures = function(x, opt_maxPrecision) {
    var precision = Math.max(opt_maxPrecision || 13, 13);
  
 -  // Convert the number to it's exponential notation form and work backwards,
 +  // Convert the number to its exponential notation form and work backwards,
    // ignoring the 'e+xx' bit.  This may seem like a hack, but doing a loop and
    // dividing by 10 leads to roundoff errors.  By using toExponential(), we let
    // the JavaScript interpreter handle the low level bits of the Number for us.
@@@ -1740,7 -1863,7 +1874,7 @@@ Dygraph.numericTicks = function(minV, m
    var ticks = [];
    if (vals) {
      for (var i = 0; i < vals.length; i++) {
 -      ticks[i] = {v: vals[i]};
 +      ticks[i].push({v: vals[i]});
      }
    } else {
      // Basic idea:
      if (low_val > high_val) scale *= -1;
      for (var i = 0; i < nTicks; i++) {
        var tickV = low_val + i * scale;
 -      ticks[i] = {v: tickV};
 +      ticks.push( {v: tickV} );
      }
    }
  
    // take the max because we can't tell if trailing 0s are significant.
    var numDigits = 0;
    for (var i = 0; i < ticks.length; i++) {
 -    var tickV = ticks[i].v;
 -    numDigits = Math.max(Dygraph.significantFigures(tickV), numDigits);
 +    numDigits = Math.max(Dygraph.significantFigures(ticks[i].v), numDigits);
    }
  
    for (var i = 0; i < ticks.length; i++) {
@@@ -2201,7 -2325,7 +2335,7 @@@ Dygraph.prototype.computeYAxisRanges_ 
                               this,
                               axis);
        axis.ticks = ret.ticks;
 -      this.numDigits_ = ret.numDigits;
 +      this.numYDigits_ = ret.numDigits;
      } else {
        var p_axis = this.axes_[0];
        var p_ticks = p_axis.ticks;
                               axis.computedValueRange[1],
                               this, axis, tick_values);
        axis.ticks = ret.ticks;
 -      this.numDigits_ = ret.numDigits;
 +      this.numYDigits_ = ret.numDigits;
      }
    }
  };
@@@ -2417,7 -2541,7 +2551,7 @@@ Dygraph.prototype.detectTypeFromString
      this.attrs_.xTicker = Dygraph.dateTicker;
      this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
    } else {
 -    this.attrs_.xValueFormatter = function(x) { return x; };
 +    this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
      this.attrs_.xValueParser = function(x) { return parseFloat(x); };
      this.attrs_.xTicker = Dygraph.numericTicks;
      this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
@@@ -2580,7 -2704,7 +2714,7 @@@ Dygraph.prototype.parseArray_ = functio
      return parsedData;
    } else {
      // Some intelligent defaults for a numeric x-axis.
 -    this.attrs_.xValueFormatter = function(x) { return x; };
 +    this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
      this.attrs_.xTicker = Dygraph.numericTicks;
      return data;
    }
@@@ -2606,7 -2730,7 +2740,7 @@@ Dygraph.prototype.parseDataTable_ = fun
      this.attrs_.xTicker = Dygraph.dateTicker;
      this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
    } else if (indepType == 'number') {
 -    this.attrs_.xValueFormatter = function(x) { return x; };
 +    this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
      this.attrs_.xValueParser = function(x) { return parseFloat(x); };
      this.attrs_.xTicker = Dygraph.numericTicks;
      this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;