diff --git a/src/dygraph-interaction-model.js b/src/dygraph-interaction-model.js index 494b63849..8221bac05 100644 --- a/src/dygraph-interaction-model.js +++ b/src/dygraph-interaction-model.js @@ -405,6 +405,15 @@ DygraphInteraction.endZoom = function(event, g, context) { context.dragStartY = null; }; +// isOutOfExtremes: checks that number p is out of extremes ex. +// ex is a tuple [a, b], with a <= b +function isOutOfExtremes(p, ex) { + + return p < ex[0] || p > ex[1] + + // note on ex[0] < ex[1]: sometimes extremes are invalid, + // ex[0] must be less than or equal to ex[1] +} /** * @private */ @@ -418,17 +427,40 @@ DygraphInteraction.startTouch = function(event, g, context) { var touches = []; for (var i = 0; i < event.touches.length; i++) { var t = event.touches[i]; + var rect = t.target.getBoundingClientRect() // we dispense with 'dragGetX_' because all touchBrowsers support pageX touches.push({ pageX: t.pageX, pageY: t.pageY, - dataX: g.toDataXCoord(t.pageX), - dataY: g.toDataYCoord(t.pageY) + dataX: g.toDataXCoord(t.clientX-rect.left), + dataY: g.toDataYCoord(t.clientY-rect.top) // identifier: t.identifier }); } context.initialTouches = touches; + var pinchCenter + context.pinchOutOfExtremes = false + if (touches.length >= 2) { + + // only screen coordinates can be averaged (data coords could be log scale). + pinchCenter = { + pageX: 0.5 * (touches[0].pageX + touches[1].pageX), + pageY: 0.5 * (touches[0].pageY + touches[1].pageY), + + // TODO(danvk): remove + dataX: 0.5 * (touches[0].dataX + touches[1].dataX), + dataY: 0.5 * (touches[0].dataY + touches[1].dataY) + }; + + var xExtremes = g.xAxisExtremes() + var yExtremes = g.yAxisExtremes() + if (xExtremes[0] >= xExtremes[1] || isOutOfExtremes(pinchCenter.dataX, xExtremes)) + context.pinchOutOfExtremes = true + if (yExtremes.find(yEx => yEx[0] >= yEx[1] || isOutOfExtremes(pinchCenter.dataY, yEx))) + context.pinchOutOfExtremes = true + } + if (touches.length == 1) { // This is just a swipe. context.initialPinchCenter = touches[0]; @@ -437,15 +469,7 @@ DygraphInteraction.startTouch = function(event, g, context) { // It's become a pinch! // In case there are 3+ touches, we ignore all but the "first" two. - // only screen coordinates can be averaged (data coords could be log scale). - context.initialPinchCenter = { - pageX: 0.5 * (touches[0].pageX + touches[1].pageX), - pageY: 0.5 * (touches[0].pageY + touches[1].pageY), - - // TODO(danvk): remove - dataX: 0.5 * (touches[0].dataX + touches[1].dataX), - dataY: 0.5 * (touches[0].dataY + touches[1].dataY) - }; + context.initialPinchCenter = pinchCenter // Make pinches in a 45-degree swath around either axis 1-dimensional zooms. var initialAngle = 180 / Math.PI * Math.atan2( @@ -457,8 +481,11 @@ DygraphInteraction.startTouch = function(event, g, context) { if (initialAngle > 90) initialAngle = 90 - initialAngle; context.touchDirections = { - x: (initialAngle < (90 - 45/2)), - y: (initialAngle > 45/2) + //TODO: it does not seem to work well + //x: (initialAngle < (90 - 45/2)), + //y: (initialAngle > 45/2) + x: true, + y: true }; } @@ -509,31 +536,62 @@ DygraphInteraction.moveTouch = function(event, g, context) { var dataHeight = context.initialRange.y[0] - context.initialRange.y[1]; swipe.dataX = (swipe.pageX / g.plotter_.area.w) * dataWidth; swipe.dataY = (swipe.pageY / g.plotter_.area.h) * dataHeight; - var xScale, yScale; + + var xExtremes = g.xAxisExtremes() + var yExtremes = g.yAxisExtremes() + + var xScale = 1.0, yScale = 1.0; // The residual bits are usually split into scale & rotate bits, but we split // them into x-scale and y-scale bits. - if (touches.length == 1) { - xScale = 1.0; - yScale = 1.0; - } else if (touches.length >= 2) { + if (touches.length >= 2 && !context.pinchOutOfExtremes) { var initHalfWidth = (initialTouches[1].pageX - c_init.pageX); - xScale = (touches[1].pageX - c_now.pageX) / initHalfWidth; - var initHalfHeight = (initialTouches[1].pageY - c_init.pageY); - yScale = (touches[1].pageY - c_now.pageY) / initHalfHeight; + var minAllowed = 5 + + if (Math.abs(initHalfWidth) > minAllowed) { + // sensitiveness dampening: smaller pinches count much less + var damp = 1 / (Math.abs(initHalfWidth)-minAllowed) + + var nowHalfWidth = touches[1].pageX - c_now.pageX + xScale = (nowHalfWidth + damp) / (initHalfWidth + damp); + } + + if (Math.abs(initHalfHeight) > minAllowed) { + var damp = 1 / (Math.abs(initHalfHeight) - minAllowed) + + var nowHalfHeight = touches[1].pageY - c_now.pageY + yScale = (nowHalfHeight + damp) / (initHalfHeight + damp); + } } // Clip scaling to [1/8, 8] to prevent too much blowup. xScale = Math.min(8, Math.max(0.125, xScale)); yScale = Math.min(8, Math.max(0.125, yScale)); + var didZoom = false; if (context.touchDirections.x) { + var oldDateWindow = g.dateWindow_ || context.initialRange.x g.dateWindow_ = [ - c_init.dataX - swipe.dataX + (context.initialRange.x[0] - c_init.dataX) / xScale, - c_init.dataX - swipe.dataX + (context.initialRange.x[1] - c_init.dataX) / xScale + c_init.dataX - swipe.dataX / xScale + (context.initialRange.x[0] - c_init.dataX) / xScale, + c_init.dataX - swipe.dataX / xScale + (context.initialRange.x[1] - c_init.dataX) / xScale ]; + if (xExtremes[0] < xExtremes[1]) { + var pef = g.getNumericOption("panEdgeFraction") || 1/10 + var a = xExtremes[0] - (xExtremes[1] - xExtremes[0]) * pef + var b = xExtremes[1] + (xExtremes[1] - xExtremes[0]) * pef + if (g.dateWindow_[0] < a) { + g.dateWindow_[0] = a + if (xScale == 1) // if it is a pan, do not scale the window + g.dateWindow_[1] = oldDateWindow[1] + } + if (g.dateWindow_[1] > b) { + g.dateWindow_[1] = b + if (xScale == 1) + g.dateWindow_[0] = oldDateWindow[0] + } + } didZoom = true; } @@ -544,10 +602,27 @@ DygraphInteraction.moveTouch = function(event, g, context) { if (logscale) { // TODO(danvk): implement } else { + var oldValueRange = axis.valueRange || context.initialRange.y axis.valueRange = [ - c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale, - c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale + c_init.dataY - swipe.dataY / yScale + (context.initialRange.y[0] - c_init.dataY) / yScale, + c_init.dataY - swipe.dataY / yScale + (context.initialRange.y[1] - c_init.dataY) / yScale ]; + if (yExtremes[i][0] < yExtremes[i][1]) { + var pef = g.getNumericOption("panEdgeFraction") || 1/10 + var a = yExtremes[i][0] - (yExtremes[i][1] - yExtremes[i][0]) * pef + var b = yExtremes[i][1] + (yExtremes[i][1] - yExtremes[i][0]) * pef + if (axis.valueRange[0] < a) { + axis.valueRange[0] = a + if (xScale == 1) // if it is a pan, do not scale + axis.valueRange[1] = oldValueRange[1] + } + if (axis.valueRange[1] > b) { + axis.valueRange[1] = b + if (xScale == 1) + axis.valueRange[0] = oldValueRange[0] + } + } + didZoom = true; } }