Response to code review comments, added missing interaction.html.
authorRobert Konigsberg <konigsberg@google.com>
Wed, 15 Dec 2010 19:24:40 +0000 (14:24 -0500)
committerRobert Konigsberg <konigsberg@google.com>
Wed, 15 Dec 2010 19:24:40 +0000 (14:24 -0500)
dygraph.js
tests/interaction.html [new file with mode: 0644]
tests/zoom.html

index df42c06..a16d2a0 100644 (file)
@@ -79,6 +79,53 @@ Dygraph.DEFAULT_WIDTH = 480;
 Dygraph.DEFAULT_HEIGHT = 320;
 Dygraph.AXIS_LINE_WIDTH = 0.3;
 
+
+Dygraph.DEFAULT_INTERACTION_MODEL = {
+  // Track the beginning of drag events
+  '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);
+      }
+    },
+
+  // 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);
+      }
+    },
+
+  '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;
+      }
+    },
+
+  // Disable zooming out if panning.
+  'dblclick' : function(event, g, context) {
+      if (event.altKey || event.shiftKey) {
+        return;
+      }
+      g.doUnzoom_();
+    }
+};
+
 // Default attribute values.
 Dygraph.DEFAULT_ATTRS = {
   highlightCircleSize: 3,
@@ -128,6 +175,8 @@ Dygraph.DEFAULT_ATTRS = {
 
   stepPlot: false,
   avoidMinZero: false,
+  interactionModel: Dygraph.DEFAULT_INTERACTION_MODEL
 };
 
 // Various logging levels.
@@ -158,7 +207,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
 
 /**
  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
- * and context &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
@@ -431,12 +480,8 @@ 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) {
@@ -451,7 +496,6 @@ Dygraph.cancelEvent = function(e) {
   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
@@ -792,6 +836,13 @@ Dygraph.prototype.dragGetY_ = function(e, context) {
   return Dygraph.pageY(e) - context.py
 };
 
+// Called in response to an interaction model operation that
+// should start the default panning behavior.
+//
+// It's used in the default callback for "mousedown" operations.
+// Custom interaction model builders can use it to provide the default
+// panning behavior.
+//
 Dygraph.startPan = function(event, g, context) {
   // have to be zoomed in to pan.
   var zoomedY = false;
@@ -824,6 +875,13 @@ Dygraph.startPan = function(event, g, context) {
   context.draggingDate = (context.dragStartX / g.width_) * context.dateRange + xRange[0];
 };
 
+// Called in response to an interaction model operation that
+// responds to an event that pans the view.
+//
+// It's used in the default callback for "mousemove" operations.
+// Custom interaction model builders can use it to provide the default
+// panning behavior.
+//
 Dygraph.movePan = function(event, g, context) {
   context.dragEndX = g.dragGetX_(event, context);
   context.dragEndY = g.dragGetY_(event, context);
@@ -854,6 +912,13 @@ Dygraph.movePan = function(event, g, context) {
   g.drawGraph_();
 }
 
+// Called in response to an interaction model operation that
+// responds to an event that ends panning.
+//
+// It's used in the default callback for "mouseup" operations.
+// Custom interaction model builders can use it to provide the default
+// panning behavior.
+//
 Dygraph.endPan = function(event, g, context) {
   context.isPanning = false;
   context.is2DPan = false;
@@ -862,10 +927,24 @@ Dygraph.endPan = function(event, g, context) {
   context.valueRange = null;
 }
 
+// Called in response to an interaction model operation that
+// responds to an event that starts zooming.
+//
+// It's used in the default callback for "mousedown" operations.
+// Custom interaction model builders can use it to provide the default
+// zooming behavior.
+//
 Dygraph.startZoom = function(event, g, context) {
   context.isZooming = true;
 }
 
+// Called in response to an interaction model operation that
+// responds to an event that defines zoom boundaries.
+//
+// It's used in the default callback for "mousemove" operations.
+// Custom interaction model builders can use it to provide the default
+// zooming behavior.
+//
 Dygraph.moveZoom = function(event, g, context) {
   context.dragEndX = g.dragGetX_(event, context);
   context.dragEndY = g.dragGetY_(event, context);
@@ -891,6 +970,14 @@ Dygraph.moveZoom = function(event, g, context) {
   context.prevDragDirection = context.dragDirection;
 }
 
+// Called in response to an interaction model operation that
+// responds to an event that performs a zoom based on previously defined
+// bounds..
+//
+// It's used in the default callback for "mouseup" operations.
+// Custom interaction model builders can use it to provide the default
+// zooming behavior.
+//
 Dygraph.endZoom = function(event, g, context) {
   context.isZooming = false;
   context.dragEndX = g.dragGetX_(event, context);
@@ -941,51 +1028,6 @@ Dygraph.endZoom = function(event, g, context) {
   context.dragStartY = null;
 }
 
-Dygraph.prototype.defaultInteractionModel = {
-  // Track the beginning of drag events
-  '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);
-      }
-    },
-
-  // 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);
-      }
-    },
-
-  '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;
-      }
-    },
-
-  // Disable zooming out if panning.
-  'dblclick' : function(event, g, context) {
-      if (event.altKey || event.shiftKey) {
-        return;
-      }
-      g.doUnzoom_();
-    }
-};
 
 /**
  * Set up all the mouse handlers needed to capture dragging behavior for zoom
@@ -1024,7 +1066,7 @@ Dygraph.prototype.createDragInterface_ = function() {
     py : 0,
 
     initializeMouseDown : function(event, g, context) {
-    // prevents mouse drags from selecting page text.
+      // prevents mouse drags from selecting page text.
       if (event.preventDefault) {
         event.preventDefault();  // Firefox, Chrome, etc.
       } else {
@@ -1039,8 +1081,8 @@ Dygraph.prototype.createDragInterface_ = function() {
     }
   };
 
-  // Defines default behavior if there are no event handlers.
-  var handlers = this.user_attrs_.interactionModel || this.defaultInteractionModel;
+  var interactionModel = this.attr_("interactionModel");
+
 
   // Function that binds g and context to the handler.
   var bindHandler = function(handler, g) {
@@ -1049,9 +1091,10 @@ Dygraph.prototype.createDragInterface_ = function() {
     };
   };
 
-  for (var eventName in handlers) {
+  for (var eventName in interactionModel) {
+    if (!interactionModel.hasOwnProperty(eventName)) continue;
     Dygraph.addEvent(this.mouseEventElement_, eventName,
-        bindHandler(handlers[eventName], this));
+        bindHandler(interactionModel[eventName], this));
   }
 
   // Self is the graph.
@@ -1081,7 +1124,7 @@ Dygraph.prototype.createDragInterface_ = function() {
 /**
  * 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 contexts with the status
+ * avoid extra redrawing, but it's tricky to avoid interactions with the status
  * dots.
  * 
  * @param {Number} direction the direction of the zoom rectangle. Acceptable
diff --git a/tests/interaction.html b/tests/interaction.html
new file mode 100644 (file)
index 0000000..082a54e
--- /dev/null
@@ -0,0 +1,216 @@
+
+<html>
+  <head>
+    <title>interaction model</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+    <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+    <script type="text/javascript" src="../dygraph-canvas.js"></script>
+    <script type="text/javascript" src="../dygraph.js"></script>
+    <script type="text/javascript" src="data.js"></script>
+  </head>
+  <body>
+    <table border='1'>
+    <tr><td>
+    <b>Default interaction model</b>
+    <div id="div_g" style="width:600px; height:300px;"></div>
+    </td><td>Zoom: click-drag<br/>Pan: shift-click-drag<br/>Restore zoom level: double-click<br/>
+    </td></tr>
+    <tr><td>
+    <b>No interaction model</b>
+    <div id="div_g2" style="width:600px; height:300px;"></div>
+    </td><td>Click and drag all you like, it won't do anything!</td></tr>
+    <tr><td>
+    <b>Custom interaction model</b>
+
+    <input type="button" value="Unzoom" onclick="unzoomGraph(g3)">
+    <div id="div_g3" style="width:600px; height:300px;"></div>
+    </td><td>
+    Zoom in: double-click, scroll wheel<br/>
+    Zoom out: ctrl-double-click, scroll wheel<br/>
+    Pan: click-drag<br/>
+    Restore zoom level: press button<br/>
+    </td></tr>
+    <tr><td>
+    <b>Fun model!</b>
+    <div id="div_g4" style="width:600px; height:300px;"></div>
+    </td><td>
+    Keep the mouse button pressed, and hover over all points
+    to mark them.   
+    </td></tr>
+
+    </table>
+
+    <script type="text/javascript">
+      function downV3(event, g, context) {
+        context.initializeMouseDown(event, g, context);
+        Dygraph.startPan(event, g, context);
+      }
+
+      function moveV3(event, g, context) {
+        if (context.isPanning) {
+          Dygraph.movePan(event, g, context);
+        }
+      }
+
+      function upV3(event, g, context) {
+        if (context.isPanning) {
+          Dygraph.endPan(event, g, context);
+        }
+      }
+
+      function dblClickV3(event, g, context) {
+        if (event.ctrlKey) {
+          zoom(g, -(1/9));
+        } else {
+          zoom(g, +.1);
+        }
+      }
+
+      function scrollV3(event, g, context) {
+        var normal = event.detail ? event.detail * -1 : event.wheelDelta / 40;
+        // For me the normalized value shows 0.075 for one click. If I took
+        // that verbatim, it would be a 7.5%. I think I'm gonna take 1/10 of that.
+        // (double for left and right side)
+        var percentage = normal / 100;
+
+        zoom(g, percentage);
+        Dygraph.cancelEvent(event);
+      }
+
+      function zoom(g, percentage) {
+         // Adjusts [x, y] toward each other by percentage%
+         function adjustAxis(axis, percentage) {
+           var delta = axis[1] - axis[0];
+           var increment = delta * percentage;
+           return [ axis[0] + increment, axis[1] - increment ];
+         }
+
+         var yAxes = g.yAxisRanges();
+         var newYAxes = [];
+         for (var i = 0; i < yAxes.length; i++) {
+           newYAxes[i] = adjustAxis(yAxes[i], percentage);
+         }
+
+         g.updateOptions({
+           dateWindow: adjustAxis(g.xAxisRange(), percentage),
+           valueRange: newYAxes[0]
+           });
+      }
+
+      var v4Active = false;
+      var v4Canvas = null;
+
+      function downV4(event, g, context) {
+        context.initializeMouseDown(event, g, context);
+        v4Active = true;
+        moveV4(event, g, context); // in case the mouse went down on a data point.
+      }
+
+      var processed = [];
+
+      function moveV4(event, g, context) {
+        var RANGE = 7;
+
+        if (v4Active) {
+          var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(g.graphDiv);
+          var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(g.graphDiv);
+
+          var rows = g.numRows();
+          // Row layout:
+          // [date, [val1, stdev1], [val2, stdev2]]
+          for (var row = 0; row < rows; row++) {
+            var date = g.getValue(row, 0);
+            var x = g.toDomCoords(date, null)[0];
+            var diff = Math.abs(canvasx - x);
+            if (diff < RANGE) {
+              for (var col = 1; col < 3; col++) {
+                // TODO(konigsberg): these will throw exceptions as data is removed.
+                var vals =  g.getValue(row, col);
+                if (vals == null) { continue; }
+                var val = vals[0];
+                var y = g.toDomCoords(null, val)[1];
+                var diff2 = Math.abs(canvasy - y);
+                if (diff2 < RANGE) {
+                  var found = false;
+                  for (var i in processed) {
+                    var stored = processed[i];
+                    if(stored[0] == row && stored[1] == col) {
+                      found = true;
+                      break;
+                    }
+                  }
+                  if (!found) {
+                    processed.push([row, col]);
+                    drawV4(x, y);
+                  }
+                  return;
+                }
+              }
+              // drawV4(false, canvasx, canvasy);
+            }
+          }
+          // drawV4(false, canvasx, canvasy);
+        }
+      }
+
+      function upV4(event, g, context) {
+        if (v4Active) {
+          v4Active = false;
+        }
+      }
+
+      function dblClickV4(event, g, context) {
+        unzoomGraph(g4);
+      }
+
+      function drawV4(x, y) {
+        var ctx = v4Canvas;
+
+        ctx.strokeStyle = "#000000";
+        ctx.fillStyle = "#FFFF00";
+        ctx.beginPath();
+        ctx.arc(x,y,5,0,Math.PI*2,true);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+      }
+
+      function captureCanvas(canvas, area, g) {
+        v4Canvas = canvas;
+      }
+
+      var g = new Dygraph(document.getElementById("div_g"),
+           NoisyData, { errorBars : true });
+      var g2 = new Dygraph(document.getElementById("div_g2"),
+           NoisyData, { errorBars : true, interactionModel : {} });
+      var g3 = new Dygraph(document.getElementById("div_g3"),
+           NoisyData, { errorBars : true, interactionModel : {
+        'mousedown' : downV3,
+        'mousemove' : moveV3,
+        'mouseup' : upV3,
+        'dblclick' : dblClickV3,
+        'mousewheel' : scrollV3
+      }});
+      var g4 = new Dygraph(document.getElementById("div_g4"),
+           NoisyData, { errorBars : true, drawPoints : true, interactionModel : {
+            'mousedown' : downV4,
+            'mousemove' : moveV4,
+            'mouseup' : upV4,
+            'dblclick' : dblClickV4,
+           },
+           underlayCallback : captureCanvas
+      });
+
+      function unzoomGraph(g) {
+        g.updateOptions({
+          dateWindow: null,
+          valueRange: null
+        });
+      }
+    </script>
+
+  </body>
+</html>
index 91cee43..2f0cf98 100644 (file)
             document.getElementById("div_g"),
             NoisyData, {
               errorBars: true,
-             zoomCallback : function(minDate, maxDate, yRanges) {
-                 showDimensions(minDate, maxDate, yRanges); }
-              }
+              zoomCallback : function(minDate, maxDate, yRanges) {
+               showDimensions(minDate, maxDate, yRanges);
+             }
+            }
           );
 
       // TODO(konigsberg): Implement a visualization that verifies that initial
@@ -56,8 +57,8 @@
       showDimensions(minDate, maxDate, [minValue, maxValue]);
 
       function showDimensions(minDate, maxDate, yRanges) {
-       showXDimensions(minDate, maxDate);
-       showYDimensions(yRanges);
+        showXDimensions(minDate, maxDate);
+        showYDimensions(yRanges);
       }
 
       function showXDimensions(first, second) {