merge upstream changes
authorDan Vanderkam <dan@dygraphs.com>
Mon, 6 Jun 2011 13:15:53 +0000 (09:15 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Mon, 6 Jun 2011 13:15:53 +0000 (09:15 -0400)
auto_tests/misc/local.html
auto_tests/tests/DygraphOps.js
auto_tests/tests/interaction_model.js [new file with mode: 0644]
auto_tests/tests/range_tests.js
dygraph-canvas.js
dygraph.js

index 6f0fd66..57764b1 100644 (file)
@@ -25,6 +25,7 @@
   <script type="text/javascript" src="../tests/axis_labels.js"></script>
   <script type="text/javascript" src="../tests/multi_csv.js"></script>
   <script type="text/javascript" src="../tests/to_dom_coords.js"></script>
+  <script type="text/javascript" src="../tests/interaction_model.js"></script>
 </head>
 <body>
   <div id='graph'></div>
index 072d992..5dfa2be 100644 (file)
@@ -90,12 +90,9 @@ DygraphOps.dispatchDoubleClick = function(g, custom) {
   g.canvas_.dispatchEvent(event);
 };
 
-DygraphOps.dispatchMouseDown = function(g, x, y, custom) {
-  var px = Dygraph.findPosX(g.canvas_);
-  var py = Dygraph.findPosY(g.canvas_);
-
-  var pageX = px + g.toDomXCoord(x);
-  var pageY = py + g.toDomYCoord(y);
+DygraphOps.dispatchMouseDown_Point = function(g, x, y, custom) {
+  var pageX = Dygraph.findPosX(g.canvas_) + x;
+  var pageY = Dygraph.findPosY(g.canvas_) + y;
 
   var opts = {
     type : 'mousedown',
@@ -108,14 +105,11 @@ DygraphOps.dispatchMouseDown = function(g, x, y, custom) {
 
   var event = DygraphOps.createEvent_(opts, custom);
   g.canvas_.dispatchEvent(event);
-};
-
-DygraphOps.dispatchMouseMove = function(g, x, y, custom) {
-  var px = Dygraph.findPosX(g.canvas_);
-  var py = Dygraph.findPosY(g.canvas_);
+}
 
-  var pageX = px + g.toDomXCoord(x);
-  var pageY = py + g.toDomYCoord(y);
+DygraphOps.dispatchMouseMove_Point = function(g, x, y, custom) {
+  var pageX = Dygraph.findPosX(g.canvas_) + x;
+  var pageY = Dygraph.findPosY(g.canvas_) + y;
 
   var opts = {
     type : 'mousemove',
@@ -129,12 +123,9 @@ DygraphOps.dispatchMouseMove = function(g, x, y, custom) {
   g.canvas_.dispatchEvent(event);
 };
 
-DygraphOps.dispatchMouseUp = function(g, x, y, custom) {
-  var px = Dygraph.findPosX(g.canvas_);
-  var py = Dygraph.findPosY(g.canvas_);
-
-  var pageX = px + g.toDomXCoord(x);
-  var pageY = py + g.toDomYCoord(y);
+DygraphOps.dispatchMouseUp_Point = function(g, x, y, custom) {
+  var pageX = Dygraph.findPosX(g.canvas_) + x;
+  var pageY = Dygraph.findPosY(g.canvas_) + y;
 
   var opts = {
     type : 'mouseup',
@@ -147,3 +138,40 @@ DygraphOps.dispatchMouseUp = function(g, x, y, custom) {
   var event = DygraphOps.createEvent_(opts, custom);
   g.canvas_.dispatchEvent(event);
 };
+
+/**
+ * Dispatches a mouse down using the graph's data coordinate system.
+ * (The y value mapped to the first axis.)
+ */
+DygraphOps.dispatchMouseDown = function(g, x, y, custom) {
+  DygraphOps.dispatchMouseDown_Point(
+      g,
+      g.toDomXCoord(x),
+      g.toDomYCoord(y),
+      custom);
+};
+
+/**
+ * Dispatches a mouse move using the graph's data coordinate system.
+ * (The y value mapped to the first axis.)
+ */
+DygraphOps.dispatchMouseMove = function(g, x, y, custom) {
+  DygraphOps.dispatchMouseMove_Point(
+      g,
+      g.toDomXCoord(x),
+      g.toDomYCoord(y),
+      custom);
+};
+
+/**
+ * Dispatches a mouse up using the graph's data coordinate system.
+ * (The y value mapped to the first axis.)
+ */
+DygraphOps.dispatchMouseUp = function(g, x, y, custom) {
+  DygraphOps.dispatchMouseUp_Point(
+      g,
+      g.toDomXCoord(x),
+      g.toDomYCoord(y),
+      custom);
+};
+
diff --git a/auto_tests/tests/interaction_model.js b/auto_tests/tests/interaction_model.js
new file mode 100644 (file)
index 0000000..9147f1b
--- /dev/null
@@ -0,0 +1,216 @@
+/** 
+ * @fileoverview Test cases for the interaction model.
+ *
+ * @author konigsberg@google.com (Robert Konigsbrg)
+ */
+var InteractionModelTestCase = TestCase("interaction-model");
+
+InteractionModelTestCase.prototype.setUp = function() {
+  document.body.innerHTML = "<div id='graph'></div>";
+};
+
+InteractionModelTestCase.prototype.tearDown = function() {
+};
+
+var data1 = "X,Y\n" +
+    "20,-1\n" +
+    "21,0\n" +
+    "22,1\n" +
+    "23,0\n";
+
+var data2 =
+    [[1, 10],
+    [2, 20],
+    [3, 30],
+    [4, 40],
+    [5, 120],
+    [6, 50],
+    [7, 70],
+    [8, 90],
+    [9, 50]];
+
+function getXLabels() {
+  var x_labels = document.getElementsByClassName("dygraph-axis-label-x");
+  var ary = [];
+  for (var i = 0; i < x_labels.length; i++) {
+    ary.push(x_labels[i].innerHTML);
+  }
+  return ary;
+}
+
+InteractionModelTestCase.prototype.pan = function(g, xRange, yRange) {
+  var originalXRange = g.xAxisRange();
+  var originalYRange = g.yAxisRange(0);
+
+  DygraphOps.dispatchMouseDown(g, xRange[0], yRange[0]);
+  DygraphOps.dispatchMouseMove(g, xRange[1], yRange[0]); // this is really necessary.
+  DygraphOps.dispatchMouseUp(g, xRange[1], yRange[0]);
+
+  assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
+  // assertEqualsDelta(originalYRange, g.yAxisRange(0), 0.2); // Not true, it's something in the middle.
+
+  var midX = (xRange[1] - xRange[0]) / 2;
+  DygraphOps.dispatchMouseDown(g, midX, yRange[0]);
+  DygraphOps.dispatchMouseMove(g, midX, yRange[1]); // this is really necessary.
+  DygraphOps.dispatchMouseUp(g, midX, yRange[1]);
+
+  assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
+  assertEqualsDelta(yRange, g.yAxisRange(0), 0.2);
+}
+
+/**
+ * This tests that when changing the interaction model so pan is used instead
+ * of zoom as the default behavior, a standard click method is still called.
+ */
+InteractionModelTestCase.prototype.testClickCallbackIsCalled = function() {
+  var clicked;
+
+  var clickCallback = function(event, x) {
+    clicked = x;
+  };
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data1,
+      {
+        width: 100,
+        height : 100,
+        clickCallback : clickCallback
+      });
+
+  DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+  DygraphOps.dispatchMouseMove_Point(g, 10, 10);
+  DygraphOps.dispatchMouseUp_Point(g, 10, 10);
+
+  assertEquals(20, clicked);
+};
+
+/**
+ * This tests that when changing the interaction model so pan is used instead
+ * of zoom as the default behavior, a standard click method is still called.
+ */
+InteractionModelTestCase.prototype.testClickCallbackIsCalledOnCustomPan = function() {
+  var clicked;
+
+  var clickCallback = function(event, x) {
+    clicked = x;
+  };
+
+  function customDown(event, g, context) {
+    context.initializeMouseDown(event, g, context);
+    Dygraph.startPan(event, g, context);
+  }
+
+  function customMove(event, g, context) {
+    Dygraph.movePan(event, g, context);
+  }
+
+  function customUp(event, g, context) {
+    Dygraph.endPan(event, g, context);
+  }
+
+  var opts = {
+    width: 100,
+    height : 100,
+    clickCallback : clickCallback,
+    interactionModel : {
+      'mousedown' : customDown,
+      'mousemove' : customMove,
+      'mouseup' : customUp,
+    }
+  };
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data1, opts);
+
+  DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+  DygraphOps.dispatchMouseMove_Point(g, 10, 10);
+  DygraphOps.dispatchMouseUp_Point(g, 10, 10);
+
+  assertEquals(20, clicked);
+};
+
+InteractionModelTestCase.clickAt = function(g, x, y) {
+  DygraphOps.dispatchMouseDown(g, x, y);
+  DygraphOps.dispatchMouseMove(g, x, y);
+  DygraphOps.dispatchMouseUp(g, x, y);
+}
+
+/**
+ * A sanity test to ensure pointClickCallback is called.
+ */
+InteractionModelTestCase.prototype.testPointClickCallback = function() {
+  var clicked;
+  var g = new Dygraph(document.getElementById("graph"), data2, {
+    pointClickCallback : function(event, point) {
+      clicked = point;
+    }
+  });
+
+  InteractionModelTestCase.clickAt(g, 4, 40);
+
+  assertEquals(4, clicked.xval);
+  assertEquals(40, clicked.yval);
+};
+
+/**
+ * A sanity test to ensure pointClickCallback is not called when out of range.
+ */
+InteractionModelTestCase.prototype.testNoPointClickCallbackWhenOffPoint = function() {
+  var clicked;
+  var g = new Dygraph(document.getElementById("graph"), data2, {
+    pointClickCallback : function(event, point) {
+      clicked = point;
+    }
+  });
+
+  InteractionModelTestCase.clickAt(g, 5, 40);
+
+  assertUndefined(clicked);
+};
+
+/**
+ * Ensures pointClickCallback circle size is taken into account.
+ */
+InteractionModelTestCase.prototype.testPointClickCallback_circleSize = function() {
+  // TODO(konigsberg): Implement.
+};
+
+/**
+ * Ensures that pointClickCallback is called prior to clickCallback
+ */
+InteractionModelTestCase.prototype.testPointClickCallbackCalledPriorToClickCallback = function() {
+  var counter = 0;
+  var pointClicked;
+  var clicked;
+  var g = new Dygraph(document.getElementById("graph"), data2, {
+    pointClickCallback : function(event, point) {
+      counter++;
+      pointClicked = counter;
+    },
+    clickCallback : function(event, point) {
+      counter++;
+      clicked = counter;
+    }
+  });
+
+  InteractionModelTestCase.clickAt(g, 4, 40);
+  assertEquals(1, pointClicked);
+  assertEquals(2, clicked);
+};
+
+/**
+ * Ensures that when there's no pointClickCallback, clicking on a point still calls
+ * clickCallback
+ */
+InteractionModelTestCase.prototype.testClickCallback_clickOnPoint = function() {
+  var clicked;
+  var g = new Dygraph(document.getElementById("graph"), data2, {
+    clickCallback : function(event, point) {
+      clicked = 1;
+    }
+  });
+
+  InteractionModelTestCase.clickAt(g, 4, 40);
+  assertEquals(1, clicked);
+};
+
index 561ceeb..32c511e 100644 (file)
@@ -84,7 +84,6 @@ RangeTestCase.prototype.zoom = function(g, xRange, yRange) {
   var originalXRange = g.xAxisRange();
   var originalYRange = g.yAxisRange(0);
 
-  // Editing e.shiftKey post construction doesn't work for Firefox. Damn.
   DygraphOps.dispatchMouseDown(g, xRange[0], yRange[0]);
   DygraphOps.dispatchMouseMove(g, xRange[1], yRange[0]); // this is really necessary.
   DygraphOps.dispatchMouseUp(g, xRange[1], yRange[0]);
@@ -115,10 +114,8 @@ RangeTestCase.prototype.testEmptyUpdateOptions_doesntUnzoom = function() {
 
   g.updateOptions({});
 
-  // This currently fails.
-  // See http://code.google.com/p/dygraphs/issues/detail?id=192
   assertEqualsDelta([11, 18], g.xAxisRange(), 0.1);
-  // assertEqualsDelta([35, 40], g.yAxisRange(0), 0.2);
+  assertEqualsDelta([35, 40], g.yAxisRange(0), 0.2);
 }
 
 /**
index 971a038..04280c4 100644 (file)
  * tick mark labels, the title and the x/y-axis labels.
  * This class is based on PlotKit.CanvasRenderer.
  *
+ * Creates a new DygraphLayout object.
+ * @return {Object} The DygraphLayout object
+ */
+DygraphLayout = function(dygraph) {
+  this.dygraph_ = dygraph;
+  this.datasets = new Array();
+  this.annotations = new Array();
+  this.yAxes_ = null;
+
+  // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and
+  // yticks are outputs. Clean this up.
+  this.xTicks_ = null;
+  this.yTicks_ = null;
+};
+
+DygraphLayout.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+DygraphLayout.prototype.addDataset = function(setname, set_xy) {
+  this.datasets[setname] = set_xy;
+};
+
+DygraphLayout.prototype.setAnnotations = function(ann) {
+  // The Dygraph object's annotations aren't parsed. We parse them here and
+  // save a copy. If there is no parser, then the user must be using raw format.
+  this.annotations = [];
+  var parse = this.attr_('xValueParser') || function(x) { return x; };
+  for (var i = 0; i < ann.length; i++) {
+    var a = {};
+    if (!ann[i].xval && !ann[i].x) {
+      this.dygraph_.error("Annotations must have an 'x' property");
+      return;
+    }
+    if (ann[i].icon &&
+        !(ann[i].hasOwnProperty('width') &&
+          ann[i].hasOwnProperty('height'))) {
+      this.dygraph_.error("Must set width and height when setting " +
+                          "annotation.icon property");
+      return;
+    }
+    Dygraph.update(a, ann[i]);
+    if (!a.xval) a.xval = parse(a.x, this.dygraph_);
+    this.annotations.push(a);
+  }
+};
+
+DygraphLayout.prototype.setXTicks = function(xTicks) {
+  this.xTicks_ = xTicks;
+};
+
+// TODO(danvk): add this to the Dygraph object's API or move it into Layout.
+DygraphLayout.prototype.setYAxes = function (yAxes) {
+  this.yAxes_ = yAxes;
+};
+
+DygraphLayout.prototype.setDateWindow = function(dateWindow) {
+  this.dateWindow_ = dateWindow;
+};
+
+DygraphLayout.prototype.evaluate = function() {
+  this._evaluateLimits();
+  this._evaluateLineCharts();
+  this._evaluateLineTicks();
+  this._evaluateAnnotations();
+};
+
+DygraphLayout.prototype._evaluateLimits = function() {
+  this.minxval = this.maxxval = null;
+  if (this.dateWindow_) {
+    this.minxval = this.dateWindow_[0];
+    this.maxxval = this.dateWindow_[1];
+  } else {
+    for (var name in this.datasets) {
+      if (!this.datasets.hasOwnProperty(name)) continue;
+      var series = this.datasets[name];
+      if (series.length > 1) {
+        var x1 = series[0][0];
+        if (!this.minxval || x1 < this.minxval) this.minxval = x1;
+  
+        var x2 = series[series.length - 1][0];
+        if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
+      }
+    }
+  }
+  this.xrange = this.maxxval - this.minxval;
+  this.xscale = (this.xrange != 0 ? 1/this.xrange : 1.0);
+
+  for (var i = 0; i < this.yAxes_.length; i++) {
+    var axis = this.yAxes_[i];
+    axis.minyval = axis.computedValueRange[0];
+    axis.maxyval = axis.computedValueRange[1];
+    axis.yrange = axis.maxyval - axis.minyval;
+    axis.yscale = (axis.yrange != 0 ? 1.0 / axis.yrange : 1.0);
+
+    if (axis.g.attr_("logscale")) {
+      axis.ylogrange = Dygraph.log10(axis.maxyval) - Dygraph.log10(axis.minyval);
+      axis.ylogscale = (axis.ylogrange != 0 ? 1.0 / axis.ylogrange : 1.0);
+      if (!isFinite(axis.ylogrange) || isNaN(axis.ylogrange)) {
+        axis.g.error('axis ' + i + ' of graph at ' + axis.g +
+            ' can\'t be displayed in log scale for range [' +
+            axis.minyval + ' - ' + axis.maxyval + ']');
+      }
+    }
+  }
+};
+
+DygraphLayout.prototype._evaluateLineCharts = function() {
+  // add all the rects
+  this.points = new Array();
+  for (var setName in this.datasets) {
+    if (!this.datasets.hasOwnProperty(setName)) continue;
+
+    var dataset = this.datasets[setName];
+    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+
+    for (var j = 0; j < dataset.length; j++) {
+      var item = dataset[j];
+
+      var yval;
+      if (axis.logscale) {
+        yval = 1.0 - ((Dygraph.log10(parseFloat(item[1])) - Dygraph.log10(axis.minyval)) * axis.ylogscale); // really should just be yscale.
+      } else {
+        yval = 1.0 - ((parseFloat(item[1]) - axis.minyval) * axis.yscale);
+      }
+      var point = {
+        // TODO(danvk): here
+        x: ((parseFloat(item[0]) - this.minxval) * this.xscale),
+        y: yval,
+        xval: parseFloat(item[0]),
+        yval: parseFloat(item[1]),
+        name: setName
+      };
+
+      this.points.push(point);
+    }
+  }
+};
+
+DygraphLayout.prototype._evaluateLineTicks = function() {
+  this.xticks = new Array();
+  for (var i = 0; i < this.xTicks_.length; i++) {
+    var tick = this.xTicks_[i];
+    var label = tick.label;
+    var pos = this.xscale * (tick.v - this.minxval);
+    if ((pos >= 0.0) && (pos <= 1.0)) {
+      this.xticks.push([pos, label]);
+    }
+  }
+
+  this.yticks = new Array();
+  for (var i = 0; i < this.yAxes_.length; i++ ) {
+    var axis = this.yAxes_[i];
+    for (var j = 0; j < axis.ticks.length; j++) {
+      var tick = axis.ticks[j];
+      var label = tick.label;
+      var pos = this.dygraph_.toPercentYCoord(tick.v, i);
+      if ((pos >= 0.0) && (pos <= 1.0)) {
+        this.yticks.push([i, pos, label]);
+      }
+    }
+  }
+};
+
+
+/**
+ * Behaves the same way as PlotKit.Layout, but also copies the errors
+ * @private
+ */
+DygraphLayout.prototype.evaluateWithError = function() {
+  this.evaluate();
+  if (!(this.attr_('errorBars') || this.attr_('customBars'))) return;
+
+  // Copy over the error terms
+  var i = 0; // index in this.points
+  for (var setName in this.datasets) {
+    if (!this.datasets.hasOwnProperty(setName)) continue;
+    var j = 0;
+    var dataset = this.datasets[setName];
+    for (var j = 0; j < dataset.length; j++, i++) {
+      var item = dataset[j];
+      var xv = parseFloat(item[0]);
+      var yv = parseFloat(item[1]);
+
+      if (xv == this.points[i].xval &&
+          yv == this.points[i].yval) {
+        this.points[i].errorMinus = parseFloat(item[2]);
+        this.points[i].errorPlus = parseFloat(item[3]);
+      }
+    }
+  }
+};
+
+DygraphLayout.prototype._evaluateAnnotations = function() {
+  // Add the annotations to the point to which they belong.
+  // Make a map from (setName, xval) to annotation for quick lookups.
+  var annotations = {};
+  for (var i = 0; i < this.annotations.length; i++) {
+    var a = this.annotations[i];
+    annotations[a.xval + "," + a.series] = a;
+  }
+
+  this.annotated_points = [];
+  for (var i = 0; i < this.points.length; i++) {
+    var p = this.points[i];
+    var k = p.xval + "," + p.name;
+    if (k in annotations) {
+      p.annotation = annotations[k];
+      this.annotated_points.push(p);
+    }
+  }
+};
+
+/**
+ * Convenience function to remove all the data sets from a graph
+ */
+DygraphLayout.prototype.removeAllDatasets = function() {
+  delete this.datasets;
+  this.datasets = new Array();
+};
+
+/**
+ * Return a copy of the point at the indicated index, with its yval unstacked.
+ * @param int index of point in layout_.points
+ */
+DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
+  var point = this.points[idx];
+  
+  // Clone the point since we modify it
+  var unstackedPoint = {};  
+  for (var i in point) {
+    unstackedPoint[i] = point[i];
+  }
+  
+  if (!this.attr_("stackedGraph")) {
+    return unstackedPoint;
+  }
+  
+  // The unstacked yval is equal to the current yval minus the yval of the 
+  // next point at the same xval.
+  for (var i = idx+1; i < this.points.length; i++) {
+    if (this.points[i].xval == point.xval) {
+      unstackedPoint.yval -= this.points[i].yval; 
+      break;
+    }
+  }
+  
+  return unstackedPoint;
+}  
+
+/**
+ * The DygraphCanvasRenderer class does the actual rendering of the chart onto
+ * a canvas. It's based on PlotKit.CanvasRenderer.
+>>>>>>> master
  * @param {Object} element The canvas to attach to
  * @param {Object} elementContext The 2d context of the canvas (injected so it
  * can be mocked for testing.)
index d1052b3..51f15ee 100644 (file)
@@ -448,9 +448,11 @@ Dygraph.prototype.xAxisExtremes = function() {
  */
 Dygraph.prototype.yAxisRange = function(idx) {
   if (typeof(idx) == "undefined") idx = 0;
-  if (idx < 0 || idx >= this.axes_.length) return null;
-  return [ this.axes_[idx].computedValueRange[0],
-           this.axes_[idx].computedValueRange[1] ];
+  if (idx < 0 || idx >= this.axes_.length) {
+    return null;
+  }
+  var axis = this.axes_[idx];
+  return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
 };
 
 /**
@@ -1227,7 +1229,7 @@ Dygraph.Interaction.movePan = function(event, g, context) {
     }
   }
 
-  g.drawGraph_();
+  g.drawGraph_(false);
 };
 
 /**
@@ -1244,7 +1246,19 @@ 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 = 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) {
+    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
+  }
+
   // TODO(konigsberg): Clear the context data from the axis.
+  // (replace with "context = {}" ?)
   // TODO(konigsberg): mouseup should just delete the
   // context object, and mousedown should create a new one.
   context.isPanning = false;
@@ -1311,6 +1325,45 @@ Dygraph.Interaction.moveZoom = function(event, g, context) {
   context.prevDragDirection = context.dragDirection;
 };
 
+Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
+  var clickCallback = g.attr_('clickCallback');
+  var pointClickCallback = g.attr_('pointClickCallback');
+
+  var selectedPoint = null;
+
+  // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
+  if (pointClickCallback) {
+    var closestIdx = -1;
+    var closestDistance = Number.MAX_VALUE;
+
+    // check if the click was on a particular point.
+    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;
+      }
+    }
+
+    // Allow any click within two pixels of the dot.
+    var radius = g.attr_('highlightCircleSize') + 2;
+    if (closestDistance <= radius * radius) {
+      selectedPoint = g.selPoints_[closestIdx];
+    }
+  }
+
+  if (selectedPoint) {
+    pointClickCallback(event, selectedPoint);
+  }
+
+  // TODO(danvk): pass along more info about the points, e.g. 'x'
+  if (clickCallback) {
+    clickCallback(event, g.lastx_, g.selPoints_);
+  }
+};
+
 /**
  * Called in response to an interaction model operation that
  * responds to an event that performs a zoom based on previously defined
@@ -1326,7 +1379,6 @@ Dygraph.Interaction.moveZoom = function(event, g, context) {
  * dragStartX/dragStartY/etc. properties). This function modifies the context.
  */
 Dygraph.Interaction.endZoom = function(event, g, context) {
-  // TODO(konigsberg): Refactor or rename this fn -- it deals with clicks, too.
   context.isZooming = false;
   context.dragEndX = g.dragGetX_(event, context);
   context.dragEndY = g.dragGetY_(event, context);
@@ -1335,30 +1387,7 @@ Dygraph.Interaction.endZoom = function(event, g, context) {
 
   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;
-        }
-      }
-
-      // 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]);
-      }
-    }
+    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
   }
 
   if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
@@ -2661,9 +2690,19 @@ Dygraph.prototype.predraw_ = function() {
  * Update the graph with new data. This method is called when the viewing area
  * has changed. If the underlying data or options have changed, predraw_ will
  * be called before drawGraph_ is called.
+ *
+ * clearSelection, when undefined or true, causes this.clearSelection to be
+ * called at the end of the draw operation. This should rarely be defined,
+ * and never true (that is it should be undefined most of the time, and
+ * rarely false.)
+ *
  * @private
  */
-Dygraph.prototype.drawGraph_ = function() {
+Dygraph.prototype.drawGraph_ = function(clearSelection) {
+  if (typeof(clearSelection) === 'undefined') {
+    clearSelection = true;
+  }
+
   var data = this.rawData_;
 
   // This is used to set the second parameter to drawCallback, below.
@@ -2806,13 +2845,15 @@ Dygraph.prototype.drawGraph_ = function() {
     // Generate a static legend before any particular point is selected.
     this.setLegendHTML_();
   } else {
-    if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
-      // We should select the point nearest the page x/y here, but it's easier
-      // to just clear the selection. This prevents erroneous hover dots from
-      // being displayed.
-      this.clearSelection();
-    } else {
-      this.clearSelection();
+    if (clearSelection) {
+      if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
+        // We should select the point nearest the page x/y here, but it's easier
+        // to just clear the selection. This prevents erroneous hover dots from
+        // being displayed.
+        this.clearSelection();
+      } else {
+        this.clearSelection();
+      }
     }
   }
 
@@ -2833,6 +2874,17 @@ Dygraph.prototype.drawGraph_ = function() {
  *   indices are into the axes_ array.
  */
 Dygraph.prototype.computeYAxes_ = function() {
+  // Preserve valueWindow settings if they exist, and if the user hasn't
+  // specified a new valueRange.
+  var valueWindows;
+  if (this.axes_ != undefined && this.user_attrs_.hasOwnProperty("valueRange") == false) {
+    valueWindows = [];
+    for (var index = 0; index < this.axes_.length; index++) {
+      valueWindows.push(this.axes_[index].valueWindow);
+    }
+  }
+
+
   this.axes_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
   this.seriesToAxisMap_ = {};
 
@@ -2909,6 +2961,13 @@ Dygraph.prototype.computeYAxes_ = function() {
     if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
   }
   this.seriesToAxisMap_ = seriesToAxisFiltered;
+
+  if (valueWindows != undefined) {
+    // Restore valueWindow settings.
+    for (var index = 0; index < valueWindows.length; index++) {
+      this.axes_[index].valueWindow = valueWindows[index];
+    }
+  }
 };
 
 /**
@@ -3631,6 +3690,8 @@ Dygraph.dateStrToMillis = function(str) {
 
 // These functions are all based on MochiKit.
 /**
+ * Copies all the properties from o to self.
+ *
  * @private
  */
 Dygraph.update = function (self, o) {