Home Manual Reference Source Repository

src/shapes/line.js

import BaseShape from './base-shape';

/**
 * A shape to display a line. Its main use is as common shape to create a
 * breakpoint visualization. (entity shape)
 *
 * [example usage](./examples/layer-breakpoint.html)
 */
export default class Line extends BaseShape {
  getClassName() { return 'line'; }

  _getAccessorList() {
    return { cx: 0, cy: 0 };
  }

  _getDefaults() {
    return { color: '#000000' };
  }

  render(renderingContext) {
    if (this.$el) { return this.$el; }

    this.$el = document.createElementNS(this.ns, 'path');
    this.$el.setAttributeNS(null, 'shape-rendering', 'geometricPrecision');
    return this.$el;
  }

  encache(data) {

    data = data.slice(0);
    data.sort((a, b) => this.cx(a) < this.cx(b) ? -1 : 1);

    return data;
  }

  _findInData(data, x) {

    // Binary search, demands that data has been encached
    // (i.e. sorted). Returns index of value that matches x. If there
    // is no exact match, returns the index just before where x would
    // appear, unless x would appear as the first element in which
    // case 0 is returned. (So the returned index is always in range
    // for the input array, unless the input array is empty.)

    // Note that x must be in model coordinates (e.g. time), not pixel
    // coords.

    let low = 0;
    let high = data.length - 1;

    while (low <= high) {
      let mid = low + ((high - low) >> 1);
      let value = this.cx(data[mid]);
      if (value < x) {
        low = mid + 1;
      } else if (value > x) {
        high = mid - 1;
      } else {
        return mid;
      }
    }

    if (high < 0) {
      return 0;
    } else {
      return high;
    }
  }
  
  update(renderingContext, data) { // data array is sorted already
    
    const before = performance.now();

    const minX = Math.floor(renderingContext.minX);
    const maxX = Math.ceil(renderingContext.maxX);

    console.log("minX = " + minX + ", maxX = " + maxX);
    
    let instructions = [];
    const n = data.length;
    
    if (n > 0) {

      // We want to start with the last element to the left of the
      // visible region, and end with the first element beyond the
      // right of it

      let cx0 = renderingContext.timeToPixel.invert(minX);
      let i0 = this._findInData(data, cx0);

      let nextX = renderingContext.timeToPixel(this.cx(data[i0]));
    
      for (let i = i0; i < n; ++i) {

        const x = nextX;

        if (i + 1 < n) {
          nextX = renderingContext.timeToPixel(this.cx(data[i+1]));
        }

        const y = renderingContext.valueToPixel(this.cy(data[i])) - 0.5;

        instructions.push(`${x},${y}`);
        
        if (x > maxX) {
          break;
        }
      }
    }          

    console.log("line instructions have " + instructions.length + " elements");

    const instructionStr = 'M' + instructions.join('L');
    this.$el.setAttributeNS(null, 'd', instructionStr);

    this.$el.style.stroke = this.params.color;
    this.$el.style.strokeWidth = 2;
    this.$el.style.fill = 'none';

    const after = performance.now();
    console.log("line update time = " + Math.round(after - before));
    
    data = null;
  }

  describe(data, t) {
    if (!data.length) return 0;
    let i = this._findInData(data, t);
    const cx = this.cx(data[i]);
    const cy = this.cy(data[i]);
    let unit = "";
    if (typeof(data[i].unit) !== 'undefined') {
      unit = data[i].unit;
    }
    return [{
      cx: cx,
      cy: cy,
      unit: unit
    }];
  }
}