Skip to content

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

@justinlevi

Description

@justinlevi

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions