API Docs for:
Show:

File: addon/mixins/graph-graphic-with-tracking-dot.js

import Ember from 'ember';
import { getMousePoint } from '../utils/nf/svg-dom';
import computed from 'ember-new-computed';

var { on, observer } = Ember;

export default Ember.Mixin.create({
  /**
    Gets or sets the tracking mode of the component.

    Possible values are:

    - 'none': no tracking behavior
    - 'hover': only track while mouse hover
    - 'snap-last': track while mouse hover, but snap to the last data element when not hovering
    - 'snap-first': track while mouse hover, but snap to the first data element when not hovering
    - 'selected-hover': The same as `'hover'` tracking mode, but only when the compononent is 
    {{#crossLink "mixins.graph-selectable-graphic/selected:property"}}{{/crossLink}}
    - 'selected-snap-last': The same as `'snap-last'` tracking mode, but only when the compononent is 
    {{#crossLink "mixins.graph-selectable-graphic/selected:property"}}{{/crossLink}}
    - 'selected-snap-first': The same as `'snap-first'` tracking mode, but only when the compononent is 
    {{#crossLink "mixins.graph-selectable-graphic/selected:property"}}{{/crossLink}}

    @property trackingMode
    @type String
    @default 'none'
  */
  trackingMode: 'none',

  /**
    The radius of the tracking dot in pixels
    @property trackingDotRadius
    @type {number}
    @default 2.5
  */
  trackingDotRadius: 2.5,

  /**
    The action to send on `didTrack`.
    @property didTrack
    @type String
    @default null
  */
  didTrack: null,

  /**
    The value of the data that is being tracked by the component.
    @property trackedData
    @type {Object} an object with the following values:  
      - point: an { x, y } pair for the exact px coordinates inside the graph-content
      - graphX: domain x value at mouse position
      - graphY: domain y value at mouse position
      - x: nearest x data value
      - y: nearest y data value
      - data: nearest raw data
      - renderX: domain x value to render a tracking dot at (stacked areas are offset)
      - renderY: domain x value to render a tracking dot at (stacked areas are offset)
      - mouseX: mouse x position in pixels
      - mouseY: mouse y position in pixels
    @default null
  */
  trackedData: null,

  /**
    The value of the data that is being tracked by the component, ONLY if the 
    graph-content is currently being hovered.
    @property hoverData
    @type {Object} an object with the following values:  
      - point: an { x, y } pair for the exact px coordinates inside the graph-content
      - graphX: domain x value at mouse position
      - graphY: domain y value at mouse position
      - x: nearest x data value
      - y: nearest y data value
      - data: nearest raw data
      - renderX: domain x value to render a tracking dot at (stacked areas are offset)
      - renderY: domain x value to render a tracking dot at (stacked areas are offset)
      - mouseX: mouse x position in pixels
      - mouseY: mouse y position in pixels
    @default null
  */
  hoverData: null,

  _showTrackingDot: true,

  /**
    Gets or sets whether the tracking dot should be shown at all.
    @property showTrackingDot
    @type {boolean}
    @default true
  */
  showTrackingDot: computed('trackedData', {
    get() {
      return Boolean(this._showTrackingDot && this.get('trackedData'));
    },

    set(value) {
      this._showTrackingDot = value;
    }
  }),

  /**
    Observes changes to tracked data and sends the
    didTrack action.
    @method _trackedDataChanged
    @private
  */
  _trackedDataChanged: Ember.observer('trackedData', function(){
    var trackedData = this.get('trackedData');
    this.set('hoverData', this._hovered ? trackedData : null);

    if(this.get('didTrack')) {
      this.sendAction('didTrack', {
        x: trackedData.x,
        y: trackedData.y,
        data: trackedData.data,
        source: this,
        graph: this.get('graph'),
      });
    }
  }),

  _cleanup: function(){
    if(this._onHoverCleanup) {
      this._onHoverCleanup();
    }
    if(this._onEndCleanup) {
      this._onEndCleanup();
    }
  },

  _updateTrackingHandling() {
    var { trackingMode, selected } = this.getProperties('trackingMode', 'selected');

    this._cleanup();

    switch(trackingMode) {
      case 'hover':
        this._onHoverTrack();
        this._onEndUntrack();
        break;
      case 'snap-first':
        this._onHoverTrack();
        this._onEndSnapFirst();
        break;
      case 'snap-last':
        this._onHoverTrack();
        this._onEndSnapLast();
        break;
      case 'selected-hover':
        if(selected) {
          this._onHoverTrack();
          this._onEndUntrack();
        }
        break;
      case 'selected-snap-first':
        if(selected) {
          this._onHoverTrack();
          this._onEndSnapFirst();
        }
        break;
      case 'selected-snap-last':
        if(selected) {
          this._onHoverTrack();
          this._onEndSnapLast();
        }
        break;
    }
  },

  _onHoverTrack() {
    var content = this._content;

    var mousemoveHandler = e => {
      this._hovered = true;
      var evt = this._getEventObject(e);
      this.set('trackedData', evt);
    };

    content.on('mousemove', mousemoveHandler);

    this._onHoverCleanup = () => {
      content.off('mousemove', mousemoveHandler);
    };
  },

  _hovered: false,

  _onEndUntrack() {
    var content = this._content;

    var mouseoutHandler = () => {
      this.set('trackedData', null);
    };

    content.on('mouseout', mouseoutHandler);

    this._onEndCleanup = () => {
      content.off('mouseout', mouseoutHandler);
    };

    if(!this._hovered) {
      this.set('trackedData', null);
    }
  },

  _onEndSnapLast() {
    var content = this._content;

    var mouseoutHandler = () => {
      this._hovered = false;
      this.set('trackedData', this.get('lastVisibleData'));
    };

    var changeHandler = () => {
      if(!this._hovered) {
        this.set('trackedData', this.get('lastVisibleData'));
      }
    };

    content.on('mouseout', mouseoutHandler);
    this.addObserver('lastVisibleData', this, changeHandler);

    this._onEndCleanup = () => {
      content.off('mouseout', mouseoutHandler);
      this.removeObserver('lastVisibleData', this, changeHandler);
    };

    changeHandler();
  },

  _onEndSnapFirst() {
    var content = this._content;

    var mouseoutHandler = () => {
      this._hovered = false;
      this.set('trackedData', this.get('firstVisibleData'));
    };

    var changeHandler = () => {
      if(!this._hovered) {
        this.set('trackedData', this.get('firstVisibleData'));
      }
    };

    content.on('mouseout', mouseoutHandler);
    this.addObserver('firstVisibleData', this, changeHandler);

    this._onEndCleanup = () => {
      content.off('mouseout', mouseoutHandler);
      this.removeObserver('firstVisibleData', this, changeHandler);
    };

    changeHandler();
  },

  _trackingModeChanged: on('init', observer('trackingMode', 'selected', function() {
    Ember.run.once(this, this._updateTrackingHandling);
  })),

  _getEventObject(e) {
    var { xScale, yScale } = this.getProperties('xScale', 'yScale');
    var content = this._content;
    var point = getMousePoint(content[0], e);
    var graphX = xScale.invert(point.x);
    var graphY = yScale.invert(point.y);
    var near = this.getDataNearXRange(point.x);

    if(!near) {
      return {
        point,
        graphX,
        graphY,
        mouseX: point.x,
        mouseY: point.y,
      };
    }

    var { x, y, data, renderX, renderY } = near;
    return {
      point,
      graphX,
      graphY,
      x,
      y,
      data,
      renderX,
      renderY,
      mouseX: point.x,
      mouseY: point.y
    };
  },

  didInsertElement() {
    this._super.apply(arguments);
    this._content = this.$().parents('.nf-graph-content');
  },

  willDestroyElement() {
    this._super.apply(arguments);
    this._cleanup();
  }
});