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