API Docs for:
Show:

File: app/components/nf-graph.js

import Ember from 'ember';
import GraphPosition from 'ember-nf-graph/utils/nf/graph-position';
import { getMousePoint } from 'ember-nf-graph/utils/nf/svg-dom';
import { toArray } from 'ember-nf-graph/utils/nf/array-helpers';

var Observable = Rx.Observable;

var computedBool = Ember.computed.bool;

var scaleFactoryProperty = function(axis) {
  var scaleTypeKey = axis + 'ScaleType';
  var powExponentKey = axis + 'PowerExponent';

  return Ember.computed(scaleTypeKey, powExponentKey, function(){
    var type = this.get(scaleTypeKey);
    var powExp = this.get(powExponentKey);

    type = typeof type === 'string' ? type.toLowerCase() : '';
    
    if(type === 'linear') {
      return d3.scale.linear;
    }

    else if(type === 'ordinal') {
      return d3.scale.ordinal;
    }
    
    else if(type === 'power' || type === 'pow') {
      return function(){
        return d3.scale.pow().exponent(powExp);
      };
    }
    
    else if(type === 'log') {
      return d3.scale.log;
    }
    
    else {
      Ember.warn('unknown scale type: ' + type);
      return d3.scale.linear;
    }
  });
};

var domainProperty = function(axis) {
  var dataKey = axis + 'UniqueData';
  var minKey = axis + 'Min';
  var maxKey = axis + 'Max';
  var scaleTypeKey = axis + 'ScaleType';
  var logMinKey = axis + 'LogMin';

  return Ember.computed(dataKey + '.@each', minKey, maxKey, scaleTypeKey, logMinKey, function(){
    var data = this.get(dataKey);
    var min = this.get(minKey);
    var max = this.get(maxKey);
    var scaleType = this.get(scaleTypeKey);
    var logMin = this.get(logMinKey);
    var domain = null;

    if(scaleType === 'ordinal') {
      domain = data;
    } else {
      var extent = [min, max];

      if(scaleType === 'log') {
        if (extent[0] <= 0) {
          extent[0] = logMin;
        }
        if (extent[1] <= 0) {
          extent[1] = logMin;
        }
      }

      domain = extent;
    }

    return domain;
  });
};

var scaleProperty = function(axis) {
  var scaleFactoryKey = axis + 'ScaleFactory'; 
  var rangeKey = axis + 'Range'; 
  var domainKey = axis + 'Domain'; 
  var scaleTypeKey = axis + 'ScaleType';
  var ordinalPaddingKey = axis + 'OrdinalPadding'; 
  var ordinalOuterPaddingKey = axis + 'OrdinalOuterPadding';

  return Ember.computed(
    scaleFactoryKey,
    rangeKey,
    scaleTypeKey,
    ordinalPaddingKey,
    domainKey,
    ordinalOuterPaddingKey,
    function(){
      var scaleFactory = this.get(scaleFactoryKey);
      var range = this.get(rangeKey);
      var domain = this.get(domainKey);
      var scaleType = this.get(scaleTypeKey);
      var ordinalPadding = this.get(ordinalPaddingKey);
      var ordinalOuterPadding = this.get(ordinalOuterPaddingKey);

      var scale = scaleFactory();

      if(scaleType === 'ordinal') {
        scale = scale.domain(domain).rangeBands(range, ordinalPadding, ordinalOuterPadding);
      } else {        
        scale = scale.domain(domain).range(range).clamp(true);
      }

      return scale;
    }
  );
};

