Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

panEdgeFraction ignored on touch screen #713

Open
calebvandenberg opened this issue Jan 17, 2016 · 5 comments · May be fixed by #991
Open

panEdgeFraction ignored on touch screen #713

calebvandenberg opened this issue Jan 17, 2016 · 5 comments · May be fixed by #991

Comments

@calebvandenberg
Copy link

Thanks for a fantastic library.
PanEdgeFraction doesn't do anything on touch screens- graph axes go off infinitely making it difficult to find graph back.
Example here: http://swellmatrix.com/current/panEdgeFraction.html
Panning limited as expected when using keyboard (shift) + mouse.
Tested on:
Android 5.1 Firefox 43.0
Android 5.1 Chrome 47
Android 4.2.2 Chrome 45
iPad iOS 9.2 Safari 9.0

@apptum
Copy link

apptum commented Feb 7, 2016

@calebvandenberg ok, so I managed to implement panEdgeFraction for touch input:
First, add Dygraph.Interaction.startPan(event, g, context); to touchstart

interactionModel: {
      touchstart: function(event, g, context) {
        Dygraph.defaultInteractionModel.touchstart(event, g, context);
        Dygraph.Interaction.startPan(event, g, context);
      },
      touchmove: function(event, g, context) {
        moveTouch(event, g, context);
      },
      touchend: Dygraph.defaultInteractionModel.touchend,
    }

(Normally Dygraph.Interaction.startPan would be called in "mousedown" when shift or alt key pressed.)

Now we need a custom interactionModel for touchmove. I took the default Dygraph.defaultInteractionModel.touchmove (wich gives us panning & pinching) and merged the relevant parts of Dygraph.Interaction.movePan for considering panEdgeFraction.

--- see post below ---

Only tested on Chrome desktop & mobile, but it should be a starting point :)
There is one quirk though: the distance from the edge of the graph to the edge of the canvas (how far away the graph can be panned) gets bigger when you zoom in. And I can't figure out where miscalculation happens.
Help with this and a general opinion and look on what I tried here of anyone who fully understands Dygraphs would very much be appreciated. @danvk ?

And a big thanks for your great work btw!

@apptum
Copy link

apptum commented Feb 12, 2016

update:

