Allow zooms to go 200px past the edge.
authorDan Vanderkam <danvdk@gmail.com>
Wed, 19 Nov 2014 01:21:55 +0000 (20:21 -0500)
committerDan Vanderkam <danvdk@gmail.com>
Wed, 19 Nov 2014 01:21:55 +0000 (20:21 -0500)
This requires registering mousemove/mouseup on document, rather than the
canvas. This is a bit scary! But the tests pass, and this is generally
pretty well-tested code.

dygraph-interaction-model.js
dygraph.js

index c6f370d..e0b7630 100644 (file)
  * @author Robert Konigsberg (konigsberg@google.com)
  */
 
-/*jshint globalstrict: true */
+(function() {
 /*global Dygraph:false */
 "use strict";
 
 /**
+ * You can drag this many pixels past the edge of the chart and still have it
+ * be considered a zoom. This makes it easier to zoom to the exact edge of the
+ * chart, a fairly common operation.
+ */
+var DRAG_EDGE_MARGIN = 200;
+
+/**
  * A collection of functions to facilitate build custom interaction models.
  * @class
  */
 Dygraph.Interaction = {};
 
 /**
+ * Checks whether the beginning & ending of an event were close enough that it
+ * should be considered a click. If it should, dispatch appropriate events.
+ * Returns true if the event was treated as a click.
+ *
+ * @param {Event} event
+ * @param {Dygraph} g
+ * @param {Object} context
+ */
+Dygraph.Interaction.maybeTreatMouseOpAsClick = function(event, g, context) {
+  context.dragEndX = Dygraph.dragGetX_(event, context);
+  context.dragEndY = Dygraph.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) {
+    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
+  }
+
+  context.regionWidth = regionWidth;
+  context.regionHeight = regionHeight;
+}
+
+/**
  * Called in response to an interaction model operation that
  * should start the default panning behavior.
  *
@@ -197,29 +228,7 @@ Dygraph.Interaction.movePan = function(event, g, context) {
  *     dragStartX/dragStartY/etc. properties). This function modifies the
  *     context.
  */
-Dygraph.Interaction.endPan = function(event, g, context) {
-  context.dragEndX = Dygraph.dragGetX_(event, context);
-  context.dragEndY = Dygraph.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) {
-    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
-  }
-
-  // TODO(konigsberg): mouseup should just delete the
-  // context object, and mousedown should create a new one.
-  context.isPanning = false;
-  context.is2DPan = false;
-  context.initialLeftmostDate = null;
-  context.dateRange = null;
-  context.valueRange = null;
-  context.boundedDates = null;
-  context.boundedValues = null;
-  context.axes = null;
-};
+Dygraph.Interaction.endPan = Dygraph.Interaction.maybeTreatMouseOpAsClick;
 
 /**
  * Called in response to an interaction model operation that
@@ -363,21 +372,14 @@ Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
  */
 Dygraph.Interaction.endZoom = function(event, g, context) {
   context.isZooming = false;
-  context.dragEndX = Dygraph.dragGetX_(event, context);
-  context.dragEndY = Dygraph.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) {
-    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
-  }
+  Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
 
   // The zoom rectangle is visibly clipped to the plot area, so its behavior
   // should be as well.
   // See http://code.google.com/p/dygraphs/issues/detail?id=280
   var plotArea = g.getArea();
-  if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
+  if (context.regionWidth >= 10 &&
+      context.dragDirection == Dygraph.HORIZONTAL) {
     var left = Math.min(context.dragStartX, context.dragEndX),
         right = Math.max(context.dragStartX, context.dragEndX);
     left = Math.max(left, plotArea.x);
@@ -386,7 +388,8 @@ Dygraph.Interaction.endZoom = function(event, g, context) {
       g.doZoomX_(left, right);
     }
     context.cancelNextDblclick = true;
-  } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
+  } else if (context.regionHeight >= 10 &&
+             context.dragDirection == Dygraph.VERTICAL) {
     var top = Math.min(context.dragStartY, context.dragEndY),
         bottom = Math.max(context.dragStartY, context.dragEndY);
     top = Math.max(top, plotArea.y);
@@ -585,6 +588,40 @@ Dygraph.Interaction.endTouch = function(event, g, context) {
   }
 };
 