var minProperty = function(axis, defaultTickCount){
  var _DataExtent_ = axis + 'DataExtent';
  var _MinMode_ = axis + 'MinMode';
  var _Axis_tickCount_ = axis + 'Axis.tickCount';
  var _ScaleFactory_ = axis + 'ScaleFactory';
  var __Min_ = '_' + axis + 'Min';
  var _prop_ = axis + 'Min';
  var _autoScaleEvent_ = 'didAutoUpdateMin' + axis.toUpperCase();

  return Ember.computed(
    _MinMode_,
    _DataExtent_,
    _Axis_tickCount_,
    _ScaleFactory_,
    function(key, value) {
      var mode = this.get(_MinMode_);
      var ext;

      if(arguments.length > 1) {
        this[__Min_] = value;
      } else {
        var self = this;

        var change = function(val) {
          self.set(_prop_, val);
          self.trigger(_autoScaleEvent_);
        };

        if(mode === 'auto') {
          change(this.get(_DataExtent_)[0] || 0);
        }

        else if(mode === 'push') {
          ext = this.get(_DataExtent_)[0];
          if(!isNaN(ext) && ext < this[__Min_]) {
            change(ext);
          }
        }

        else if(mode === 'push-tick') {
          var extent = this.get(_DataExtent_);
          ext = extent[0];

          if(!isNaN(ext) && ext < this[__Min_]) {
            var tickCount = this.get(_Axis_tickCount_) || defaultTickCount;
            var newDomain = this.get(_ScaleFactory_)().domain(extent).nice(tickCount).domain();
            change(newDomain[0]);
          }
        }
      }

      return this[__Min_];
    }
  );
};

var maxProperty = function(axis, defaultTickCount) {
  var _DataExtent_ = axis + 'DataExtent';
  var _Axis_tickCount_ = axis + 'Axis.tickCount';
  var _ScaleFactory_ = axis + 'ScaleFactory';
  var _MaxMode_ = axis + 'MaxMode';
  var __Max_ = '_' + axis + 'Max';
  var _prop_ = axis + 'Max';
  var _autoScaleEvent_ = 'didAutoUpdateMax' + axis.toUpperCase();

  return Ember.computed(
    _MaxMode_,
    _DataExtent_,
    _ScaleFactory_,
    _Axis_tickCount_,
    function(key, value) {
      var mode = this.get(_MaxMode_);
      var ext;

      if(arguments.length > 1) {
        this[__Max_] = value;
      } else {
        var self = this;

        var change = function(val) {
          self.set(_prop_, val);
          self.trigger(_autoScaleEvent_);
        };

        if(mode === 'auto') {
          change(this.get(_DataExtent_)[1] || 1);
        }

        else if(mode === 'push') {
          ext = this.get(_DataExtent_)[1];
          if(!isNaN(ext) && this[__Max_] < ext) {
            change(ext);
          }
        }

        else if(mode === 'push-tick') {
          var extent = this.get(_DataExtent_);
          ext = extent[1];

          if(!isNaN(ext) && this[__Max_] < ext) {
            var tickCount = this.get(_Axis_tickCount_) || defaultTickCount;
            var newDomain = this.get(_ScaleFactory_)().domain(extent).nice(tickCount).domain();
            change(newDomain[1]);
          }
        }
      }

      return this[__Max_];
    }
  );
};

