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