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)
dygraph.js
tests/daylight-savings.html [new file with mode: 0644]
tests/interaction.html [new file with mode: 0644]
tests/zoom.html

index a0afa68..032b1cd 100644 (file)
@@ -79,6 +79,7 @@ Dygraph.DEFAULT_WIDTH = 480;
 Dygraph.DEFAULT_HEIGHT = 320;
 Dygraph.AXIS_LINE_WIDTH = 0.3;
 
+
 // Default attribute values.
 Dygraph.DEFAULT_ATTRS = {
   highlightCircleSize: 3,
@@ -129,7 +130,9 @@ Dygraph.DEFAULT_ATTRS = {
   hideOverlayOnMouseOut: true,
 
   stepPlot: false,
-  avoidMinZero: false
+  avoidMinZero: false,
+
+  interactionModel: null  // will be set to Dygraph.defaultInteractionModel.
 };
 
 // Various logging levels.
@@ -160,7 +163,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
 
 /**
  * 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
@@ -446,6 +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 +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;
+Dygraph.prototype.dragGetX_ = function(e, context) {
+  return Dygraph.pageX(e) - context.px
+};
 
-  // 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.dragGetY_ = function(e, context) {
+  return Dygraph.pageY(e) - context.py
+};
 
-  // 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;
+// 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;
 
-  // TODO(danvk): update this comment
-  // The range in second/value units that the viewport encompasses during a
-  // panning operation.
-  var dateRange = null;
+  context.isPanning = true;
+  var xRange = g.xAxisRange();
+  context.dateRange = xRange[1] - xRange[0];
 
-  // 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 };
+  // 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;
+  }
 
-  // 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 ];
-        }
-      }
+  // 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];
+};
 
-      self.drawGraph_();
-    }
-  });
+// 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);
 
-  // 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;  
+  // 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 ];
     }
+  }
 
-    px = Dygraph.findPosX(self.canvas_);
-    py = Dygraph.findPosY(self.canvas_);
-    dragStartX = getX(event);
-    dragStartY = getY(event);
+  g.drawGraph_();
+}
 
-    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 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;
+}
+
+// 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]);
       }
+    }
+  }
+
+  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);
 
-      // TODO(konigsberg): Switch from all this math to toDataCoords?
-      // Seems to work for the dragging value.
-      draggingDate = (dragStartX / self.width_) * dateRange + xRange[0];
+    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");
 
-  // Double-clicking zooms back out
-  Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
-    // Disable zooming out if panning.
-    if (event.altKey || event.shiftKey) return;
+  // 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);
+    };
+  };
 
-    self.doUnzoom_();
+  for (var eventName in interactionModel) {
+    if (!interactionModel.hasOwnProperty(eventName)) continue;
+    Dygraph.addEvent(this.mouseEventElement_, eventName,
+        bindHandler(interactionModel[eventName]));
+  }
+
+  // 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;
+    }
+
+    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 +1312,7 @@ Dygraph.prototype.mouseMove_ = function(event) {
   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;
@@ -1442,7 +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();
@@ -1533,7 +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 +1697,7 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
     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 +1770,11 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
     } 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();
diff --git a/tests/daylight-savings.html b/tests/daylight-savings.html
new file mode 100644 (file)
index 0000000..68c30b1
--- /dev/null
@@ -0,0 +1,35 @@
+<html>
+  <head>
+    <title>Daylight Savings</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+    <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+    <script type="text/javascript" src="../dygraph-canvas.js"></script>
+    <script type="text/javascript" src="../dygraph.js"></script>
+  </head>
+  <body>
+    <h2>DST</h2>
+    <p>This tests that tick marks don't break when the axis crosses into
+    daylight savings time.</p>
+
+    <div id="demodiv"></div>
+
+    <p>The tick marks should all be on day boundaries or nice hours (6, 12, 18),
+    rather than on odd time boundaries like 5, 11, 17 and 23.</p>
+
+    <script type="text/javascript">
+      g = new Dygraph(
+              document.getElementById("demodiv"),
+"Date/Time,Purchases\n" +
+"2010-11-05 00:00:00,167082\n" +
+"2010-11-06 00:00:00,168571\n" +
+"2010-11-07 00:00:00,177796\n" +
+"2010-11-08 00:00:00,165587\n" +
+"2010-11-09 00:00:00,164380\n",
+          { width: 1024 }
+          );
+    </script>
+</body>
+</html>
diff --git a/tests/interaction.html b/tests/interaction.html
new file mode 100644 (file)
index 0000000..d768dd6
--- /dev/null
@@ -0,0 +1,225 @@
+
+<html>
+  <head>
+    <title>interaction model</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+    <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+    <script type="text/javascript" src="../dygraph-canvas.js"></script>
+    <script type="text/javascript" src="../dygraph.js"></script>
+    <script type="text/javascript" src="data.js"></script>
+  </head>
+  <body>
+    <table border='1'>
+    <tr><td>
+    <b>Default interaction model</b>
+    <div id="div_g" style="width:600px; height:300px;"></div>
+    </td><td>Zoom: click-drag<br/>Pan: shift-click-drag<br/>Restore zoom level: double-click<br/>
+    </td></tr>
+    <tr><td>
+    <b>No interaction model</b>
+    <div id="div_g2" style="width:600px; height:300px;"></div>
+    </td><td>Click and drag all you like, it won't do anything!</td></tr>
+    <tr><td>
+    <b>Custom interaction model</b>
+
+    <input type="button" value="Unzoom" onclick="unzoomGraph(g3)">
+    <div id="div_g3" style="width:600px; height:300px;"></div>
+    </td><td>
+    Zoom in: double-click, scroll wheel<br/>
+    Zoom out: ctrl-double-click, scroll wheel<br/>
+    Standard Zoom: shift-click-drag
+    Standard Pan: click-drag<br/>
+    Restore zoom level: press button<br/>
+    </td></tr>
+    <tr><td>
+    <b>Fun model!</b>
+    <div id="div_g4" style="width:600px; height:300px;"></div>
+    </td><td>
+    Keep the mouse button pressed, and hover over all points
+    to mark them.   
+    </td></tr>
+
+    </table>
+        
+    <script type="text/javascript">
+      function downV3(event, g, context) {
+        context.initializeMouseDown(event, g, context);
+        if (event.altKey || event.shiftKey) {
+          Dygraph.startZoom(event, g, context);
+        } else {
+          Dygraph.startPan(event, g, context);
+        }
+      }
+
+      function moveV3(event, g, context) {
+        if (context.isPanning) {
+          Dygraph.movePan(event, g, context);
+        } else if (context.isZooming) {
+          Dygraph.moveZoom(event, g, context);
+        }
+      }
+
+      function upV3(event, g, context) {
+        if (context.isPanning) {
+          Dygraph.endPan(event, g, context);
+        } else if (context.isZooming) {
+          Dygraph.endZoom(event, g, context);
+        }
+      }
+
+      function dblClickV3(event, g, context) {
+        if (event.ctrlKey) {
+          zoom(g, -(1/9));
+        } else {
+          zoom(g, +.1);
+        }
+      }
+
+      function scrollV3(event, g, context) {
+        var normal = event.detail ? event.detail * -1 : event.wheelDelta / 40;
+        // For me the normalized value shows 0.075 for one click. If I took
+        // that verbatim, it would be a 7.5%. I think I'm gonna take 1/10 of that.
+        // (double for left and right side)
+        var percentage = normal / 100;
+
+        zoom(g, percentage);
+        Dygraph.cancelEvent(event);
+      }
+
+      function zoom(g, percentage) {
+         // Adjusts [x, y] toward each other by percentage%
+         function adjustAxis(axis, percentage) {
+           var delta = axis[1] - axis[0];
+           var increment = delta * percentage;
+           return [ axis[0] + increment, axis[1] - increment ];
+         }
+
+         var yAxes = g.yAxisRanges();
+         var newYAxes = [];
+         for (var i = 0; i < yAxes.length; i++) {
+           newYAxes[i] = adjustAxis(yAxes[i], percentage);
+         }
+
+         g.updateOptions({
+           dateWindow: adjustAxis(g.xAxisRange(), percentage),
+           valueRange: newYAxes[0]
+           });
+      }
+
+      var v4Active = false;
+      var v4Canvas = null;
+
+      function downV4(event, g, context) {
+        context.initializeMouseDown(event, g, context);
+        v4Active = true;
+        moveV4(event, g, context); // in case the mouse went down on a data point.
+      }
+
+      var processed = [];
+
+      function moveV4(event, g, context) {
+        var RANGE = 7;
+
+        if (v4Active) {
+          var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(g.graphDiv);
+          var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(g.graphDiv);
+
+          var rows = g.numRows();
+          // Row layout:
+          // [date, [val1, stdev1], [val2, stdev2]]
+          for (var row = 0; row < rows; row++) {
+            var date = g.getValue(row, 0);
+            var x = g.toDomCoords(date, null)[0];
+            var diff = Math.abs(canvasx - x);
+            if (diff < RANGE) {
+              for (var col = 1; col < 3; col++) {
+                // TODO(konigsberg): these will throw exceptions as data is removed.
+                var vals =  g.getValue(row, col);
+                if (vals == null) { continue; }
+                var val = vals[0];
+                var y = g.toDomCoords(null, val)[1];
+                var diff2 = Math.abs(canvasy - y);
+                if (diff2 < RANGE) {
+                  var found = false;
+                  for (var i in processed) {
+                    var stored = processed[i];
+                    if(stored[0] == row && stored[1] == col) {
+                      found = true;
+                      break;
+                    }
+                  }
+                  if (!found) {
+                    processed.push([row, col]);
+                    drawV4(x, y);
+                  }
+                  return;
+                }
+              }
+              // drawV4(false, canvasx, canvasy);
+            }
+          }
+          // drawV4(false, canvasx, canvasy);
+        }
+      }
+
+      function upV4(event, g, context) {
+        if (v4Active) {
+          v4Active = false;
+        }
+      }
+
+      function dblClickV4(event, g, context) {
+        unzoomGraph(g4);
+      }
+
+      function drawV4(x, y) {
+        var ctx = v4Canvas;
+
+        ctx.strokeStyle = "#000000";
+        ctx.fillStyle = "#FFFF00";
+        ctx.beginPath();
+        ctx.arc(x,y,5,0,Math.PI*2,true);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+      }
+
+      function captureCanvas(canvas, area, g) {
+        v4Canvas = canvas;
+      }
+
+      var g = new Dygraph(document.getElementById("div_g"),
+           NoisyData, { errorBars : true });
+      var g2 = new Dygraph(document.getElementById("div_g2"),
+           NoisyData, { errorBars : true, interactionModel : {} });
+      var g3 = new Dygraph(document.getElementById("div_g3"),
+           NoisyData, { errorBars : true, interactionModel : {
+        'mousedown' : downV3,
+        'mousemove' : moveV3,
+        'mouseup' : upV3,
+        'dblclick' : dblClickV3,
+        'mousewheel' : scrollV3
+      }});
+      var g4 = new Dygraph(document.getElementById("div_g4"),
+           NoisyData, { errorBars : true, drawPoints : true, interactionModel : {
+            'mousedown' : downV4,
+            'mousemove' : moveV4,
+            'mouseup' : upV4,
+            'dblclick' : dblClickV4,
+           },
+           underlayCallback : captureCanvas
+      });
+
+      function unzoomGraph(g) {
+        g.updateOptions({
+          dateWindow: null,
+          valueRange: null
+        });
+      }
+    </script>
+
+  </body>
+</html>
index 10b0457..ad0bc9d 100644 (file)
@@ -40,8 +40,8 @@
             document.getElementById("div_g"),
             NoisyData, {
               errorBars: true,
-              zoomCallback : function(minDate, maxDate, yRange) {
-                showDimensions(minDate, maxDate, yRange);
+              zoomCallback : function(minDate, maxDate, yRanges) {
+                showDimensions(minDate, maxDate, yRanges);
               }
             }
           );
       // Pull an initial value for logging.
       var minDate = g.xAxisRange()[0];
       var maxDate = g.xAxisRange()[1];
-      var minValue = g.yAxisRange();
-      showDimensions(minDate, maxDate, yAxisRange);
+      var minValue = g.yAxisRange()[0];
+      var maxValue = g.yAxisRange()[1];
+      showDimensions(minDate, maxDate, [minValue, maxValue]);
 
-      function showDimensions(minDate, maxDate, yAxisRange) {
+      function showDimensions(minDate, maxDate, yRanges) {
         showXDimensions(minDate, maxDate);
-        showYDimensions(yAxisRange);
+        showYDimensions(yRanges);
       }
 
       function showXDimensions(first, second) {
@@ -65,9 +66,9 @@
         elem.innerHTML = "dateWindow : [" + first + ", "+ second + "]";
       }
 
-      function showYDimensions(range) {
+      function showYDimensions(ranges) {
         var elem = document.getElementById("ydimensions");
-        elem.innerHTML = "valueRange : [" + range + "]";
+        elem.innerHTML = "valueRange : [" + ranges + "]";
       }
 
       function zoomGraphX(minDate, maxDate) {
@@ -81,7 +82,7 @@
         g.updateOptions({
           valueRange: [minValue, maxValue]
         });
-        showYDimensions(minValue, maxValue);
+        showYDimensions(g.yAxisRanges());
       }
 
       function unzoomGraph() {