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

Custom Segment Behavior: Shopping Cart/Trolley Queue Metaphor - Super Slow Redraw #32

Open
justinlevi opened this issue Oct 26, 2017 · 1 comment

Comments

@justinlevi
Copy link

@b-ma Hoping you might be able to take a look at this custom behavior I've been working on for a few days and give some feedback on my approach.

Here is the gist
https://gist.github.com/justinlevi/fa6afbe108620c9806c39b325c17bdef

Summary:
I'm trying to recreate the shopping cart/ train car queue/ (trolly) metaphor. In other words, if you drag a segment left or right, it will respect the boundary of a neighbor/sibling and then push the the neighbor/sibling along the track as well.

I have this working, kind of. The redraw is jittery and slow, which makes me think there must be a better approach.

My first attempt was to create my own queue array, and during the drag, test to see if the current item's start/end (depending on direction) overlaps with a neighbor's start/end. Then, in the overridden _move method, I am looping through my queue array calling `shape.x(segment, renderingContext.timeToPixel.invert(targetX));

After reading through the code closer, I'm noticing there is an internal mechanism to track selectedItems on the Layer class. I'm wondering if there is a built in mechanism that all selected items would receive an edit callback from the Layer?

Ultimately I'm looking for a smooth UX when dragging any number of segments left/right.

As you can see, I also clearly got a bit overboard with my destructuring syntax. I very likely have some super inefficient code. Any feedback you might have would be greatly appreciated.

Full custom behavior below as well:

import * as ui from 'waves-ui';

class CollisionSegmentBehavior extends ui.behaviors.BaseBehavior {

  segmentsData = [];
  DIRECTION = {
    LEFT: 'LEFT',
    RIGHT: 'RIGHT'
  };

  segmentIndex = (search) => {return this.segmentsData.findIndex( obj => { return obj === search }); };

  segmentValues = (renderingContext, shape, dataObj) => {
    return ({
        startX: renderingContext.timeToPixel(shape.x(dataObj)),
        // y: renderingContext.valueToPixel(shape.y(dataObj)),
        endX: renderingContext.timeToPixel(shape.x(dataObj) + shape.width(dataObj)),
        // height: renderingContext.valueToPixel(shape.height(dataObj))
      });
  };

  constructor(segmentsData) {
    super();
    // segmentsData is a reference to the data defining all your segments
    this.segmentsData = segmentsData;

    // binding
    this.isTouchingSibling = this.isTouchingSibling.bind(this);
    this.connectedSiblings = this.connectedSiblings.bind(this);
  }

  edit(renderingContext, shape, datum, dx, dy, target) {
    const classList = target.classList;
    let action = 'move';

    if (classList.contains('handler') && classList.contains('left')) {
      action = 'resizeLeft';
    } else if (classList.contains('handler') && classList.contains('right')) {
      action = 'resizeRight';
    }

    this[`_${action}`](renderingContext, shape, datum, dx, dy, target);
  }


  isTouchingSibling(renderingContext, shape, currentSegmentObj, nextSegmentObj, direction) {
    if (!nextSegmentObj) { return }

    // CONVENIENCE
    const { segmentValues, DIRECTION } = this;
    const { LEFT, RIGHT} = DIRECTION;

    const currentSegmentValues = segmentValues(renderingContext, shape, currentSegmentObj);
    const cSegStart = currentSegmentValues.startX;
    const cSegEnd = currentSegmentValues.endX;
    
    const nextSegmentValues = segmentValues(renderingContext, shape, nextSegmentObj);
    const nSegStart = nextSegmentValues.startX;
    const nSegEnd = nextSegmentValues.endX;

    if (direction === LEFT) {
      // Does the left edge of the current segment hit the right edge of the next segment
      return (cSegStart <= nSegEnd) ? true : false;
    }else if (direction === RIGHT) {
      // Does the right edge of the current segment hit the left edge of the next segment
      return (cSegEnd >= nSegStart) ? true : false;
    }

    return false;
  }

  connectedSiblings(renderingContext, shape, currentIndex, direction, siblings = []) {
    // CONVENIENCE
    const { DIRECTION, segmentsData, isTouchingSibling, connectedSiblings } = this;
    const { LEFT, RIGHT } = DIRECTION;

    const currentSegmentObj = this.segmentsData[currentIndex];

    // Exception cases : FIRST & LAST segments
    if (currentIndex === 0 && direction === LEFT ) { return [currentSegmentObj] }
    if (currentIndex === segmentsData.length - 1 && direction === RIGHT ) { return [currentSegmentObj] }

    const siblingIndex = (direction === LEFT) ? currentIndex - 1 : currentIndex + 1;
    const sibling = segmentsData[siblingIndex];

    // recursion :(
    if ( siblingIndex >= 0 && siblingIndex < segmentsData.length){
      connectedSiblings(renderingContext, shape, siblingIndex, direction, siblings)
    }

    const isTouching = isTouchingSibling(renderingContext, shape, currentSegmentObj, sibling, direction);
    if ( isTouching === true ){
      siblings.push(sibling);

      // TO DO: TRY SELECTING NEIGHBOR
      // Do all selected neighbors receive the edit callback when an event is triggered?
    }

    return siblings;
  }

  _move(renderingContext, shape, dataObj, dx, dy, target) {
    // convenience destructuring
    const { segmentIndex, DIRECTION, connectedSiblings } = this;
    const { LEFT, RIGHT } = DIRECTION;

    // Build Collision Train Array
    const currentIndex = segmentIndex(dataObj);
    const direction = (dx < 0) ? LEFT : RIGHT;
    const train = connectedSiblings(renderingContext, shape, currentIndex, direction);
    
    if (train.length === 0){
      train.push(dataObj);
    }

    // TODO: loop through and make sure siblings are set end to end

    train.forEach(sibling => {
      const x = renderingContext.timeToPixel(shape.x(sibling))
      const targetX = Math.max(x + dx, 0);
      shape.x(sibling, renderingContext.timeToPixel.invert(targetX));
    });
  }

  _resizeLeft(renderingContext, shape, datum, dx, dy, target) {
    // current values
    const x     = renderingContext.timeToPixel(shape.x(datum));
    const width = renderingContext.timeToPixel(shape.width(datum));
    // target values
    let maxTargetX  = x + width;
    let targetX     = x + dx < maxTargetX ? Math.max(x + dx, 0) : x;
    let targetWidth = targetX !== 0 ? Math.max(width - dx, 1) : width;

    shape.x(datum, renderingContext.timeToPixel.invert(targetX));
    shape.width(datum, renderingContext.timeToPixel.invert(targetWidth));
  }

  _resizeRight(renderingContext, shape, datum, dx, dy, target) {
    // current values
    const width = renderingContext.timeToPixel(shape.width(datum));
    // target values
    let targetWidth = Math.max(width + dx, 1);

    shape.width(datum, renderingContext.timeToPixel.invert(targetWidth));
  }
}

export default CollisionSegmentBehavior;
@justinlevi
Copy link
Author

justinlevi commented Oct 31, 2017

Update: Here is a working example using layer selection.

import * as ui from 'waves-ui';

class CollisionSegmentBehavior extends ui.behaviors.BaseBehavior {

  segmentsData = [];
  leadingSegment = undefined;
  train = [];

  DIRECTION = {
    LEFT: 'LEFT',
    RIGHT: 'RIGHT'
  };

  segmentIndex = (search) => {return this.segmentsData.findIndex( obj => { return obj === search }); };

  segmentValues = (renderingContext, shape, dataObj) => {
    return ({
        startX: renderingContext.timeToPixel(shape.x(dataObj)),
        // y: renderingContext.valueToPixel(shape.y(dataObj)),
        endX: renderingContext.timeToPixel(shape.x(dataObj) + shape.width(dataObj)),
        // height: renderingContext.valueToPixel(shape.height(dataObj))
      });
  };

  constructor(segmentsData) {
    super();
    // segmentsData is a reference to the data defining all your segments
    this.segmentsData = segmentsData;

    // binding
    this.isTouchingSibling = this.isTouchingSibling.bind(this);
  }

  edit(renderingContext, shape, datum, dx, dy, target) {
    const classList = target.classList;
    let action = 'move';

    if (classList.contains('handler') && classList.contains('left')) {
      action = 'resizeLeft';
    } else if (classList.contains('handler') && classList.contains('right')) {
      action = 'resizeRight';
    }

    this[`_${action}`](renderingContext, shape, datum, dx, dy, target);
  }


  isTouchingSibling(renderingContext, shape, currentIndex, direction) {

    // CONVENIENCE
    const { segmentValues, DIRECTION } = this;
    const { LEFT, RIGHT} = DIRECTION;

    const currentSegmentObj = this.segmentsData[currentIndex];

    const nextSegmentObj = this.segmentsData[(direction === LEFT) ? currentIndex - 1 : currentIndex + 1];
    if (!nextSegmentObj) { return }

    const currentSegmentValues = segmentValues(renderingContext, shape, currentSegmentObj);
    const cSegStart = currentSegmentValues.startX;
    const cSegEnd = currentSegmentValues.endX;
    
    const nextSegmentValues = segmentValues(renderingContext, shape, nextSegmentObj);
    const nSegStart = nextSegmentValues.startX;
    const nSegEnd = nextSegmentValues.endX;

    if (direction === LEFT) {
      // Does the left edge of the current segment hit the right edge of the next segment
      return (cSegStart <= nSegEnd) ? true : false;
    }else if (direction === RIGHT) {
      // Does the right edge of the current segment hit the left edge of the next segment
      return (cSegEnd >= nSegStart) ? true : false;
    }

    return false;
  }


  _move(renderingContext, shape, dataObj, dx, dy, target) {
    // convenience destructuring
    const { segmentIndex, DIRECTION, isTouchingSibling } = this;
    const { LEFT, RIGHT } = DIRECTION;

    const currentIndex = segmentIndex(dataObj);
    const direction = (dx < 0) ? LEFT : RIGHT;


    // TODO: If changing direction of drag, all selected items should be deselected expect current

    if (isTouchingSibling(renderingContext, shape, currentIndex, direction)) {
      this.select(this._layer.items[(direction === LEFT)? currentIndex - 1 : currentIndex + 1]);
    }

    const x = renderingContext.timeToPixel(shape.x(dataObj))
    var targetX = Math.max(x + dx, 0);


    // START - OVERLAP NEIGHBOR CHECK
    // TODO: This can definitely be refactored
    const nextSegmentObj = this.segmentsData[(direction === LEFT) ? currentIndex - 1 : currentIndex + 1];

    if(nextSegmentObj){

      const currentSegmentValues = this.segmentValues(renderingContext, shape, dataObj);
      const cSegStart = currentSegmentValues.startX;
      const cSegEnd = currentSegmentValues.endX;
      const cSegWidth = cSegEnd - cSegStart;

      const nextSegmentValues = this.segmentValues(renderingContext, shape, nextSegmentObj);
      const nSegStart = nextSegmentValues.startX;
      const nSegEnd = nextSegmentValues.endX;

      if (direction === LEFT && targetX < nSegEnd) {
        targetX = nSegEnd + dx;
      }
      else if (direction === RIGHT && targetX + cSegWidth > nSegStart){
        targetX = nSegStart - cSegWidth + dx;
      }

      // END - OVERLAP NEIGHBOR CHECK
    }

    shape.x(dataObj, renderingContext.timeToPixel.invert(targetX));
  }

  _resizeLeft(renderingContext, shape, datum, dx, dy, target) {
    // current values
    const x     = renderingContext.timeToPixel(shape.x(datum));
    const width = renderingContext.timeToPixel(shape.width(datum));
    // target values
    let maxTargetX  = x + width;
    let targetX     = x + dx < maxTargetX ? Math.max(x + dx, 0) : x;
    let targetWidth = targetX !== 0 ? Math.max(width - dx, 1) : width;

    shape.x(datum, renderingContext.timeToPixel.invert(targetX));
    shape.width(datum, renderingContext.timeToPixel.invert(targetWidth));
  }

  _resizeRight(renderingContext, shape, datum, dx, dy, target) {
    // current values
    const width = renderingContext.timeToPixel(shape.width(datum));
    // target values
    let targetWidth = Math.max(width + dx, 1);

    shape.width(datum, renderingContext.timeToPixel.invert(targetWidth));
  }
}

export default CollisionSegmentBehavior;

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

No branches or pull requests

1 participant