API Docs for:
Show:

File: addon/mixins/graph-data-graphic.js

import Ember from 'ember';
import parsePropertyExpr from '../utils/parse-property-expression';
import { nearestIndexTo } from '../utils/nf/array-helpers';
import computed from 'ember-new-computed';

var { on, observer } = Ember;

var noop = function(){};

/**
  This is mixed in to {{#crossLink components.nf-graph}}nf-graph{{/crossLink}} child components that need to register data
  with the graph. Includes methods for extracting, sorting and scrubbing data
  for use in graphing components.

  Requires {{#crossLink "mixins.graph-registered-graphic"}}{{/crossLink}} and 
  {{#crossLink "mixins.graph-has-graph-parent"}}{{/crossLink}}

  @namespace mixins
  @class graph-data-graphic
  @extends Ember.Mixin
*/
export default Ember.Mixin.create({
  isDataGraphic: true,

  /**
    Gets or sets the data used by the component to plot itself.

    @property data
    @type Array
    @default null
  */
  data: null,

  mappedData: computed('data.@each', {
    get() {
      var yPropFn = this.get('yPropFn');
      var xPropFn = this.get('xPropFn');
      var data = this.get('data');
      if(Ember.isArray(data)) {
        return data.map(function(d, i) {
          var item = [xPropFn(d), yPropFn(d)];
          item.data = d;
          item.origIndex = i;
          return item;
        });
      }
      return [];
    }
  }),

  _triggerHasData: on('init', observer('data.@each', function(){
    Ember.run.once(this, this._sendTriggerHasData);
  })),

  _sendTriggerHasData() {
    this.trigger('hasData', this.get('mappedData'));
  },

  /**
    The path of the property on each object in 
    {{#crossLink "mixins.graph-data-graphic/data:property"}}{{/crossLink}}
    to use as x data to plot on the graph.

    @property xprop
    @type String
    @default 'x'
  */
  xprop: 'x',

  /**
    The path of the property on each object in 
    {{#crossLink "mixins.graph-data-graphic/data:property"}}{{/crossLink}}
    to use as y data to plot on the graph.

    @property yprop
    @type String
    @default 'y'
  */
  yprop: 'y',

  /**
    The function to get the x value from each 
    {{#crossLink "mixins.graph-data-graphic/data:property"}}{{/crossLink}} object

    @property xPropFn
    @type Function
    @readonly
  */
  xPropFn: computed('xprop', {
    get() {
      var xprop = this.get('xprop');
      return xprop ? parsePropertyExpr(xprop) : noop;
    }
  }),

  /**
    The function to get the y value from each 
    {{#crossLink "mixins.graph-data-graphic/data:property"}}{{/crossLink}} object

    @property yPropFn
    @type Function
    @readonly
  */
  yPropFn: computed('yprop', {
    get() {
      var yprop = this.get('yprop');
      return yprop ? parsePropertyExpr(yprop) : noop;
    }
  }),

  /**
    The list of data points from {{#crossLink "mixins.graph-data-graphc/mappedData:property"}}{{/crossLink}} that
    fits within the x domain, plus up to one data point outside of that domain in each direction.
    @property renderedData
    @type Array
    @readonly
  */
  renderedData: computed(
    'mappedData.@each',
    'graph.xScaleType',
    'graph.xMin',
    'graph.xMax',
    {
      get() {
        var mappedData = this.get('mappedData');
        var graph = this.get('graph');
        var xScaleType = graph.get('xScaleType');
        var xMin = graph.get('xMin');
        var xMax = graph.get('xMax');

        if(!mappedData || mappedData.length === 0) {
          return [];
        }

        if(xScaleType === 'ordinal') {
          return mappedData;
        }

        return mappedData.filter(function(d, i) {
          var x = d[0];
          var prev = mappedData[i-1];
          var next = mappedData[i+1];
          var prevX = prev ? prev[0] : null;
          var nextX = next ? next[0] : null;

          return between(x, xMin, xMax) || between(prevX, xMin, xMax) || between(nextX, xMin, xMax);
        });
      }
    }
  ),

  /**
    The first element from {{#crossLink "mixins.graph-data-graphic/renderedData:property"}}{{/crossLink}}
    that is actually visible within the x domain.
    @property firstVisibleData
    @type {Object}
    @readonly
  */
  firstVisibleData: computed('renderedData.@each', 'xMin', {
    get() {
      var { renderedData, xPropFn, yPropFn, xMin } = this.getProperties('renderedData', 'xPropFn', 'yPropFn', 'xMin');

      var first = renderedData[0];
      if(first && xMin > first[0] && renderedData.length > 1) {
        first = renderedData[1];
      }

      return first ? {
        x: xPropFn(first.data),
        y: yPropFn(first.data),
        data: first.data,
        renderX: first[0],
        renderY: first[1]
      } : null;
    }
  }),


  /**
    The last element from {{#crossLink "mixins.graph-data-graphic/renderedData:property"}}{{/crossLink}}
    that is actually visible within the x domain.
    @property lastVisibleData
    @type {Object}
    @readonly
  */
  lastVisibleData: computed('renderedData.@each', 'yPropFn', 'xPropFn', 'xMax', {
    get() {
      var { renderedData, xPropFn, yPropFn, xMax } = this.getProperties('renderedData', 'xPropFn', 'yPropFn', 'xMax');
      var last = renderedData[renderedData.length - 1];

      if(last && xMax < last[0] && renderedData.length > 1) {
        last = renderedData[renderedData.length - 2];
      }

      return last ? {
        x: xPropFn(last.data),
        y: yPropFn(last.data),
        data: last.data,
        renderX: last[0],
        renderY: last[1]
      }: null;
    }
  }),

  _getRenderedDataNearXRange: function(rangeX) {
    var xScale = this.get('xScale');
    var isLinear = xScale && xScale.invert;
    if(isLinear) {
      return this.getDataNearX(xScale.invert(rangeX));
    } else {
      //ordinal
      var range = this.get('graph.xRange');
      var v = Math.abs(rangeX - range[0]) / Math.abs(range[1] - range[0]);
      var renderedData = this.get('renderedData');
      var i = Math.floor(v * renderedData.length);
      return renderedData[i];
    }
  },

  getDataNearXRange(rangeX) {
    var rendered = this._getRenderedDataNearXRange(rangeX);

    if(!rendered) { 
      return null;
    }
    
    var renderX = rendered[0];
    var renderY = rendered[1];
    var data = rendered.data;
    var { x, y } = this.getActualTrackData(renderX, renderY, data);

    return { renderX, renderY, data, x, y };
  },

  /** 
    Gets the actual data at a rendered tracking point passed to it.
    This is overridden in nf-area to account for stacking of data.
    @method getActualTrackData
    @param renderX {number} the x domain value the data is rendered at
    @param renderY {number} the y domain value the data is rendered at 
    @param data {Object} the raw data from the point 
    @return {Object} simple x, y point structure
  */
  getActualTrackData(renderX, renderY, data) {
    return { x: renderX, y: renderY, data };
  },

  getDataNearX: function(x) {
    x = +x;
    if(x === x) {
      var renderedData = this.get('renderedData');
      var index = nearestIndexTo(renderedData, x, function(d){
        return d ? d[0] : null;
      });
      return index !== -1 ? renderedData[index] : null;
    }
  },
});

function between(x, a, b) {
  return a <= x && x <= b;
}