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