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     // we dispense with 'dragGetX_' because all touchBrowsers support pageX
422     touches.push({
423       pageX: t.pageX,
424       pageY: t.pageY,
425       dataX: g.toDataXCoord(t.pageX),
426       dataY: g.toDataYCoord(t.pageY)
427       // identifier: t.identifier
428     });
429   }
430   context.initialTouches = touches;
431 
432   if (touches.length == 1) {
433     // This is just a swipe.
434     context.initialPinchCenter = touches[0];
435     context.touchDirections = { x: true, y: true };
436   } else if (touches.length >= 2) {
437     // It's become a pinch!
438     // In case there are 3+ touches, we ignore all but the "first" two.
439 
440     // only screen coordinates can be averaged (data coords could be log scale).
441     context.initialPinchCenter = {
442       pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
443       pageY: 0.5 * (touches[0].pageY + touches[1].pageY),
444 
445       // TODO(danvk): remove
446       dataX: 0.5 * (touches[0].dataX + touches[1].dataX),
447       dataY: 0.5 * (touches[0].dataY + touches[1].dataY)
448     };
449 
450     // Make pinches in a 45-degree swath around either axis 1-dimensional zooms.
451     var initialAngle = 180 / Math.PI * Math.atan2(
452         context.initialPinchCenter.pageY - touches[0].pageY,
453         touches[0].pageX - context.initialPinchCenter.pageX);
454 
455     // use symmetry to get it into the first quadrant.
456     initialAngle = Math.abs(initialAngle);
457     if (initialAngle > 90) initialAngle = 90 - initialAngle;
458 
459     context.touchDirections = {
460       x: (initialAngle < (90 - 45/2)),
461       y: (initialAngle > 45/2)
462     };
463   }
464 
465   // save the full x & y ranges.
466   context.initialRange = {
467     x: g.xAxisRange(),
468     y: g.yAxisRange()
469   };
470 };
471 
472 /**
473  * @private
474  */
475 DygraphInteraction.moveTouch = function(event, g, context) {
476   // If the tap moves, then it's definitely not part of a double-tap.
477   context.startTimeForDoubleTapMs = null;
478 
479   var i, touches = [];
480   for (i = 0; i < event.touches.length; i++) {
481     var t = event.touches[i];
482     touches.push({
483       pageX: t.pageX,
484       pageY: t.pageY
485     });
486   }
487   var initialTouches = context.initialTouches;
488 
489   var c_now;
490 
491   // old and new centers.
492   var c_init = context.initialPinchCenter;
493   if (touches.length == 1) {
494     c_now = touches[0];
495   } else {
496     c_now = {
497       pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
498       pageY: 0.5 * (touches[0].pageY + touches[1].pageY)
499     };
500   }
501 
502   // this is the "swipe" component
503   // we toss it out for now, but could use it in the future.
504   var swipe = {
505     pageX: c_now.pageX - c_init.pageX,
506     pageY: c_now.pageY - c_init.pageY
507   };
508   var dataWidth = context.initialRange.x[1] - context.initialRange.x[0];
509   var dataHeight = context.initialRange.y[0] - context.initialRange.y[1];
510   swipe.dataX = (swipe.pageX / g.plotter_.area.w) * dataWidth;
511   swipe.dataY = (swipe.pageY / g.plotter_.area.h) * dataHeight;
512   var xScale, yScale;
513 
514   // The residual bits are usually split into scale & rotate bits, but we split
515   // them into x-scale and y-scale bits.
516   if (touches.length == 1) {
517     xScale = 1.0;
518     yScale = 1.0;
519   } else if (touches.length >= 2) {
520     var initHalfWidth = (initialTouches[1].pageX - c_init.pageX);
521     xScale = (touches[1].pageX - c_now.pageX) / initHalfWidth;
522 
523     var initHalfHeight = (initialTouches[1].pageY - c_init.pageY);
524     yScale = (touches[1].pageY - c_now.pageY) / initHalfHeight;
525   }
526 
527   // Clip scaling to [1/8, 8] to prevent too much blowup.
528   xScale = Math.min(8, Math.max(0.125, xScale));
529   yScale = Math.min(8, Math.max(0.125, yScale));
530 
531   var didZoom = false;
532   if (context.touchDirections.x) {
533     g.dateWindow_ = [
534       c_init.dataX - swipe.dataX + (context.initialRange.x[0] - c_init.dataX) / xScale,
535       c_init.dataX - swipe.dataX + (context.initialRange.x[1] - c_init.dataX) / xScale
536     ];
537     didZoom = true;
538   }
539 
540   if (context.touchDirections.y) {
541     for (i = 0; i < 1  /*g.axes_.length*/; i++) {
542       var axis = g.axes_[i];
543       var logscale = g.attributes_.getForAxis("logscale", i);
544       if (logscale) {
545         // TODO(danvk): implement
546       } else {
547         axis.valueRange = [
548           c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale,
549           c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale
550         ];
551         didZoom = true;
552       }
553     }
554   }
555 
556   g.drawGraph_(false);
557 
558   // We only call zoomCallback on zooms, not pans, to mirror desktop behavior.
559   if (didZoom && touches.length > 1 && g.getFunctionOption('zoomCallback')) {
560     var viewWindow = g.xAxisRange();
561     g.getFunctionOption("zoomCallback").call(g, viewWindow[0], viewWindow[1], g.yAxisRanges());
562   }
563 };
564 
565 /**
566  * @private
567  */
568 DygraphInteraction.endTouch = function(event, g, context) {
569   if (event.touches.length !== 0) {
570     // this is effectively a "reset"
571     DygraphInteraction.startTouch(event, g, context);
572   } else if (event.changedTouches.length == 1) {
573     // Could be part of a "double tap"
574     // The heuristic here is that it's a double-tap if the two touchend events
575     // occur within 500ms and within a 50x50 pixel box.
576     var now = new Date().getTime();
577     var t = event.changedTouches[0];
578     if (context.startTimeForDoubleTapMs &&
579         now - context.startTimeForDoubleTapMs < 500 &&
580         context.doubleTapX && Math.abs(context.doubleTapX - t.screenX) < 50 &&
581         context.doubleTapY && Math.abs(context.doubleTapY - t.screenY) < 50) {
582       g.resetZoom();
583     } else {
584       context.startTimeForDoubleTapMs = now;
585       context.doubleTapX = t.screenX;
586       context.doubleTapY = t.screenY;
587     }
588   }
589 };
590 
591 // Determine the distance from x to [left, right].
592 var distanceFromInterval = function(x, left, right) {
593   if (x < left) {
594     return left - x;
595   } else if (x > right) {
596     return x - right;
597   } else {
598     return 0;
599   }
600 };
601 
602 /**
603  * Returns the number of pixels by which the event happens from the nearest
604  * edge of the chart. For events in the interior of the chart, this returns zero.
605  */
606 var distanceFromChart = function(event, g) {
607   var chartPos = utils.findPos(g.canvas_);
608   var box = {
609     left: chartPos.x,
610     right: chartPos.x + g.canvas_.offsetWidth,
611     top: chartPos.y,
612     bottom: chartPos.y + g.canvas_.offsetHeight
613   };
614 
615   var pt = {
616     x: utils.pageX(event),
617     y: utils.pageY(event)
618   };
619 
620   var dx = distanceFromInterval(pt.x, box.left, box.right),
621       dy = distanceFromInterval(pt.y, box.top, box.bottom);
622   return Math.max(dx, dy);
623 };
624 
625 /**
626  * Default interation model for dygraphs. You can refer to specific elements of
627  * this when constructing your own interaction model, e.g.:
628  * g.updateOptions( {
629  *   interactionModel: {
630  *     mousedown: DygraphInteraction.defaultInteractionModel.mousedown
631  *   }
632  * } );
633  */
634 DygraphInteraction.defaultModel = {
635   // Track the beginning of drag events
636   mousedown: function(event, g, context) {
637     // Right-click should not initiate a zoom.
638     if (event.button && event.button == 2) return;
639 
640     context.initializeMouseDown(event, g, context);
641 
642     if (event.altKey || event.shiftKey) {
643       DygraphInteraction.startPan(event, g, context);
644     } else {
645       DygraphInteraction.startZoom(event, g, context);
646     }
647 
648     // Note: we register mousemove/mouseup on document to allow some leeway for
649     // events to move outside of the chart. Interaction model events get
650     // registered on the canvas, which is too small to allow this.
651     var mousemove = function(event) {
652       if (context.isZooming) {
653         // When the mouse moves >200px from the chart edge, cancel the zoom.
654         var d = distanceFromChart(event, g);
655         if (d < DRAG_EDGE_MARGIN) {
656           DygraphInteraction.moveZoom(event, g, context);
657         } else {
658           if (context.dragEndX !== null) {
659             context.dragEndX = null;
660             context.dragEndY = null;
661             g.clearZoomRect_();
662           }
663         }
664       } else if (context.isPanning) {
665         DygraphInteraction.movePan(event, g, context);
666       }
667     };
668     var mouseup = function(event) {
669       if (context.isZooming) {
670         if (context.dragEndX !== null) {
671           DygraphInteraction.endZoom(event, g, context);
672         } else {
673           DygraphInteraction.maybeTreatMouseOpAsClick(event, g, context);
674         }
675       } else if (context.isPanning) {
676         DygraphInteraction.endPan(event, g, context);
677       }
678 
679       utils.removeEvent(document, 'mousemove', mousemove);
680       utils.removeEvent(document, 'mouseup', mouseup);
681       context.destroy();
682     };
683 
684     g.addAndTrackEvent(document, 'mousemove', mousemove);
685     g.addAndTrackEvent(document, 'mouseup', mouseup);
686   },
687   willDestroyContextMyself: true,
688 
689   touchstart: function(event, g, context) {
690     DygraphInteraction.startTouch(event, g, context);
691   },
692   touchmove: function(event, g, context) {
693     DygraphInteraction.moveTouch(event, g, context);
694   },
695   touchend: function(event, g, context) {
696     DygraphInteraction.endTouch(event, g, context);
697   },
698 
699   // Disable zooming out if panning.
700   dblclick: function(event, g, context) {
701     if (context.cancelNextDblclick) {
702       context.cancelNextDblclick = false;
703       return;
704     }
705 
706     // Give plugins a chance to grab this event.
707     var e = {
708       canvasx: context.dragEndX,
709       canvasy: context.dragEndY,
710       cancelable: true,
711     };
712     if (g.cascadeEvents_('dblclick', e)) {
713       return;
714     }
715 
716     if (event.altKey || event.shiftKey) {
717       return;
718     }
719     g.resetZoom();
720   }
721 };
722 
723 /*
724 Dygraph.DEFAULT_ATTRS.interactionModel = DygraphInteraction.defaultModel;
725 
726 // old ways of accessing these methods/properties
727 Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel;
728 Dygraph.endZoom = DygraphInteraction.endZoom;
729 Dygraph.moveZoom = DygraphInteraction.moveZoom;
730 Dygraph.startZoom = DygraphInteraction.startZoom;
731 Dygraph.endPan = DygraphInteraction.endPan;
732 Dygraph.movePan = DygraphInteraction.movePan;
733 Dygraph.startPan = DygraphInteraction.startPan;
734 */
735 
736 DygraphInteraction.nonInteractiveModel_ = {
737   mousedown: function(event, g, context) {
738     context.initializeMouseDown(event, g, context);
739   },
740   mouseup: DygraphInteraction.maybeTreatMouseOpAsClick
741 };
742 
743 // Default interaction model when using the range selector.
744 DygraphInteraction.dragIsPanInteractionModel = {
745   mousedown: function(event, g, context) {
746     context.initializeMouseDown(event, g, context);
747     DygraphInteraction.startPan(event, g, context);
748   },
749   mousemove: function(event, g, context) {
750     if (context.isPanning) {
751       DygraphInteraction.movePan(event, g, context);
752     }
753   },
754   mouseup: function(event, g, context) {
755     if (context.isPanning) {
756       DygraphInteraction.endPan(event, g, context);
757     }
758   }
759 };
760 
761 export default DygraphInteraction;
762