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