Fix for issue 112: off-by-one issues with panning.
[dygraphs.git] / dygraph.js
index dd4e7b5..f1ecd42 100644 (file)
@@ -129,8 +129,8 @@ Dygraph.DEFAULT_ATTRS = {
 
   stepPlot: false,
   avoidMinZero: false,
-  interactionModel: null // will be set to Dygraph.defaultInteractionModel.
+
+  interactionModel: null  // will be set to Dygraph.defaultInteractionModel.
 };
 
 // Various logging levels.
@@ -798,22 +798,11 @@ Dygraph.prototype.dragGetY_ = function(e, context) {
 // 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];
+  context.initialLeftmostDate = xRange[0];
+  context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
 
   // 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.
@@ -821,15 +810,12 @@ Dygraph.startPan = function(event, g, context) {
   for (var i = 0; i < g.axes_.length; i++) {
     var axis = g.axes_[i];
     var yRange = g.yAxisRange(i);
+    // TODO(konigsberg): These values should be in |context|.
     axis.dragValueRange = yRange[1] - yRange[0];
-    var r = g.toDataCoords(null, context.dragStartY, i);
-    axis.draggingValue = r[1];
+    axis.initialTopValue = yRange[1];
+    axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 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];
 };
 
 // Called in response to an interaction model operation that
@@ -843,14 +829,8 @@ Dygraph.movePan = function(event, g, context) {
   context.dragEndX = g.dragGetX_(event, context);
   context.dragEndY = g.dragGetY_(event, context);
 
-  // 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 minDate = context.initialLeftmostDate -
+    (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
   var maxDate = minDate + context.dateRange;
   g.dateWindow_ = [minDate, maxDate];
 
@@ -860,7 +840,8 @@ Dygraph.movePan = function(event, g, context) {
     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 maxValue = axis.initialTopValue +
+        (context.dragEndY - context.dragStartY) * axis.unitsPerPixel;
       var minValue = maxValue - axis.dragValueRange;
       axis.valueWindow = [ minValue, maxValue ];
     }
@@ -877,9 +858,12 @@ Dygraph.movePan = function(event, g, context) {
 // panning behavior.
 //
 Dygraph.endPan = function(event, g, context) {
+  // TODO(konigsberg): Clear the context data from the axis.
+  // TODO(konigsberg): mouseup should just delete the
+  // context object, and mousedown should create a new one.
   context.isPanning = false;
   context.is2DPan = false;
-  context.draggingDate = null;
+  context.initialLeftmostDate = null;
   context.dateRange = null;
   context.valueRange = null;
 }
@@ -987,50 +971,50 @@ Dygraph.endZoom = function(event, g, context) {
 
 Dygraph.defaultInteractionModel = {
   // Track the beginning of drag events
-  mousedown : function(event, g, context) {
-      context.initializeMouseDown(event, g, context);
+  mousedown: function(event, g, context) {
+    context.initializeMouseDown(event, g, context);
 
-      if (event.altKey || event.shiftKey) {
-        Dygraph.startPan(event, g, context);
-      } else {
-        Dygraph.startZoom(event, g, context);
-      }
-    },
+    if (event.altKey || event.shiftKey) {
+      Dygraph.startPan(event, g, context);
+    } 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);
-      }
-    },
+  mousemove: function(event, g, context) {
+    if (context.isZooming) {
+      Dygraph.moveZoom(event, g, context);
+    } else if (context.isPanning) {
+      Dygraph.movePan(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);
-      }
-    },
+  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
-  mouseout : function(event, g, context) {
-      if (context.isZooming) {
-        context.dragEndX = null;
-        context.dragEndY = null;
-      }
-    },
+  mouseout: function(event, g, context) {
+    if (context.isZooming) {
+      context.dragEndX = null;
+      context.dragEndY = null;
+    }
+  },
 
   // 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_();
+  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_();
+  }
 };
 
 Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
@@ -1043,43 +1027,43 @@ Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
 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,
+    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,
+
+    // The value on the left side of the graph when a pan operation starts.
+    initialLeftmostDate: null,
+
+    // The number of units each pixel spans. (This won't be valid for log
+    // scales)
+    xUnitsPerPixel: null,
 
     // TODO(danvk): update this comment
     // The range in second/value units that the viewport encompasses during a
     // panning operation.
-    dateRange : null,
+    dateRange: null,
 
     // Utility function to convert page-wide coordinates to canvas coords
-    px : 0,
-    py : 0,
+    px: 0,
+    py: 0,
 
-    initializeMouseDown : function(event, g, context) {
+    initializeMouseDown: function(event, g, context) {
       // prevents mouse drags from selecting page text.
       if (event.preventDefault) {
         event.preventDefault();  // Firefox, Chrome, etc.
       } else {
         event.returnValue = false;  // IE
-        event.cancelBubble = true;  
+        event.cancelBubble = true;
       }
-  
+
       context.px = Dygraph.findPosX(g.canvas_);
       context.py = Dygraph.findPosY(g.canvas_);
       context.dragStartX = g.dragGetX_(event, context);
@@ -1089,7 +1073,6 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   var interactionModel = this.attr_("interactionModel");
 
-
   // Self is the graph.
   var self = this;
 
@@ -1551,7 +1534,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();
@@ -1653,7 +1638,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;
@@ -1689,6 +1675,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;
@@ -1761,6 +1748,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();