Introduce interaction model API.
authorRobert Konigsberg <konigsberg@google.com>
Tue, 14 Dec 2010 21:35:08 +0000 (16:35 -0500)
committerRobert Konigsberg <konigsberg@google.com>
Tue, 14 Dec 2010 21:35:08 +0000 (16:35 -0500)
dygraph.js

index e28c273..f3f2107 100644 (file)
@@ -37,7 +37,7 @@
 
  And error bars will be calculated automatically using a binomial distribution.
 
- For further documentation and examples, see http://www.danvk.org/dygraphs
+ For further documentation and examples, see http://dygraphs.com/
 
  */
 
@@ -127,7 +127,7 @@ Dygraph.DEFAULT_ATTRS = {
   hideOverlayOnMouseOut: true,
 
   stepPlot: false,
-  avoidMinZero: false
+  avoidMinZero: false,
 };
 
 // Various logging levels.
@@ -158,7 +158,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
@@ -430,6 +430,28 @@ Dygraph.addEvent = function(el, evt, fn) {
   }
 };
 
+
+//
+// An attempt at scroll wheel management.
+//
+// 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
@@ -762,249 +784,309 @@ 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;
+Dygraph.startPan = function(event, g, context) {
+  // have to be zoomed in to pan.
+  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;
+  }
+
+  // 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];
+};
+
+Dygraph.movePan = function(event, g, context) {
+  context.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.dragGetY_(event, context);
 
   // TODO(danvk): update this comment
-  // The range in second/value units that the viewport encompasses during a
-  // panning operation.
-  var dateRange = null;
-
-  // 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 };
-
-  // 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 ];
-        }
-      }
+  // 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.
 
-      self.drawGraph_();
-    }
-  });
+  var minDate = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange;
+  var maxDate = minDate + context.dateRange;
+  g.dateWindow_ = [minDate, maxDate];
 
-  // 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;  
+  // 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;
-        }
-      }
-      if (!self.dateWindow_ && !zoomedY) return;
+Dygraph.endPan = function(event, g, context) {
+  context.isPanning = false;
+  context.is2DPan = false;
+  context.draggingDate = null;
+  context.dateRange = null;
+  context.valueRange = null;
+}
 
-      isPanning = true;
-      var xRange = self.xAxisRange();
-      dateRange = xRange[1] - xRange[0];
+Dygraph.startZoom = function(event, g, context) {
+  context.isZooming = true;
+}
 
-      // 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;
+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;
+}
+
+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;
+        }
       }
 
-      // TODO(konigsberg): Switch from all this math to toDataCoords?
-      // Seems to work for the dragging value.
-      draggingDate = (dragStartX / self.width_) * dateRange + xRange[0];
-    } else {
-      isZooming = 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 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;
-    }
+  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;
+}
 
-    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;
-      }
-    }
-  });
+// Track the beginning of drag events
+Dygraph.prototype.defaultMouseDownFunction = function(event, g, context) {
+  context.initializeMouseDown(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;
-    }
-  });
+  if (event.altKey || event.shiftKey) {
+    Dygraph.startPan(event, g, context);
+  } else {
+    Dygraph.startZoom(event, g, context);
+  }
+};
 
-  // 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;
-            }
-          }
+// Draw zoom rectangles when the mouse is down and the user moves around
+Dygraph.prototype.defaultMouseMoveFunction = function(event, g, context) {
+  if (context.isZooming) {
+    Dygraph.moveZoom(event, g, context);
+  } else if (context.isPanning) {
+    Dygraph.movePan(event, g, context);
+  }
+};
 
-          // 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.prototype.defaultMouseUpFunction = function(event, g, context) {
+  if (context.isZooming) {
+    Dygraph.endZoom(event, g, context);
+  } else if (context.isPanning) {
+    Dygraph.endPan(event, g, context);
+  }
+};
+
+Dygraph.prototype.defaultMouseOutFunction = function(event, g, context) {
+  // Temporarily cancel the dragging event when the mouse leaves the graph
+  if (context.isZooming) {
+    context.dragEndX = null;
+    context.dragEndY = null;
+  }
+};
+
+// Double-clicking zooms back out
+Dygraph.prototype.defaultMouseDoubleClickFunction = function(event, g, context) {
+  // Disable zooming out if panning.
+  if (event.altKey || event.shiftKey) {
+    return;
+  }
+  g.doUnzoom_();
+};
 
-      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;
-    }
-  });
+  // Defines default behavior if there are no event handlers.
+  var handlers = this.user_attrs_.interactionModel || {
+    'mousedown' : this.defaultMouseDownFunction,
+    'mousemove' : this.defaultMouseMoveFunction,
+    'mouseup' : this.defaultMouseUpFunction,
+    'mouseout' : this.defaultMouseOutFunction,
+    'dblclick' : this.defaultMouseDoubleClickFunction
+  };
+
+  // Function that binds g and context to the handler.
+  var bindHandler = function(handler, g) {
+    return function(event) {
+      handler(event, g, context);
+    };
+  };
+
+  for (var eventName in handlers) {
+    Dygraph.addEvent(this.mouseEventElement_, eventName,
+        bindHandler(handlers[eventName], this));
+  }
 
-  // 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;
+
+  // 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;
+      }
+    }
   });
 };
 
 /**
  * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
  * 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
+ * avoid extra redrawing, but it's tricky to avoid contexts with the status
  * dots.
  * 
  * @param {Number} direction the direction of the zoom rectangle. Acceptable
@@ -1171,7 +1253,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;