//Dygraph.Interaction.moveTouch = function(event, g, context) {
moveTouch = function(event, g, context) {
  // If the tap moves, then it's definitely not part of a double-tap.
  context.startTimeForDoubleTapMs = null;

  var i, touches = [];
  for (i = 0; i < event.touches.length; i++) {
    var t = event.touches[i];
    touches.push({
      pageX: t.pageX,
      pageY: t.pageY
    });
  }
  var initialTouches = context.initialTouches;

  var c_now;

  // old and new centers.
  var c_init = context.initialPinchCenter;
  if (touches.length == 1) {
    c_now = touches[0];
  } else {
    c_now = {
      pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
      pageY: 0.5 * (touches[0].pageY + touches[1].pageY)
    };
  }

  // this is the "swipe" component
  // we toss it out for now, but could use it in the future.
  var swipe = {
    pageX: c_now.pageX - c_init.pageX,
    pageY: c_now.pageY - c_init.pageY
  };
  var dataWidth = context.initialRange.x[1] - context.initialRange.x[0];
  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;

  // 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) {
    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;
  }

  // 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;

  //in Dygraph.Interaction.movePan dragEndX/Y are declared like this what gives NaN:
  //context.dragEndX = Dygraph.dragGetX_(event, context);
  //context.dragEndY = Dygraph.dragGetY_(event, context);
  context.dragEndX = event.touches[0].pageX - context.px;
  context.dragEndY = event.touches[0].pageY - context.py;

  //in Dygraph.Interaction.movePan dragStartX/Y is not declared but needed here
  context.dragStartX = context.initialTouches[0].pageX - context.px;
  context.dragStartY = context.initialTouches[0].pageY - context.py;

  if (context.touchDirections.x) {
    if (event.touches.length == 1){
      var minDate = context.initialLeftmostDate -
          (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;

      if (context.boundedDates) {
        minDate = Math.max(minDate, context.boundedDates[0]);
      }

      var maxDate = minDate + context.dateRange;
      if (context.boundedDates) {
        if (maxDate > context.boundedDates[1]) {
          // Adjust minDate, and recompute maxDate.
          minDate = minDate - (maxDate - context.boundedDates[1]);
          maxDate = minDate + context.dateRange;
        }
      }

      if (g.getOptionForAxis("logscale", "x")) {
        g.dateWindow_ = [ Math.pow(Dygraph.LOG_SCALE, minDate),
                         Math.pow(Dygraph.LOG_SCALE, maxDate) ];
      } else {
        g.dateWindow_ = [minDate, maxDate];    
      }

    } else {
      var minDateWindow = c_init.dataX - swipe.dataX + (context.initialRange.x[0] - c_init.dataX) / xScale
      var maxDateWindow = c_init.dataX - swipe.dataX + (context.initialRange.x[1] - c_init.dataX) / xScale

      if (context.boundedDates) {
        if (minDateWindow < context.boundedDates[0]){
          minDateWindow = context.boundedDates[0];
        }
        if (maxDateWindow > context.boundedDates[1]){
          maxDateWindow = context.boundedDates[1];
        }
      }

      // limit the smallest shown resolution for xAxes to 1 sec
      if (maxDateWindow - minDateWindow <= 60000){
        return;
      }

      g.dateWindow_ = [
        minDateWindow,
        maxDateWindow
      ];
      didZoom = true;
    }
  }

  if (context.touchDirections.y) {
    var pixelsDragged = context.dragEndY - context.dragStartY;

    // Adjust each axis appropriately.
    for (var i = 0; i < g.axes_.length; i++) {
      var axis = g.axes_[i];
      var axis_data = context.axes[i];
      var unitsDragged = pixelsDragged * axis_data.unitsPerPixel;

      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;

      var logscale = g.attributes_.getForAxis("logscale", i);

      if (event.touches.length == 1){
        // y-axis scaling is automatic unless this is a full 2D pan.
        if (context.is2DPan) {
          // In log scale, maxValue and minValue are the logs of those values.
          var maxValue = axis_data.initialTopValue + unitsDragged;
          if (boundedValue) {
            maxValue = Math.min(maxValue, boundedValue[1]);
          }

          var minValue = maxValue - axis_data.dragValueRange;
          if (boundedValue) {
            if (minValue < boundedValue[0]) {
              // Adjust maxValue, and recompute minValue.
              maxValue = maxValue - (minValue - boundedValue[0]);
              minValue = maxValue - axis_data.dragValueRange;
            }
          }

          if (logscale) {
            axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
                                Math.pow(Dygraph.LOG_SCALE, maxValue) ];
          } else {
            axis.valueWindow = [ minValue, maxValue ];
          }
        }

      } else {
        var minValueWindow = c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale
        var maxValueWindow = c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale
        if (boundedValue) {
          if (minValueWindow < boundedValue[0]){
            minValueWindow = boundedValue[0];
          }
          if (maxValueWindow > boundedValue[1]){
            maxValueWindow = boundedValue[1];
          } 
        }

        if (logscale) {
          // TODO(danvk): implement
        } else  {
          // limit the smallest shown resolution for yAxes
          // TODO: make dependend of actual values
          if (maxValueWindow - minValueWindow <= 0.1){
            return;
          }
          axis.valueWindow = [
            minValueWindow,
            maxValueWindow
          ];
          didZoom = true;
        }
      }
    }
  }

  if (event.touches.length == 2){
    Dygraph.Interaction.startPan(event, g, context);
  }

  g.drawGraph_(false);

  // We only call zoomCallback on zooms, not pans, to mirror desktop behavior.
if (didZoom && touches.length > 1 && g.getFunctionOption('zoomCallback')) {
    var viewWindow = g.xAxisRange();
    g.getFunctionOption("zoomCallback").call(g, viewWindow[0], viewWindow[1], g.yAxisRanges());
  }
};

@calebvandenberg
Copy link
Author

Thanks for putting this together. I finally got around to implementing this on my own site! For my own purposes I modified the dygraphs source instead of creating a custom interaction model.
If any other .01x developers are interested, I simply

  • added Dygraph.Interaction.startPan(event, g, context); after line 7905 of dygraph-combined-dev.js,
  • then changed Dygraph.Interaction.moveTouch (line 7578) to @apptum 's spliced code (removing line 2).

@Narmer23
Copy link

Narmer23 commented Aug 7, 2018

This would be a great feature! I tried the proposed solution but didn't manage to get it working (I suppose mainly because its related to an older version). Any updates on this? Thanks!

@Alpt
Copy link

Alpt commented Apr 9, 2020

Please, make it work. Zooming on mobile is very difficult. It goes to almost -Infinity, +Infinity very soon. It is not easy to zoom.

Are there any quick fixes? The above workaround does not work. It completely disables touch interaction.

Update: I found a fix, and made a pull request

@Alpt Alpt linked a pull request Apr 11, 2020 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants