1 /**
  2  * @license
  3  * Copyright 2011 Robert Konigsberg (konigsberg@google.com)
  4  * MIT-licenced: https://opensource.org/licenses/MIT
  5  */
  6 
  7 /**
  8  * @fileoverview The default interaction model for Dygraphs. This is kept out
  9  * of dygraph.js for better navigability.
 10  * @author Robert Konigsberg (konigsberg@google.com)
 11  */
 12 
 13 /*global Dygraph:false */
 14 "use strict";
 15 
 16 import * as utils from './dygraph-utils';
 17 
 18 /**
 19  * You can drag this many pixels past the edge of the chart and still have it
 20  * be considered a zoom. This makes it easier to zoom to the exact edge of the
 21  * chart, a fairly common operation.
 22  */
 23 var DRAG_EDGE_MARGIN = 100;
 24 
 25 /**
 26  * A collection of functions to facilitate build custom interaction models.
 27  * @class
 28  */
 29 var DygraphInteraction = {};
 30 
 31 /**
 32  * Checks whether the beginning & ending of an event were close enough that it
 33  * should be considered a click. If it should, dispatch appropriate events.
 34  * Returns true if the event was treated as a click.
 35  *
 36  * @param {Event} event
 37  * @param {Dygraph} g
 38  * @param {Object} context
 39  */
 40 DygraphInteraction.maybeTreatMouseOpAsClick = function(event, g, context) {
 41   context.dragEndX = utils.dragGetX_(event, context);
 42   context.dragEndY = utils.dragGetY_(event, context);
 43   var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
 44   var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
 45 
 46   if (regionWidth < 2 && regionHeight < 2 &&
 47       g.lastx_ !== undefined && g.lastx_ != -1) {
 48     DygraphInteraction.treatMouseOpAsClick(g, event, context);
 49   }
 50 
 51   context.regionWidth = regionWidth;
 52   context.regionHeight = regionHeight;
 53 };
 54 
 55 /**
 56  * Called in response to an interaction model operation that
 57  * should start the default panning behavior.
 58  *
 59  * It's used in the default callback for "mousedown" operations.
 60  * Custom interaction model builders can use it to provide the default
 61  * panning behavior.
 62  *
 63  * @param {Event} event the event object which led to the startPan call.
 64  * @param {Dygraph} g The dygraph on which to act.
 65  * @param {Object} context The dragging context object (with
 66  *     dragStartX/dragStartY/etc. properties). This function modifies the
 67  *     context.
 68  */
 69 DygraphInteraction.startPan = function(event, g, context) {
 70   var i, axis;
 71   context.isPanning = true;
 72   var xRange = g.xAxisRange();
 73 
 74   if (g.getOptionForAxis("logscale", "x")) {
 75     context.initialLeftmostDate = utils.log10(xRange[0]);
 76     context.dateRange = utils.log10(xRange[1]) - utils.log10(xRange[0]);
 77   } else {
 78     context.initialLeftmostDate = xRange[0];
 79     context.dateRange = xRange[1] - xRange[0];
 80   }
 81   context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
 82 
 83   if (g.getNumericOption("panEdgeFraction")) {
 84     var maxXPixelsToDraw = g.width_ * g.getNumericOption("panEdgeFraction");
 85     var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
 86 
 87     var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
 88     var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
 89 
 90     var boundedLeftDate = g.toDataXCoord(boundedLeftX);
 91     var boundedRightDate = g.toDataXCoord(boundedRightX);
 92     context.boundedDates = [boundedLeftDate, boundedRightDate];
 93 
 94     var boundedValues = [];
 95     var maxYPixelsToDraw = g.height_ * g.getNumericOption("panEdgeFraction");
 96 
 97     for (i = 0; i < g.axes_.length; i++) {
 98       axis = g.axes_[i];
 99       var yExtremes = axis.extremeRange;
100 
101       var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
102       var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
103 
104       var boundedTopValue = g.toDataYCoord(boundedTopY, i);
105       var boundedBottomValue = g.toDataYCoord(boundedBottomY, i);
106 
107       boundedValues[i] = [boundedTopValue, boundedBottomValue];
108     }
109     context.boundedValues = boundedValues;
110   }
111 
112   // Record the range of each y-axis at the start of the drag.
113   // If any axis has a valueRange, then we want a 2D pan.
114   // We can't store data directly in g.axes_, because it does not belong to us
115   // and could change out from under us during a pan (say if there's a data
116   // update).
117   context.is2DPan = false;
118   context.axes = [];
119   for (i = 0; i < g.axes_.length; i++) {
120     axis = g.axes_[i];
121     var axis_data = {};
122     var yRange = g.yAxisRange(i);
123     // TODO(konigsberg): These values should be in |context|.
124     // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
125     var logscale = g.attributes_.getForAxis("logscale", i);
126     if (logscale) {
127       axis_data.initialTopValue = utils.log10(yRange[1]);
128       axis_data.dragValueRange = utils.log10(yRange[1]) - utils.log10(yRange[0]);
129     } else {
130       axis_data.initialTopValue = yRange[1];
131       axis_data.dragValueRange = yRange[1] - yRange[0];
132     }
133     axis_data.unitsPerPixel = axis_data.dragValueRange / (g.plotter_.area.h - 1);
134     context.axes.push(axis_data);
135 
136     // While calculating axes, set 2dpan.
137     if (axis.valueRange) context.is2DPan = true;
138   }
139 };
140 
141 /**
142  * Called in response to an interaction model operation that
143  * responds to an event that pans the view.
144  *
145  * It's used in the default callback for "mousemove" operations.
146  * Custom interaction model builders can use it to provide the default
147  * panning behavior.
148  *
149  * @param {Event} event the event object which led to the movePan call.
150  * @param {Dygraph} g The dygraph on which to act.
151  * @param {Object} context The dragging context object (with
152  *     dragStartX/dragStartY/etc. properties). This function modifies the
153  *     context.
154  */
155 DygraphInteraction.movePan = function(event, g, context) {
156   context.dragEndX = utils.dragGetX_(event, context);
157   context.dragEndY = utils.dragGetY_(event, context);
158 
159   var minDate = context.initialLeftmostDate -
160     (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
161   if (context.boundedDates) {
162     minDate = Math.max(minDate, context.boundedDates[0]);
163   }
164   var maxDate = minDate + context.dateRange;
165   if (context.boundedDates) {
166     if (maxDate > context.boundedDates[1]) {
167       // Adjust minDate, and recompute maxDate.
168       minDate = minDate - (maxDate - context.boundedDates[1]);
169       maxDate = minDate + context.dateRange;
170     }
171   }
172 
173   if (g.getOptionForAxis("logscale", "x")) {
174     g.dateWindow_ = [ Math.pow(utils.LOG_SCALE, minDate),
175                       Math.pow(utils.LOG_SCALE, maxDate) ];
176   } else {
177     g.dateWindow_ = [minDate, maxDate];
178   }
179 
180   // y-axis scaling is automatic unless this is a full 2D pan.
181   if (context.is2DPan) {
182 
183     var pixelsDragged = context.dragEndY - context.dragStartY;
184 
185     // Adjust each axis appropriately.
186     for (var i = 0; i < g.axes_.length; i++) {
187       var axis = g.axes_[i];
188       var axis_data = context.axes[i];
189       var unitsDragged = pixelsDragged * axis_data.unitsPerPixel;
190 
191       var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
192 
193       // In log scale, maxValue and minValue are the logs of those values.
194       var maxValue = axis_data.initialTopValue + unitsDragged;
195       if (boundedValue) {
196         maxValue = Math.min(maxValue, boundedValue[1]);
197       }
198       var minValue = maxValue - axis_data.dragValueRange;
199       if (boundedValue) {
200         if (minValue < boundedValue[0]) {
201           // Adjust maxValue, and recompute minValue.
202           maxValue = maxValue - (minValue - boundedValue[0]);
203           minValue = maxValue - axis_data.dragValueRange;
204         }
205       }
206       if (g.attributes_.getForAxis("logscale", i)) {
207         axis.valueRange = [ Math.pow(utils.LOG_SCALE, minValue),
208                             Math.pow(utils.LOG_SCALE, maxValue) ];
209       } else {
210         axis.valueRange = [ minValue, maxValue ];
211       }
212     }
213   }
214 
215   g.drawGraph_(false);
216 };
217 
218 /**
219  * Called in response to an interaction model operation that
220  * responds to an event that ends panning.
221  *
222  * It's used in the default callback for "mouseup" operations.
223  * Custom interaction model builders can use it to provide the default
224  * panning behavior.
225  *
226  * @param {Event} event the event object which led to the endPan call.
227  * @param {Dygraph} g The dygraph on which to act.
228  * @param {Object} context The dragging context object (with
229  *     dragStartX/dragStartY/etc. properties). This function modifies the
230  *     context.
231  */
232 DygraphInteraction.endPan = DygraphInteraction.maybeTreatMouseOpAsClick;
233 
234 /**
235  * Called in response to an interaction model operation that
236  * responds to an event that starts zooming.
237  *
238  * It's used in the default callback for "mousedown" operations.
239  * Custom interaction model builders can use it to provide the default
240  * zooming behavior.
241  *
242  * @param {Event} event the event object which led to the startZoom call.
243  * @param {Dygraph} g The dygraph on which to act.
244  * @param {Object} context The dragging context object (with
245  *     dragStartX/dragStartY/etc. properties). This function modifies the
246  *     context.
247  */
248 DygraphInteraction.startZoom = function(event, g, context) {
249   context.isZooming = true;
250   context.zoomMoved = false;
251 };
252 
253 /**
254  * Called in response to an interaction model operation that
255  * responds to an event that defines zoom boundaries.
256  *
257  * It's used in the default callback for "mousemove" operations.
258  * Custom interaction model builders can use it to provide the default
259  * zooming behavior.
260  *
261  * @param {Event} event the event object which led to the moveZoom call.
262  * @param {Dygraph} g The dygraph on which to act.
263  * @param {Object} context The dragging context object (with
264  *     dragStartX/dragStartY/etc. properties). This function modifies the
265  *     context.
266  */
267 DygraphInteraction.moveZoom = function(event, g, context) {
268   context.zoomMoved = true;
269   context.dragEndX = utils.dragGetX_(event, context);
270   context.dragEndY = utils.dragGetY_(event, context);
271 
272   var xDelta = Math.abs(context.dragStartX - context.dragEndX);
273   var yDelta = Math.abs(context.dragStartY - context.dragEndY);
274 
275   // drag direction threshold for y axis is twice as large as x axis
276   context.dragDirection = (xDelta < yDelta / 2) ? utils.VERTICAL : utils.HORIZONTAL;
277 
278   g.drawZoomRect_(
279       context.dragDirection,
280       context.dragStartX,
281       context.dragEndX,
282       context.dragStartY,
283       context.dragEndY,
284       context.prevDragDirection,
285       context.prevEndX,
286       context.prevEndY);
287 
288   context.prevEndX = context.dragEndX;
289   context.prevEndY = context.dragEndY;
290   context.prevDragDirection = context.dragDirection;
291 };
292 
293 /**
294  * TODO(danvk): move this logic into dygraph.js
295  * @param {Dygraph} g
296  * @param {Event} event
297  * @param {Object} context
298  */
299 DygraphInteraction.treatMouseOpAsClick = function(g, event, context) {
300   var clickCallback = g.getFunctionOption('clickCallback');
301   var pointClickCallback = g.getFunctionOption('pointClickCallback');
302 
303   var selectedPoint = null;
304 
305   // Find out if the click occurs on a point.
306   var closestIdx = -1;
307   var closestDistance = Number.MAX_VALUE;
308 
309   // check if the click was on a particular point.
310   for (var i = 0; i < g.selPoints_.length; i++) {
311     var p = g.selPoints_[i];
312     var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
313                    Math.pow(p.canvasy - context.dragEndY, 2);
314     if (!isNaN(distance) &&
315         (closestIdx == -1 || distance < closestDistance)) {
316       closestDistance = distance;
317       closestIdx = i;
318     }
319   }
320 
321   // Allow any click within two pixels of the dot.
322   var radius = g.getNumericOption('highlightCircleSize') + 2;
323   if (closestDistance <= radius * radius) {
324     selectedPoint = g.selPoints_[closestIdx];
325   }
326 
327   if (selectedPoint) {
328     var e = {
329       cancelable: true,
330       point: selectedPoint,
331       canvasx: context.dragEndX,
332       canvasy: context.dragEndY
333     };
334     var defaultPrevented = g.cascadeEvents_('pointClick', e);
335     if (defaultPrevented) {
336       // Note: this also prevents click / clickCallback from firing.
337       return;
338     }
339     if (pointClickCallback) {
340       pointClickCallback.call(g, event, selectedPoint);
341     }
342   }
343 
344   var e = {
345     cancelable: true,
346     xval: g.lastx_,  // closest point by x value
347     pts: g.selPoints_,
348     canvasx: context.dragEndX,
349     canvasy: context.dragEndY
350   };
351   if (!g.cascadeEvents_('click', e)) {
352     if (clickCallback) {
353       // TODO(danvk): pass along more info about the points, e.g. 'x'
354       clickCallback.call(g, event, g.lastx_, g.selPoints_);
355     }
356   }
357 };
358 
359 /**
360  * Called in response to an interaction model operation that
361  * responds to an event that performs a zoom based on previously defined
362  * bounds..
363  *
364  * It's used in the default callback for "mouseup" operations.
365  * Custom interaction model builders can use it to provide the default
366  * zooming behavior.
367  *
368  * @param {Event} event the event object which led to the endZoom call.
369  * @param {Dygraph} g The dygraph on which to end the zoom.
370  * @param {Object} context The dragging context object (with
371  *     dragStartX/dragStartY/etc. properties). This function modifies the
372  *     context.
373  */
374 DygraphInteraction.endZoom = function(event, g, context) {
375   g.clearZoomRect_();
376   context.isZooming = false;
377   DygraphInteraction.maybeTreatMouseOpAsClick(event, g, context);
378 
379   // The zoom rectangle is visibly clipped to the plot area, so its behavior
380   // should be as well.
381   // See http://code.google.com/p/dygraphs/issues/detail?id=280
382   var plotArea = g.getArea();
383   if (context.regionWidth >= 10 &&
384       context.dragDirection == utils.HORIZONTAL) {
385     var left = Math.min(context.dragStartX, context.dragEndX),
386         right = Math.max(context.dragStartX, context.dragEndX);
387     left = Math.max(left, plotArea.x);
388     right = Math.min(right, plotArea.x + plotArea.w);
389     if (left < right) {
390       g.doZoomX_(left, right);
391     }
392     context.cancelNextDblclick = true;
393   } else if (context.regionHeight >= 10 &&
394              context.dragDirection == utils.VERTICAL) {
395     var top = Math.min(context.dragStartY, context.dragEndY),
396         bottom = Math.max(context.dragStartY, context.dragEndY);
397     top = Math.max(top, plotArea.y);
398     bottom = Math.min(bottom, plotArea.y + plotArea.h);
399     if (top < bottom) {
400       g.doZoomY_(top, bottom);
401     }
402     context.cancelNextDblclick = true;
403   }
404   context.dragStartX = null;
405   context.dragStartY = null;
406 };
407 
408 /**
409  * @private
410  */
411 DygraphInteraction.startTouch = function(event, g, context) {
412   event.preventDefault();  // touch browsers are all nice.
413   if (event.touches.length > 1) {
414     // If the user ever puts two fingers down, it's not a double tap.
415     context.startTimeForDoubleTapMs = null;
416   }
417 
418   var touches = [];
419   for (var i = 0; i < event.touches.length; i++) {
420     var t = event.touches[i];
421     var rect = t.target.getBoundingClientRect()
422     // we dispense with 'dragGetX_' because all touchBrowsers support pageX
423     touches.push({
424       pageX: t.pageX,
425       pageY: t.pageY,
426       dataX: g.toDataXCoord(t.clientX - rect.left),
427       dataY: g.toDataYCoord(t.clientY - rect.top)
428       // identifier: t.identifier
429     });
430   }
431   context.initialTouches = touches;
432 
433   if (touches.length == 1) {
434     // This is just a swipe.
435     context.initialPinchCenter = touches[0];
436     context.touchDirections = { x: true, y: true };
437   } else if (touches.length >= 2) {
438     // It's become a pinch!
439     // In case there are 3+ touches, we ignore all but the "first" two.
440 
441     // only screen coordinates can be averaged (data coords could be log scale).
442     context.initialPinchCenter = {
443       pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
444       pageY: 0.5 * (touches[0].pageY + touches[1].pageY),
445 
446       // TODO(danvk): remove
447       dataX: 0.5 * (touches[0].dataX + touches[1].dataX),
448       dataY: 0.5 * (touches[0].dataY + touches[1].dataY)
449     };
450 
451     // Make pinches in a 45-degree swath around either axis 1-dimensional zooms.
452     var initialAngle = 180 / Math.PI * Math.atan2(
453         context.initialPinchCenter.pageY - touches[0].pageY,
454         touches[0].pageX - context.initialPinchCenter.pageX);
455 
456     // use symmetry to get it into the first quadrant.
457     initialAngle = Math.abs(initialAngle);
458     if (initialAngle > 90) initialAngle = 90 - initialAngle;
459 
460     context.touchDirections = {
461       x: (initialAngle < (90 - 45/2)),
462       y: (initialAngle > 45/2)
463     };
464   }
465 
466   // save the full x & y ranges.
467   context.initialRange = {
468     x: g.xAxisRange(),
469     y: g.yAxisRange()
470   };
471 };
472 
473 /**
474  * @private
475  */
476 DygraphInteraction.moveTouch = function(event, g, context) {
477   // If the tap moves, then it's definitely not part of a double-tap.
478   context.startTimeForDoubleTapMs = null;
479 
480   var i, touches = [];
481   for (i = 0; i < event.touches.length; i++) {
482     var t = event.touches[i];
483     touches.push({
484       pageX: t.pageX,
485       pageY: t.pageY
486     });
487   }
488   var initialTouches = context.initialTouches;
489 
490   var c_now;
491 
492   // old and new centers.
493   var c_init = context.initialPinchCenter;
494   if (touches.length == 1) {
495     c_now = touches[0];
496   } else {
497     c_now = {
498       pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
499       pageY: 0.5 * (touches[0].pageY + touches[1].pageY)
500     };
501   }
502 
503   // this is the "swipe" component
504   // we toss it out for now, but could use it in the future.
505   var swipe = {
506     pageX: c_now.pageX - c_init.pageX,
507     pageY: c_now.pageY - c_init.pageY
508   };
509   var dataWidth = context.initialRange.x[1] - context.initialRange.x[0];
510   var dataHeight = context.initialRange.y[0] - context.initialRange.y[1];
511   swipe.dataX = (swipe.pageX / g.plotter_.area.w) * dataWidth;
512   swipe.dataY = (swipe.pageY / g.plotter_.area.h) * dataHeight;
513   var xScale, yScale;
514 
515   // The residual bits are usually split into scale & rotate bits, but we split
516   // them into x-scale and y-scale bits.
517   if (touches.length == 1) {
518     xScale = 1.0;
519     yScale = 1.0;
520   } else if (touches.length >= 2) {
521     var initHalfWidth = (initialTouches[1].pageX - c_init.pageX);
522     xScale = (touches[1].pageX - c_now.pageX) / initHalfWidth;
523 
524     var initHalfHeight = (initialTouches[1].pageY - c_init.pageY);
525     yScale = (touches[1].pageY - c_now.pageY) / initHalfHeight;
526   }
527 
528   // Clip scaling to [1/8, 8] to prevent too much blowup.
529   xScale = Math.min(8, Math.max(0.125, xScale));
530   yScale = Math.min(8, Math.max(0.125, yScale));
531 
532   var didZoom = false;
533   if (context.touchDirections.x) {
534     var cFactor = c_init.dataX - swipe.dataX / xScale;
535     g.dateWindow_ = [
536       cFactor + (context.initialRange.x[0] - c_init.dataX) / xScale,
537       cFactor + (context.initialRange.x[1] - c_init.dataX) / xScale
538     ];
539     didZoom = true;
540   }
541 
542   if (context.touchDirections.y) {
543     for (i = 0; i < 1  /*g.axes_.length*/; i++) {
544       var axis = g.axes_[i];
545       var logscale = g.attributes_.getForAxis("logscale", i);
546       if (logscale) {
547         // TODO(danvk): implement
548       } else {
549         var cFactor = c_init.dataY - swipe.dataY / yScale;
550         axis.valueRange = [
551           cFactor + (context.initialRange.y[0] - c_init.dataY) / yScale,
552           cFactor + (context.initialRange.y[1] - c_init.dataY) / yScale
553         ];
554         didZoom = true;
555       }
556     }
557   }
558 
559   g.drawGraph_(false);
560 
561   // We only call zoomCallback on zooms, not pans, to mirror desktop behavior.
562   if (didZoom && touches.length > 1 && g.getFunctionOption('zoomCallback')) {
563     var viewWindow = g.xAxisRange();
564     g.getFunctionOption("zoomCallback").call(g, viewWindow[0], viewWindow[1], g.yAxisRanges());
565   }
566 };
567 
568 /**
569  * @private
570  */
571 DygraphInteraction.endTouch = function(event, g, context) {
572   if (event.touches.length !== 0) {
573     // this is effectively a "reset"
574     DygraphInteraction.startTouch(event, g, context);
575   } else if (event.changedTouches.length == 1) {
576     // Could be part of a "double tap"
577     // The heuristic here is that it's a double-tap if the two touchend events
578     // occur within 500ms and within a 50x50 pixel box.
579     var now = new Date().getTime();
580     var t = event.changedTouches[0];
581     if (context.startTimeForDoubleTapMs &&
582         now - context.startTimeForDoubleTapMs < 500 &&
583         context.doubleTapX && Math.abs(context.doubleTapX - t.screenX) < 50 &&
584         context.doubleTapY && Math.abs(context.doubleTapY - t.screenY) < 50) {
585       g.resetZoom();
586     } else {
587       context.startTimeForDoubleTapMs = now;
588       context.doubleTapX = t.screenX;
589       context.doubleTapY = t.screenY;
590     }
591   }
592 };
593 
594 // Determine the distance from x to [left, right].
595 var distanceFromInterval = function(x, left, right) {
596   if (x < left) {
597     return left - x;
598   } else if (x > right) {
599     return x - right;
600   } else {
601     return 0;
602   }
603 };
604 
605 /**
606  * Returns the number of pixels by which the event happens from the nearest
607  * edge of the chart. For events in the interior of the chart, this returns zero.
608  */
609 var distanceFromChart = function(event, g) {
610   var chartPos = utils.findPos(g.canvas_);
611   var box = {
612     left: chartPos.x,
613     right: chartPos.x + g.canvas_.offsetWidth,
614     top: chartPos.y,
615     bottom: chartPos.y + g.canvas_.offsetHeight
616   };
617 
618   var pt = {
619     x: utils.pageX(event),
620     y: utils.pageY(event)
621   };
622 
623   var dx = distanceFromInterval(pt.x, box.left, box.right),
624       dy = distanceFromInterval(pt.y, box.top, box.bottom);
625   return Math.max(dx, dy);
626 };
627 
628 /**
629  * Default interation model for dygraphs. You can refer to specific elements of
630  * this when constructing your own interaction model, e.g.:
631  * g.updateOptions( {
632  *   interactionModel: {
633  *     mousedown: DygraphInteraction.defaultInteractionModel.mousedown
634  *   }
635  * } );
636  */
637 DygraphInteraction.defaultModel = {
638   // Track the beginning of drag events
639   mousedown: function(event, g, context) {
640     // Right-click should not initiate a zoom.
641     if (event.button && event.button == 2) return;
642 
643     context.initializeMouseDown(event, g, context);
644 
645     if (event.altKey || event.shiftKey) {
646       DygraphInteraction.startPan(event, g, context);
647     } else {
648       DygraphInteraction.startZoom(event, g, context);
649     }
650 
651     // Note: we register mousemove/mouseup on document to allow some leeway for
652     // events to move outside of the chart. Interaction model events get
653     // registered on the canvas, which is too small to allow this.
654     var mousemove = function(event) {
655       if (context.isZooming) {
656         // When the mouse moves >200px from the chart edge, cancel the zoom.
657         var d = distanceFromChart(event, g);
658         if (d < DRAG_EDGE_MARGIN) {
659           DygraphInteraction.moveZoom(event, g, context);
660         } else {
661           if (context.dragEndX !== null) {
662             context.dragEndX = null;
663             context.dragEndY = null;
664             g.clearZoomRect_();
665           }
666         }
667       } else if (context.isPanning) {
668         DygraphInteraction.movePan(event, g, context);
669       }
670     };
671     var mouseup = function(event) {
672       if (context.isZooming) {
673         if (context.dragEndX !== null) {
674           DygraphInteraction.endZoom(event, g, context);
675         } else {
676           DygraphInteraction.maybeTreatMouseOpAsClick(event, g, context);
677         }
678       } else if (context.isPanning) {
679         DygraphInteraction.endPan(event, g, context);
680       }
681 
682       utils.removeEvent(document, 'mousemove', mousemove);
683       utils.removeEvent(document, 'mouseup', mouseup);
684       context.destroy();
685     };
686 
687     g.addAndTrackEvent(document, 'mousemove', mousemove);
688     g.addAndTrackEvent(document, 'mouseup', mouseup);
689   },
690   willDestroyContextMyself: true,
691 
692   touchstart: function(event, g, context) {
693     DygraphInteraction.startTouch(event, g, context);
694   },
695   touchmove: function(event, g, context) {
696     DygraphInteraction.moveTouch(event, g, context);
697   },
698   touchend: function(event, g, context) {
699     DygraphInteraction.endTouch(event, g, context);
700   },
701 
702   // Disable zooming out if panning.
703   dblclick: function(event, g, context) {
704     if (context.cancelNextDblclick) {
705       context.cancelNextDblclick = false;
706       return;
707     }
708 
709     // Give plugins a chance to grab this event.
710     var e = {
711       canvasx: context.dragEndX,
712       canvasy: context.dragEndY,
713       cancelable: true,
714     };
715     if (g.cascadeEvents_('dblclick', e)) {
716       return;
717     }
718 
719     if (event.altKey || event.shiftKey) {
720       return;
721     }
722     g.resetZoom();
723   }
724 };
725 
726 /*
727 Dygraph.DEFAULT_ATTRS.interactionModel = DygraphInteraction.defaultModel;
728 
729 // old ways of accessing these methods/properties
730 Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel;
731 Dygraph.endZoom = DygraphInteraction.endZoom;
732 Dygraph.moveZoom = DygraphInteraction.moveZoom;
733 Dygraph.startZoom = DygraphInteraction.startZoom;
734 Dygraph.endPan = DygraphInteraction.endPan;
735 Dygraph.movePan = DygraphInteraction.movePan;
736 Dygraph.startPan = DygraphInteraction.startPan;
737 */
738 
739 DygraphInteraction.nonInteractiveModel_ = {
740   mousedown: function(event, g, context) {
741     context.initializeMouseDown(event, g, context);
742   },
743   mouseup: DygraphInteraction.maybeTreatMouseOpAsClick
744 };
745 
746 // Default interaction model when using the range selector.
747 DygraphInteraction.dragIsPanInteractionModel = {
748   mousedown: function(event, g, context) {
749     context.initializeMouseDown(event, g, context);
750     DygraphInteraction.startPan(event, g, context);
751   },
752   mousemove: function(event, g, context) {
753     if (context.isPanning) {
754       DygraphInteraction.movePan(event, g, context);
755     }
756   },
757   mouseup: function(event, g, context) {
758     if (context.isPanning) {
759       DygraphInteraction.endPan(event, g, context);
760     }
761   }
762 };
763 
764 export default DygraphInteraction;
765