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