API Docs for:
Show:

File: app/components/nf-y-diff.js

import Ember from 'ember';
import HasGraphParent from 'ember-nf-graph/mixins/graph-has-graph-parent';
import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source';
import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils';

/**
  Draws a box underneath (or over) the y axis to between the given `a` and `b`
  domain values. Component content is used to template a label in that box.

  ## Tips

  - Should be outside of `nf-graph-content`.
  - Should be "above" `nf-y-axis` in the markup.
  - As a convenience, `<text>` elements will automatically be positioned based on y-axis orientation
    due to default styling.

  @namespace components
  @class nf-y-diff
  @extends Ember.Component
  @uses mixins.graph-has-graph-parent
  @uses mixins.graph-requires-scale-source
*/
export default Ember.Component.extend(HasGraphParent, RequireScaleSource, {
  tagName: 'g',

  attributeBindings: ['transform'],

  classNameBindings: [':nf-y-diff', 'isPositive:positive:negative', 'isOrientRight:orient-right:orient-left'],

  /**
    The starting domain value of the difference measurement. The subrahend of the difference calculation.
    @property a
    @type Number
    @default null
  */
  a: null,
  
  /**
    The ending domain value of the difference measurement. The minuend of the difference calculation.
    @property b
    @type Number
    @default null
  */
  b: null,
  
  /**
    The amount of padding, in pixels, between the edge of the difference "box" and the content container
    @property contentPadding
    @type Number
    @default 5
  */
  contentPadding: 5,
  
  /**
    The duration of the transition, in milliseconds, as the difference slides vertically
    @property duration
    @type Number
    @default 400
  */
  duration: 400,

  /**
    The calculated vertical center of the difference box, in pixels.
    @property yCenter
    @type Number
    @readonly
  */
  yCenter: Ember.computed('yA', 'yB', function(){
    var yA = +this.get('yA') || 0;
    var yB = +this.get('yB') || 0;
    return (yA + yB) / 2;
  }),

  /**
    The y pixel value of b.
    @property yB
    @type Number
  */
  yB: Ember.computed('yScale', 'b', function(){
    return normalizeScale(this.get('yScale'), this.get('b'));
  }),

  /**
    The y pixel value of a.
    @property yA
    @type Number
  */
  yA: Ember.computed('yScale', 'a', function() {
    return normalizeScale(this.get('yScale'), this.get('a'));
  }),

  /**
    The SVG transformation of the component.
    @property transform
    @type String
    @private
    @readonly
  */
  transform: Ember.computed.alias('graph.yAxis.transform'),

  /**
    The calculated difference between `a` and `b`.
    @property diff
    @type Number
    @readonly
  */
  diff: Ember.computed('a', 'b', function(){
    return +this.get('b') - this.get('a');
  }),

  /**
    Returns `true` if `diff` is a positive number
    @property isPositive
    @type Boolean
    @readonly
  */
  isPositive: Ember.computed.gte('diff', 0),
  
  /**
    Returns `true` if the graph's y-axis component is configured to orient right.
    @property isOrientRight
    @type Boolean
    @readonly
  */
  isOrientRight: Ember.computed.equal('graph.yAxis.orient', 'right'),

  /**
    The width of the difference box
    @property width
    @type Number
    @readonly
  */
  width: Ember.computed.alias('graph.yAxis.width'),

  /**
    The view controller for the view this component is present in
    @property parentController
    @type Ember.Controller
    @private
    @readonly
  */
  parentController: Ember.computed.alias('templateData.view.controller'),

  /**
    The x pixel coordinate of the content container.
    @property contentX
    @type Number
    @readonly
  */
  contentX: Ember.computed('isOrientRight', 'width', 'contentPadding', function(){
    var contentPadding = this.get('contentPadding');
    var width = this.get('width');
    return this.get('isOrientRight') ? width - contentPadding : contentPadding;
  }),

  rectPath: Ember.computed('yA', 'yB', 'width', function(){
    var x = 0;
    var w = +this.get('width') || 0;
    var x2 = x + w;
    var yA = +this.get('yA') || 0;
    var yB = +this.get('yB') || 0;
    return `M${x},${yA} L${x},${yB} L${x2},${yB} L${x2},${yA} L${x},${yA}`;
  }),

  /**
    The SVG transformation used to position the content container.
    @property contentTransform
    @type String
    @private
    @readonly
  */
  contentTransform: Ember.computed('contentX', 'yCenter', function(){
    var contentX = this.get('contentX');
    var yCenter = this.get('yCenter');
    return `translate(${contentX} ${yCenter})`;
  }),

  /**
    Sets up the d3 related elements when component is inserted 
    into the DOM
    @method didInsertElement
  */
  didInsertElement: function(){
    var element = this.get('element');
    var g = d3.select(element);
    
    var rectPath = this.get('rectPath');
    var rect = g.insert('path', ':first-child')
      .attr('class', 'nf-y-diff-rect')
      .attr('d', rectPath);

    var contentTransform = this.get('contentTransform');
    var content = g.select('.nf-y-diff-content');
    content.attr('transform', contentTransform);

    this.set('rectElement', rect);
    this.set('contentElement', content);
  },

  /**
    Performs the transition (animation) of the elements.
    @method doTransition
  */
  doTransition: function(){
    var duration = this.get('duration');
    var rectElement = this.get('rectElement');
    var contentElement = this.get('contentElement');

    if(rectElement) {
      rectElement.transition().duration(duration)
        .attr('d', this.get('rectPath'));
    }

    if(contentElement) {
      contentElement.transition().duration(duration)
        .attr('transform', this.get('contentTransform'));
    }
  },

  /**
    Schedules a transition once at afterRender.
    @method transition
  */
  transition: Ember.observer('a', 'b', function(){
    Ember.run.once(this, this.doTransition);
  }),

  /**
    Updates to d3 managed DOM elments that do
    not require transitioning, because they're width-related.
    @method doAdjustWidth
  */
  doAdjustWidth: function(){
    var contentElement = this.get('contentElement');
    if(contentElement) {
      var contentTransform = this.get('contentTransform');
      contentElement.attr('transform', contentTransform);
    }
  },

  adjustGraphHeight: Ember.on('didInsertElement', Ember.observer('graph.graphHeight', function(){
    var rectElement = this.get('rectElement');
    var contentElement = this.get('contentElement');

    if(rectElement) {
      rectElement.attr('d', this.get('rectPath'));
    }

    if(contentElement) {
      contentElement.attr('transform', this.get('contentTransform'));
    }
  })),

  /**
    Schedules a call to `doAdjustWidth` on afterRender
    @method adjustWidth
  */
  adjustWidth: Ember.on(
    'didInsertElement',
    Ember.observer('isOrientRight', 'width', 'contentPadding', function(){
      Ember.run.once(this, this.doAdjustWidth);
    })
  ),
});