merge in kberg changes
authorDan Vanderkam <dan@dygraphs.com>
Thu, 2 Jun 2011 21:33:29 +0000 (17:33 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Thu, 2 Jun 2011 21:33:29 +0000 (17:33 -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 4ea5ef9..155a4b4 100644 (file)
@@ -70,7 +70,7 @@ DygraphLayout.prototype.setAnnotations = function(ann) {
       return;
     }
     Dygraph.update(a, ann[i]);
-    if (!a.xval) a.xval = parse(a.x);
+    if (!a.xval) a.xval = parse(a.x, this.dygraph_);
     this.annotations.push(a);
   }
 };
index d89056e..9235d49 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) {
@@ -4016,7 +4077,7 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
   "xValueParser": {
     "default": "parseFloat() or Date.parse()*",
     "labels": ["CSV parsing"],
-    "type": "function(str) -> number",
+    "type": "function(str[, dygraph]) -> number",
     "description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
   },
   "stackedGraph": {