+// Determine the distance from x to [left, right].
+var distanceFromInterval = function(x, left, right) {
+  if (x < left) {
+    return left - x;
+  } else if (x > right) {
+    return x - right;
+  } else {
+    return 0;
+  }
+};
+
+/**
+ * Returns the number of pixels by which the event happens from the nearest
+ * edge of the chart. For events in the interior of the chart, this returns zero.
+ */
+var distanceFromChart = function(event, g) {
+  var chartPos = Dygraph.findPos(g.canvas_);
+  var box = {
+    left: chartPos.x,
+    right: chartPos.x + g.canvas_.offsetWidth,
+    top: chartPos.y,
+    bottom: chartPos.y + g.canvas_.offsetHeight
+  };
+
+  var pt = {
+    x: Dygraph.pageX(event),
+    y: Dygraph.pageY(event)
+  };
+
+  var dx = distanceFromInterval(pt.x, box.left, box.right),
+      dy = distanceFromInterval(pt.y, box.top, box.bottom);
+  return Math.max(dx, dy);
+};
+
 /**
  * Default interation model for dygraphs. You can refer to specific elements of
  * this when constructing your own interaction model, e.g.:
@@ -607,24 +644,47 @@ Dygraph.Interaction.defaultModel = {
     } else {
       Dygraph.startZoom(event, g, context);
     }
-  },
 
-  // 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);
-    }
-  },
+    // Note: we register mousemove/mouseup on document to allow some leeway for
+    // events to move outside of the chart. Interaction model events get
+    // registered on the canvas, which is too small to allow this.
+    var mousemove = function(event) {
+      if (context.isZooming) {
+        // When the mouse moves >200px from the chart edge, cancel the zoom.
+        var d = distanceFromChart(event, g);
+        if (d < 200) {
+          Dygraph.moveZoom(event, g, context);
+        } else {
+          if (context.dragEndX !== null) {
+            context.dragEndX = null;
+            context.dragEndY = null;
+            g.clearZoomRect_();
+          }
+        }
+      } else if (context.isPanning) {
+        Dygraph.movePan(event, g, context);
+      }
+    };
+    var mouseup = function(event) {
+      if (context.isZooming) {
+        if (context.dragEndX !== null) {
+          Dygraph.endZoom(event, g, context);
+        } else {
+          Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
+        }
+      } else if (context.isPanning) {
+        Dygraph.endPan(event, g, context);
+      }
 
-  mouseup: function(event, g, context) {
-    if (context.isZooming) {
-      Dygraph.endZoom(event, g, context);
-    } else if (context.isPanning) {
-      Dygraph.endPan(event, g, context);
-    }
+      Dygraph.removeEvent(document, 'mousemove', mousemove);
+      Dygraph.removeEvent(document, 'mouseup', mouseup);
+      context.destroy();
+    };
+
+    g.addAndTrackEvent(document, 'mousemove', mousemove);
+    g.addAndTrackEvent(document, 'mouseup', mouseup);
   },
+  willDestroyContextMyself: true,
 
   touchstart: function(event, g, context) {
     Dygraph.Interaction.startTouch(event, g, context);
@@ -636,15 +696,6 @@ Dygraph.Interaction.defaultModel = {
     Dygraph.Interaction.endTouch(event, g, context);
   },
 
-  // Temporarily cancel the dragging event when the mouse leaves the graph
-  mouseout: function(event, g, context) {
-    if (context.isZooming) {
-      context.dragEndX = null;
-      context.dragEndY = null;
-      g.clearZoomRect_();
-    }
-  },
-
   // Disable zooming out if panning.
   dblclick: function(event, g, context) {
     if (context.cancelNextDblclick) {
@@ -683,18 +734,7 @@ Dygraph.Interaction.nonInteractiveModel_ = {
   mousedown: function(event, g, context) {
     context.initializeMouseDown(event, g, context);
   },
-  mouseup: function(event, g, context) {
-    // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
-    context.dragEndX = Dygraph.dragGetX_(event, context);
-    context.dragEndY = Dygraph.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) {
-      Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
-    }
-  }
+  mouseup: Dygraph.Interaction.maybeTreatMouseOpAsClick
 };
 
 // Default interaction model when using the range selector.
@@ -714,3 +754,5 @@ Dygraph.Interaction.dragIsPanInteractionModel = {
     }
   }
 };
+
+})();
index 7dbf29f..49b13e6 100644 (file)
@@ -1489,6 +1489,26 @@ Dygraph.prototype.createDragInterface_ = function() {
       contextB.dragStartY = Dygraph.dragGetY_(event, contextB);
       contextB.cancelNextDblclick = false;
       contextB.tarp.cover();
+    },
+    destroy: function() {
+      var context = this;
+      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;
+        }
+      }
+
+      context.tarp.uncover();
     }
   };
 
@@ -1512,27 +1532,13 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   // If the user releases the mouse button during a drag, but not over the
   // canvas, then it doesn't count as a zooming action.
-  var mouseUpHandler = 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;
-      }
-    }
-
-    context.tarp.uncover();
-  };
+  if (!interactionModel.willDestroyContextMyself) {
+    var mouseUpHandler = function(event) {
+      context.destroy();
+    };
 
-  this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
+    this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
+  }
 };
 
 /**