      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