/**
  A container component for building complex Cartesian graphs.

  ## Minimal example

       {{#nf-graph width=100 height=50}}
         {{#nf-graph-content}}
           {{nf-line data=lineData xprop="foo" yprop="bar"}}
         {{/nf-graph-content}}
       {{/nf-graph}}

  The above will create a simple 100x50 graph, with no axes, and a single line
  plotting the data it finds on each object in the array `lineData` at properties
  `foo` and `bar` for x and y values respectively.

  ## More advanced example

       {{#nf-graph width=500 height=300}}
         {{#nf-x-axis height="50" as |tick|}}
           <text>{{tick.value}}</text>
         {{/nf-x-axis}}

         {{#nf-y-axis width="120" as |tick|}}
           <text>{{tick.value}}</text>
         {{/nf-y-axis}}
   
         {{#nf-graph-content}}
           {{nf-line data=lineData xprop="foo" yprop="bar"}}
         {{/nf-graph-content}}
       {{/nf-graph}}

  The above example will create a 500x300 graph with both axes visible. The graph will not 
  render either axis unless its component is present.


  @namespace components
  @class nf-graph
  @extends Ember.Component
*/
export default Ember.Component.extend({
  tagName: 'div',  

  /**
    The exponent to use for xScaleType "pow" or "power".
    @property xPowerExponent
    @type Number
    @default 3
  */
  xPowerExponent: 3,

  /**
    The exponent to use for yScaleType "pow" or "power".
    @property yPowerExponent
    @type Number
    @default 3
  */
  yPowerExponent: 3,

  /**
    The min value to use for xScaleType "log" if xMin <= 0
    @property xLogMin
    @type Number
    @default 0.1
  */
  xLogMin: 0.1,

  /**
    The min value to use for yScaleType "log" if yMin <= 0
    @property yLogMin
    @type Number
    @default 0.1
  */
  yLogMin: 0.1,

  /** 
    Allows child compoenents to identify graph parent.
    @property isGraph
    @private
  */
  isGraph: true,

  /**
    Identifies this graph to its children as providing scales.
    @property isScaleSource
    @private
  */
  isScaleSource: true,

  /**
    @property hasRendered
    @private
  */
  hasRendered: false,

  /**
    Gets or sets the whether or not multiple selectable graphics may be
    selected simultaneously.
    @property selectMultiple
    @type Boolean
    @default false
  */
  selectMultiple: false,

  /**
    The width of the graph in pixels.
    @property width
    @type Number
    @default 300
  */
  width: 300,

  /**
    The height of the graph in pixels.
    @property height
    @type Number
    @default 100
  */
  height: 100,

  /**
    The padding at the top of the graph
    @property paddingTop
    @type Number
    @default 0
  */
  paddingTop: 0,

  /**
    The padding at the left of the graph
    @property paddingLeft
    @type Number
    @default 0
  */
  paddingLeft: 0,

  /**
    The padding at the right of the graph
    @property paddingRight
    @type Number
    @default 0
  */
  paddingRight: 0,

  /**
    The padding at the bottom of the graph
    @property paddingBottom
    @type Number
    @default 0
  */
  paddingBottom: 0,

  /**
    Determines whether to display "lanes" in the background of
    the graph.
    @property showLanes
    @type Boolean
    @default false
  */
  showLanes: false,

  /**
    Determines whether to display "frets" in the background of
    the graph.
    @property showFrets
    @type Boolean
    @default false 
  */
  showFrets: false,

  /**
    The type of scale to use for x values.
    
    Possible Values:
    - `'linear'` - a standard linear scale
    - `'log'` - a logarithmic scale
    - `'power'` - a power-based scale (exponent = 3)
    - `'ordinal'` - an ordinal scale, used for ordinal data. required for bar graphs.
    
    @property xScaleType
    @type String
    @default 'linear'
  */
  xScaleType: 'linear',

  /**
    The type of scale to use for y values.
    
    Possible Values:
    - `'linear'` - a standard linear scale
    - `'log'` - a logarithmic scale
    - `'power'` - a power-based scale (exponent = 3)
    - `'ordinal'` - an ordinal scale, used for ordinal data. required for bar graphs.
    
    @property yScaleType
    @type String
    @default 'linear'
  */
  yScaleType: 'linear',
  
  /**
    The padding between value steps when `xScaleType` is `'ordinal'`
    @property xOrdinalPadding
    @type Number
    @default 0.1
  */
  xOrdinalPadding: 0.1,

  /**
    The padding at the ends of the domain data when `xScaleType` is `'ordinal'`
    @property xOrdinalOuterPadding
    @type Number
    @default 0.1
  */
  xOrdinalOuterPadding: 0.1,

  /**
    The padding between value steps when `xScaleType` is `'ordinal'`
    @property yOrdinalPadding
    @type Number
    @default 0.1
  */
  yOrdinalPadding: 0.1,

  /**
    The padding at the ends of the domain data when `yScaleType` is `'ordinal'`
    @property yOrdinalOuterPadding
    @type Number
    @default 0.1
  */
  yOrdinalOuterPadding: 0.1,

  /**
    the `nf-y-axis` component is registered here if there is one present
    @property yAxis
    @readonly
    @default null
  */
  yAxis: null,

  /**
    The `nf-x-axis` component is registered here if there is one present
    @property xAxis
    @readonly
    @default null
  */
  xAxis: null,

  /**
    Backing field for `xMin`
    @property _xMin
    @private
  */
  _xMin: null,

  /**
    Backing field for `xMax`
    @property _xMax
    @private
  */
  _xMax: null,

  /**
    Backing field for `yMin`
    @property _yMin
    @private
  */
  _yMin: null,

  /**
    Backing field for `yMax`
    @property _yMax
    @private
  */
  _yMax: null,

  /**
    Gets or sets the minimum x domain value to display on the graph.
    Behavior depends on `xMinMode`.
    @property xMin
  */
  xMin: minProperty('x', 8),

  /**
    Gets or sets the maximum x domain value to display on the graph.
    Behavior depends on `xMaxMode`.
    @property xMax
  */
  xMax: maxProperty('x', 8),

  /**
    Gets or sets the minimum y domain value to display on the graph.
    Behavior depends on `yMinMode`.
    @property yMin
  */
  yMin: minProperty('y', 5),

  /**
    Gets or sets the maximum y domain value to display on the graph.
    Behavior depends on `yMaxMode`.
    @property yMax
  */
  yMax: maxProperty('y', 5),
  

  /**
    Sets the behavior of `xMin` for the graph.

    ### Possible values:

    - 'auto': (default) xMin is always equal to the minimum domain value contained in the graphed data. Cannot be set.
    - 'fixed': xMin can be set to an exact value and will not change based on graphed data.
    - 'push': xMin can be set to a specific value, but will update if the minimum x value contained in the graph is less than 
      what xMin is currently set to.
    - 'push-tick': xMin can be set to a specific value, but will update to next "nice" tick if the minimum x value contained in
      the graph is less than that xMin is set to.

    @property xMinMode
    @type String
    @default 'auto'
  */
  xMinMode: 'auto',

  /**
    Sets the behavior of `xMax` for the graph.

    ### Possible values:

    - 'auto': (default) xMax is always equal to the maximum domain value contained in the graphed data. Cannot be set.
    - 'fixed': xMax can be set to an exact value and will not change based on graphed data.
    - 'push': xMax can be set to a specific value, but will update if the maximum x value contained in the graph is greater than 
      what xMax is currently set to.
    - 'push-tick': xMax can be set to a specific value, but will update to next "nice" tick if the maximum x value contained in
      the graph is greater than that xMax is set to.
      
    @property xMaxMode
    @type String
    @default 'auto'
  */
  xMaxMode: 'auto',

  /**
    Sets the behavior of `yMin` for the graph.

    ### Possible values:

    - 'auto': (default) yMin is always equal to the minimum domain value contained in the graphed data. Cannot be set.
    - 'fixed': yMin can be set to an exact value and will not change based on graphed data.
    - 'push': yMin can be set to a specific value, but will update if the minimum y value contained in the graph is less than 
      what yMin is currently set to.
    - 'push-tick': yMin can be set to a specific value, but will update to next "nice" tick if the minimum y value contained in
      the graph is less than that yMin is set to.

    @property yMinMode
    @type String
    @default 'auto'
  */
  yMinMode: 'auto',

  /**
    Sets the behavior of `yMax` for the graph.

    ### Possible values:

    - 'auto': (default) yMax is always equal to the maximum domain value contained in the graphed data. Cannot be set.
    - 'fixed': yMax can be set to an exact value and will not change based on graphed data.
    - 'push': yMax can be set to a specific value, but will update if the maximum y value contained in the graph is greater than 
      what yMax is currently set to.
    - 'push-tick': yMax can be set to a specific value, but will update to next "nice" tick if the maximum y value contained in
      the graph is greater than that yMax is set to.
      
    @property yMaxMode
    @type String
    @default 'auto'
  */
  yMaxMode: 'auto',

  /**
    The data extents for all data in the registered `graphics`.

    @property dataExtents
    @type {Object}
    @default {
      xMin: Number.MAX_VALUE,
      xMax: Number.MIN_VALUE,
      yMin: Number.MAX_VALUE,
      yMax: Number.MIN_VALUE
    }
  */
  dataExtents: Ember.computed('graphics.@each.data', function(){
    var graphics = this.get('graphics');
    return graphics.reduce((c, x) => c.concat(x.get('mappedData')), []).reduce((extents, [x, y]) => {
      extents.xMin = extents.xMin < x ? extents.xMin : x;
      extents.xMax = extents.xMax > x ? extents.xMax : x;
      extents.yMin = extents.yMin < y ? extents.yMin : y;
      extents.yMax = extents.yMax > y ? extents.yMax : y;
      return extents;
    }, {
      xMin: Number.MAX_VALUE,
      xMax: Number.MIN_VALUE,
      yMin: Number.MAX_VALUE,
      yMax: Number.MIN_VALUE
    });
  }),

  /**
    The action to trigger when the graph automatically updates the xScale 
    due to an "auto" "push" or "push-tick" domainMode.

    sends the graph component instance value as the argument.

    @property autoScaleXAction
    @type {string}
    @default null
  */
  autoScaleXAction: null,

  _sendAutoUpdateXAction() {
    this.sendAction('autoScaleXAction', this);
  },

  _sendAutoUpdateYAction() {
    this.sendAction('autoScaleYAction', this);
  },

  /**
    Event handler that is fired for the `didAutoUpdateMaxX` event
    @method didAutoUpdateMaxX
  */
  didAutoUpdateMaxX() {
    Ember.run.once(this, this._sendAutoUpdateXAction);
  },

  /**
    Event handler that is fired for the `didAutoUpdateMinX` event
    @method didAutoUpdateMinX
  */
  didAutoUpdateMinX() {
    Ember.run.once(this, this._sendAutoUpdateXAction);
  },

  /**
    Event handler that is fired for the `didAutoUpdateMaxY` event
    @method didAutoUpdateMaxY
  */
  didAutoUpdateMaxY() {
    Ember.run.once(this, this._sendAutoUpdateYAction);
  },

  /**
    Event handler that is fired for the `didAutoUpdateMinY` event
    @method didAutoUpdateMinY
  */
  didAutoUpdateMinY() {
    Ember.run.once(this, this._sendAutoUpdateYAction);
  },

  /**
    The action to trigger when the graph automatically updates the yScale 
    due to an "auto" "push" or "push-tick" domainMode.

    Sends the graph component instance as the argument.

    @property autoScaleYAction
    @type {string}
    @default null
  */
  autoScaleYAction: null,

  /**
    Gets the highest and lowest x values of the graphed data in a two element array.
    @property xDataExtent
    @type Array
    @readonly
  */
  xDataExtent: Ember.computed('dataExtents', function(){
    var { xMin, xMax } = this.get('dataExtents');
    return [xMin, xMax];
  }),

  /**
    Gets the highest and lowest y values of the graphed data in a two element array.
    @property yDataExtent
    @type Array
    @readonly
  */
  yDataExtent: Ember.computed('dataExtents', function(){
    var { yMin, yMax } = this.get('dataExtents');
    return [yMin, yMax];
  }),

  /**
    @property xUniqueData
    @type Array
    @readonly
  */
  xUniqueData: Ember.computed('graphics.@each.mappedData', function(){
    var graphics = this.get('graphics');
    var uniq = graphics.reduce((uniq, graphic) => {
      return graphic.get('mappedData').reduce((uniq, d) => {
        if(!uniq.some(x => x === d[0])) {
          uniq.push(d[0]);
        }
        return uniq;
      }, uniq);
    }, []);
    return Ember.A(uniq);
  }),


  /**
    @property yUniqueData
    @type Array
    @readonly
  */
  yUniqueData: Ember.computed('graphics.@each.mappedData', function(){
    var graphics = this.get('graphics');
    var uniq = graphics.reduce((uniq, graphic) => {
      return graphic.get('mappedData').reduce((uniq, d) => {
        if(!uniq.some(y => y === d[1])) {
          uniq.push(d[1]);
        }
        return uniq;
      }, uniq);
    }, []);
    return Ember.A(uniq);
  }),

  /**
    Gets the DOM id for the content clipPath element.
    @property contentClipPathId
    @type String
    @readonly
    @private
  */
  contentClipPathId: Ember.computed('elementId', function(){
    return this.get('elementId') + '-content-mask';
  }),

  /**
    Registry of contained graphic elements such as `nf-line` or `nf-area` components.
    This registry is used to pool data for scaling purposes.
    @property graphics
    @type Array
    @readonly
   */
  graphics: Ember.computed(function(){
    return Ember.A();
  }),

  /**
    An array of "selectable" graphics that have been selected within this graph.
    @property selected
    @type Array
    @readonly
  */
  selected: null,

  /**
    Computed property to show yAxis. Returns `true` if a yAxis is present.
    @property showYAxis
    @type Boolean
    @default false
   */
  showYAxis: computedBool('yAxis'),

  /**
    Computed property to show xAxis. Returns `true` if an xAxis is present.
    @property showXAxis
    @type Boolean
    @default false
   */
  showXAxis: computedBool('xAxis'),

  /**
    Gets a function to create the xScale
    @property xScaleFactory
    @readonly
   */
  xScaleFactory: scaleFactoryProperty('x'),

  /**
    Gets a function to create the yScale
    @property yScaleFactory
    @readonly
   */
  yScaleFactory: scaleFactoryProperty('y'),

  /**
    Gets the domain of x values.
    @property xDomain
    @type Array
    @readonly
   */
  xDomain: domainProperty('x'),

  /**
    Gets the domain of y values.
    @property yDomain
    @type Array
    @readonly
   */
  yDomain: domainProperty('y'),

  /**
    Gets the current xScale used to draw the graph.
    @property xScale
    @type Function
    @readonly
   */
  xScale: scaleProperty('x'),

  /**
    Gets the current yScale used to draw the graph.
    @property yScale
    @type Function
    @readonly
   */
  yScale: scaleProperty('y'),

  /**
    Registers a graphic such as `nf-line` or `nf-area` components with the graph.
    @method registerGraphic
    @param graphic {Ember.Component} The component object to register
   */
  registerGraphic: function (graphic) {
    var graphics = this.get('graphics');
    graphics.pushObject(graphic);
    graphic.on('hasData', this, this.updateExtents);
  },

  /**
    Unregisters a graphic such as an `nf-line` or `nf-area` from the graph.
    @method unregisterGraphic
    @param graphic {Ember.Component} The component to unregister
   */
  unregisterGraphic: function(graphic) {
    graphic.off('hasData', this, this.updateExtents);
    var graphics = this.get('graphics');
    graphics.removeObject(graphic);
  },
  
  updateExtents() {
    this.get('xDataExtent');
    this.get('yDataExtent');
  },

  /**
    The y range of the graph in pixels. The min and max pixel values
    in an array form.
    @property yRange
    @type Array
    @readonly
   */
  yRange: Ember.computed('graphHeight', function(){ 
    return [this.get('graphHeight'), 0];
  }),

  /**
    The x range of the graph in pixels. The min and max pixel values
    in an array form.
    @property xRange
    @type Array
    @readonly
   */
  xRange: Ember.computed('graphWidth', function(){
    return [0, this.get('graphWidth')];
  }),

  /**
    Returns `true` if the graph has data to render. Data is conveyed
    to the graph by registered graphics.
    @property hasData
    @type Boolean
    @default false
    @readonly
   */
  hasData: Ember.computed.notEmpty('graphics'),

  /**
    The x coordinate position of the graph content
    @property graphX
    @type Number
    @readonly
   */
  graphX: Ember.computed('paddingLeft', 'yAxis.width', 'yAxis.orient', function() {
    var paddingLeft = this.get('paddingLeft');
    var yAxisWidth = this.get('yAxis.width') || 0;
    var yAxisOrient = this.get('yAxis.orient');
    if(yAxisOrient === 'right') {
      return paddingLeft;
    }
    return paddingLeft + yAxisWidth;
  }),

  /** 
    The y coordinate position of the graph content
    @property graphY
    @type Number
    @readonly
   */
  graphY: Ember.computed('paddingTop', 'xAxis.orient', 'xAxis.height', function(){
    var paddingTop = this.get('paddingTop');
    var xAxisOrient = this.get('xAxis.orient');
    if(xAxisOrient === 'top') {
      var xAxisHeight = this.get('xAxis.height') || 0;
      return xAxisHeight + paddingTop;
    }
    return paddingTop;
  }), 

  /**
    The width, in pixels, of the graph content
    @property graphWidth
    @type Number
    @readonly
   */
  graphWidth: Ember.computed('width', 'paddingRight', 'paddingLeft', 'yAxis.width', function() {
    var paddingRight = this.get('paddingRight') || 0;
    var paddingLeft = this.get('paddingLeft') || 0;
    var yAxisWidth = this.get('yAxis.width') || 0;
    var width = this.get('width') || 0;
    return Math.max(0, width - paddingRight - paddingLeft - yAxisWidth);
  }),

  /**
    The height, in pixels, of the graph content
    @property graphHeight
    @type Number
    @readonly
   */
  graphHeight: Ember.computed('height', 'paddingTop', 'paddingBottom', 'xAxis.height', function(){
    var paddingTop = this.get('paddingTop') || 0;
    var paddingBottom = this.get('paddingBottom') || 0;
    var xAxisHeight = this.get('xAxis.height') || 0;
    var height = this.get('height') || 0;
    return Math.max(0, height - paddingTop - paddingBottom - xAxisHeight);
  }),

  /**
    An SVG transform to position the graph content
    @property graphTransform
    @type String
    @readonly
   */
  graphTransform: Ember.computed('graphX', 'graphY', function(){
    var graphX = this.get('graphX');
    var graphY = this.get('graphY');
    return `translate(${graphX} ${graphY})`;
  }),

  /**
    Sets `hasRendered` to `true` on `willInsertElement`.
    @method _notifyHasRendered
    @private
  */
  _notifyHasRendered: Ember.on('willInsertElement', function () {
    this.set('hasRendered', true);
  }),

  /**
    Gets the mouse position relative to the container
    @method mousePoint
    @param container {SVGElement} the SVG element that contains the mouse event
    @param e {Object} the DOM mouse event
    @return {Array} an array of `[xMouseCoord, yMouseCoord]`
   */
  mousePoint: function (container, e) {
    var svg = container.ownerSVGElement || container;
    if (svg.createSVGPoint) {
      var point = svg.createSVGPoint();
      point.x = e.clientX;
      point.y = e.clientY;
      point = point.matrixTransform(container.getScreenCTM().inverse());
      return [ point.x, point.y ];
    }
    var rect = container.getBoundingClientRect();
    return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];
  },

  /**
    A computed property returned the view's controller.
    @property parentController
    @type Ember.Controller
    @readonly
  */
  parentController: Ember.computed.alias('templateData.view.controller'),

  /**
    Selects the graphic passed. If `selectMultiple` is false, it will deselect the currently
    selected graphic if it's different from the one passed.
    @method selectGraphic
    @param graphic {Ember.Component} the graph component to select within the graph.
  */
  selectGraphic: function(graphic) {
    if(!graphic.get('selected')) {
      graphic.set('selected', true);
    }
    if(this.selectMultiple) {
      this.get('selected').pushObject(graphic);
    } else {
      var current = this.get('selected');
      if(current && current !== graphic) {
        current.set('selected', false);
      }
      this.set('selected', graphic);
    }
  },

  /**
    deselects the graphic passed.
    @method deselectGraphic
    @param graphic {Ember.Component} the graph child component to deselect.
  */
  deselectGraphic: function(graphic) {
    graphic.set('selected', false);
    if(this.selectMultiple) {
      this.get('selected').removeObject(graphic);
    } else {
      var current = this.get('selected');
      if(current && current === graphic) {
        this.set('selected', null);
      }
    }
  },

  /**
    The initialization method. Fired on `init`.
    @method _setup
    @private
  */
  init() {
    this._super(...arguments);
    this.set('selected', this.selectMultiple ? Ember.A() : null);
  },

  /**
    The amount of leeway, in pixels, to give before triggering a brush start.
    @property brushThreshold
    @type {Number}
    @default 7
  */
  brushThreshold: 7,

  /**
    The name of the action to trigger when brushing starts
    @property brushStartAction
    @type {String}
    @default null
  */
  brushStartAction: null,

  /**
    The name of the action to trigger when brushing emits a new value
    @property brushAction
    @type {String}
    @default null
  */
  brushAction: null,

  /**
    The name of the action to trigger when brushing ends
    @property brushEndAction
    @type {String}
    @default null
  */
  brushEndAction: null,

  _setupBrushAction: Ember.on('didInsertElement', function(){
    var content = this.$('.nf-graph-content');

    var mouseMoves = Observable.fromEvent(content, 'mousemove');
    var mouseDowns = Observable.fromEvent(content, 'mousedown');
    var mouseUps = Observable.fromEvent(Ember.$(document), 'mouseup');
    var mouseLeaves = Observable.fromEvent(content, 'mouseleave');

    this._brushDisposable = Observable.merge(mouseDowns, mouseMoves, mouseLeaves).
      // get a streams of mouse events that start on mouse down and end on mouse up
      window(mouseDowns, function() { return mouseUps; })
      // filter out all of them if there are no brush actions registered
      // map the mouse event streams into brush event streams
      .map(x => this._toBrushEventStreams(x)).
      // flatten to a stream of action names and event objects
      flatMap(x => this._toComponentEventStream(x)).
      // HACK: this is fairly cosmetic, so skip errors.
      retry().
      // subscribe and send the brush actions via Ember
      subscribe(x => {
        Ember.run(this, () => this._triggerComponentEvent(x));
      });
  }),

  _toBrushEventStreams: function(mouseEvents) {
    // get the starting mouse event
    return mouseEvents.take(1).
      // calculate it's mouse point and info
      map( this._getStartInfo ).
      // combine the start with the each subsequent mouse event
      combineLatest(mouseEvents.skip(1), toArray).
      // filter out everything until the brushThreshold is crossed
      filter(x => this._byBrushThreshold(x)).
      // create the brush event object
      map(x => this._toBrushEvent(x));
  },

  _triggerComponentEvent: function(d) {
    this.trigger(d[0], d[1]);
  },

  _toComponentEventStream: function(events) {
    return Observable.merge(
      events.take(1).map(function(e) {
        return ['didBrushStart', e];
      }), events.map(function(e) {
        return ['didBrush', e];
      }), events.last().map(function(e) {
        return ['didBrushEnd', e];
      })
    );
  },

  didBrush: function(e) {
    if(this.get('brushAction')) {
      this.sendAction('brushAction', e);
    }
  },

  didBrushStart: function(e) {
    document.body.style.setProperty('-webkit-user-select', 'none');
    document.body.style.setProperty('-moz-user-select', 'none');
    document.body.style.setProperty('user-select', 'none');
    if(this.get('brushStartAction')) {
      this.sendAction('brushStartAction', e);
    }
  },

  didBrushEnd: function(e) {
    document.body.style.removeProperty('-webkit-user-select');
    document.body.style.removeProperty('-moz-user-select');
    document.body.style.removeProperty('user-select');
    if(this.get('brushEndAction')) {
      this.sendAction('brushEndAction', e);
    }
  },

  _toBrushEvent: function(d) {
    var start = d[0];
    var currentEvent =  d[1];
    var currentPoint = getMousePoint(currentEvent.currentTarget, d[1]);

    var startPosition = GraphPosition.create({
      originalEvent: start.originalEvent,
      graph: this,
      graphX: start.mousePoint.x,
      graphY: start.mousePoint.y
    });

    var currentPosition = GraphPosition.create({
      originalEvent: currentEvent,
      graph: this,
      graphX: currentPoint.x,
      graphY: currentPoint.y
    });

    var left = startPosition;
    var right = currentPosition;

    if(start.originalEvent.clientX > currentEvent.clientX) {
      left = currentPosition;
      right = startPosition;
    }

    return {
      start: startPosition,
      current: currentPosition,
      left: left,
      right: right
    }; 
  },

  _byBrushThreshold: function(d) {
    var startEvent = d[0].originalEvent;
    var currentEvent = d[1];
    return Math.abs(currentEvent.clientX - startEvent.clientX) > this.get('brushThreshold');
  },

  _getStartInfo: function(e) {
    return {
      originalEvent: e,
      mousePoint: getMousePoint(e.currentTarget, e)
    };
  },

  willDestroyElement: function(){
    if(this._brushDisposable) {
      this._brushDisposable.dispose();
    }
  },
});