From 1b3c9ac2e42b52e3e3864ff998635ff71e57d7d3 Mon Sep 17 00:00:00 2001 From: Offir Golan Date: Mon, 1 May 2017 12:50:28 -0700 Subject: [PATCH] Refactor files into addon dir --- .eslintrc.js | 2 +- addon/components/.gitignore | 1 + addon/components/nf-area-stack.js | 107 ++ addon/components/nf-area.js | 157 ++ addon/components/nf-bars-group.js | 75 + addon/components/nf-bars.js | 183 +++ addon/components/nf-brush-selection.js | 180 +++ .../components/nf-crosshairs.js | 30 +- addon/components/nf-dot.js | 91 ++ addon/components/nf-graph-content.js | 176 +++ addon/components/nf-graph-yieldables.js | 22 + addon/components/nf-graph.js | 1289 +++++++++++++++++ addon/components/nf-group.js | 43 + addon/components/nf-horizontal-line.js | 67 + addon/components/nf-line.js | 83 ++ addon/components/nf-plot.js | 118 ++ addon/components/nf-plots.js | 45 + addon/components/nf-range-marker.js | 198 +++ addon/components/nf-range-markers.js | 94 ++ addon/components/nf-right-tick.js | 143 ++ addon/components/nf-selection-box.js | 159 ++ addon/components/nf-svg-image.js | 155 ++ addon/components/nf-svg-line.js | 100 ++ addon/components/nf-svg-path.js | 95 ++ addon/components/nf-svg-rect.js | 167 +++ addon/components/nf-tick-label.js | 17 + addon/components/nf-tracker.js | 41 + addon/components/nf-vertical-line.js | 67 + addon/components/nf-x-axis.js | 300 ++++ addon/components/nf-y-axis.js | 291 ++++ addon/components/nf-y-diff.js | 264 ++++ .../templates/components/nf-area-stack.hbs | 0 .../templates/components/nf-area.hbs | 0 .../templates/components/nf-bars-group.hbs | 0 .../templates/components/nf-bars.hbs | 0 .../components/nf-brush-selection.hbs | 0 addon/templates/components/nf-crosshairs.hbs | 7 + .../templates/components/nf-dot.hbs | 0 .../templates/components/nf-graph-content.hbs | 0 .../components/nf-graph-yieldables.hbs | 2 +- .../templates/components/nf-graph.hbs | 0 .../templates/components/nf-group.hbs | 0 .../components/nf-horizontal-line.hbs | 1 + .../templates/components/nf-line.hbs | 0 addon/templates/components/nf-plot.hbs | 1 + .../templates/components/nf-plots.hbs | 0 .../templates/components/nf-range-marker.hbs | 0 .../templates/components/nf-range-markers.hbs | 0 .../templates/components/nf-right-tick.hbs | 0 addon/templates/components/nf-svg-image.hbs | 1 + addon/templates/components/nf-svg-line.hbs | 1 + addon/templates/components/nf-svg-path.hbs | 1 + addon/templates/components/nf-svg-rect.hbs | 1 + addon/templates/components/nf-tick-label.hbs | 1 + .../templates/components/nf-tracker.hbs | 0 .../templates/components/nf-vertical-line.hbs | 1 + .../templates/components/nf-x-axis.hbs | 0 .../templates/components/nf-y-axis.hbs | 0 .../templates/components/nf-y-diff.hbs | 0 app/components/nf-area-stack.js | 106 +- app/components/nf-area.js | 158 +- app/components/nf-bars-group.js | 74 +- app/components/nf-bars.js | 182 +-- app/components/nf-brush-selection.js | 179 +-- app/components/nf-crosshairs.js | 1 + app/components/nf-dot.js | 90 +- app/components/nf-graph-content.js | 175 +-- app/components/nf-graph-yieldables.js | 21 +- app/components/nf-graph.js | 1273 +--------------- app/components/nf-group.js | 42 +- app/components/nf-horizontal-line.js | 66 +- app/components/nf-line.js | 84 +- app/components/nf-plot.js | 117 +- app/components/nf-plots.js | 44 +- app/components/nf-range-marker.js | 197 +-- app/components/nf-range-markers.js | 93 +- app/components/nf-right-tick.js | 142 +- app/components/nf-selection-box.js | 158 +- app/components/nf-svg-image.js | 154 +- app/components/nf-svg-line.js | 99 +- app/components/nf-svg-path.js | 94 +- app/components/nf-svg-rect.js | 166 +-- app/components/nf-tick-label.js | 16 +- app/components/nf-tracker.js | 40 +- app/components/nf-vertical-line.js | 66 +- app/components/nf-x-axis.js | 303 +--- app/components/nf-y-axis.js | 294 +--- app/components/nf-y-diff.js | 263 +--- app/templates/components/nf-crosshair.hbs | 2 - app/templates/components/nf-table-manager.hbs | 9 - package.json | 2 +- tests/dummy/app/controllers/nf-graph/index.js | 18 +- tests/dummy/app/templates/nf-graph/index.hbs | 22 +- 93 files changed, 4822 insertions(+), 4705 deletions(-) create mode 100644 addon/components/.gitignore create mode 100644 addon/components/nf-area-stack.js create mode 100644 addon/components/nf-area.js create mode 100644 addon/components/nf-bars-group.js create mode 100644 addon/components/nf-bars.js create mode 100644 addon/components/nf-brush-selection.js rename app/components/nf-crosshair.js => addon/components/nf-crosshairs.js (64%) create mode 100644 addon/components/nf-dot.js create mode 100644 addon/components/nf-graph-content.js create mode 100644 addon/components/nf-graph-yieldables.js create mode 100644 addon/components/nf-graph.js create mode 100644 addon/components/nf-group.js create mode 100644 addon/components/nf-horizontal-line.js create mode 100644 addon/components/nf-line.js create mode 100644 addon/components/nf-plot.js create mode 100644 addon/components/nf-plots.js create mode 100644 addon/components/nf-range-marker.js create mode 100644 addon/components/nf-range-markers.js create mode 100644 addon/components/nf-right-tick.js create mode 100644 addon/components/nf-selection-box.js create mode 100644 addon/components/nf-svg-image.js create mode 100644 addon/components/nf-svg-line.js create mode 100644 addon/components/nf-svg-path.js create mode 100644 addon/components/nf-svg-rect.js create mode 100644 addon/components/nf-tick-label.js create mode 100644 addon/components/nf-tracker.js create mode 100644 addon/components/nf-vertical-line.js create mode 100644 addon/components/nf-x-axis.js create mode 100644 addon/components/nf-y-axis.js create mode 100644 addon/components/nf-y-diff.js rename {app => addon}/templates/components/nf-area-stack.hbs (100%) rename {app => addon}/templates/components/nf-area.hbs (100%) rename {app => addon}/templates/components/nf-bars-group.hbs (100%) rename {app => addon}/templates/components/nf-bars.hbs (100%) rename {app => addon}/templates/components/nf-brush-selection.hbs (100%) create mode 100644 addon/templates/components/nf-crosshairs.hbs rename app/templates/components/nf-tick-label.hbs => addon/templates/components/nf-dot.hbs (100%) rename {app => addon}/templates/components/nf-graph-content.hbs (100%) rename {app => addon}/templates/components/nf-graph-yieldables.hbs (95%) rename {app => addon}/templates/components/nf-graph.hbs (100%) rename {app => addon}/templates/components/nf-group.hbs (100%) create mode 100644 addon/templates/components/nf-horizontal-line.hbs rename {app => addon}/templates/components/nf-line.hbs (100%) create mode 100644 addon/templates/components/nf-plot.hbs rename {app => addon}/templates/components/nf-plots.hbs (100%) rename {app => addon}/templates/components/nf-range-marker.hbs (100%) rename {app => addon}/templates/components/nf-range-markers.hbs (100%) rename {app => addon}/templates/components/nf-right-tick.hbs (100%) create mode 100644 addon/templates/components/nf-svg-image.hbs create mode 100644 addon/templates/components/nf-svg-line.hbs create mode 100644 addon/templates/components/nf-svg-path.hbs create mode 100644 addon/templates/components/nf-svg-rect.hbs create mode 100644 addon/templates/components/nf-tick-label.hbs rename {app => addon}/templates/components/nf-tracker.hbs (100%) create mode 100644 addon/templates/components/nf-vertical-line.hbs rename {app => addon}/templates/components/nf-x-axis.hbs (100%) rename {app => addon}/templates/components/nf-y-axis.hbs (100%) rename {app => addon}/templates/components/nf-y-diff.hbs (100%) create mode 100644 app/components/nf-crosshairs.js delete mode 100644 app/templates/components/nf-crosshair.hbs delete mode 100644 app/templates/components/nf-table-manager.hbs diff --git a/.eslintrc.js b/.eslintrc.js index 8bcc36e..76a85fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { }, globals: { d3: true, - rx: true + Rx: true }, rules: { } diff --git a/addon/components/.gitignore b/addon/components/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/addon/components/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/addon/components/nf-area-stack.js b/addon/components/nf-area-stack.js new file mode 100644 index 0000000..f385b85 --- /dev/null +++ b/addon/components/nf-area-stack.js @@ -0,0 +1,107 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-area-stack'; +import computed from 'ember-new-computed'; + +/** + A component for grouping and stacking `nf-area` components in an `nf-graph`. + + This component looks at the order of the `nf-area` components underneath it + and uses the ydata of the next sibling `nf-area` component to determine the bottom + of each `nf-area` components path to be drawn. + + ### Example + + {{#nf-graph width=300 height=100}} + {{#nf-graph-content}} + {{#nf-area-stack}} + {{nf-area data=myData xprop="time" yprop="high"}} + {{nf-area data=myData xprop="time" yprop="med"}} + {{nf-area data=myData xprop="time" yprop="low"}} + {{/nf-area-stack}} + {{/nf-graph-content}} + {{/nf-graph}} + + @namespace components + @class nf-area-stack +*/ +export default Ember.Component.extend({ + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + Whether or not to add the values together to create the stacked area + @property aggregate + @type {boolean} + @default false + */ + aggregate: computed({ + get() { + Ember.warn('nf-area-stack.aggregate must be set. Currently defaulting to `false` but will default to `true` in the future.'); + return this._aggregate = false; + }, + set(key, value) { + return this._aggregate = value; + } + }), + + /** + The collection of `nf-area` components under this stack. + @property areas + @type Array + @readonly + */ + areas: computed(function(){ + return Ember.A(); + }), + + /** + Registers an area component with this stack. Also links areas to one + another by setting `nextArea` on each area component. + @method registerArea + @param area {Ember.Component} The area component to register. + */ + registerArea: function(area) { + let areas = this.get('areas'); + let prev = areas[areas.length - 1]; + + Ember.run.schedule('afterRender', () => { + if(prev) { + prev.set('nextArea', area); + area.set('prevArea', prev); + } + + areas.pushObject(area); + }); + }, + + /** + Unregisters an area component from this stack. Also updates next + and previous links. + @method unregisterArea + @param area {Ember.Component} the area to unregister + */ + unregisterArea: function(area) { + let prev = area.get('prevArea'); + let next = area.get('nextArea'); + + Ember.run.schedule('afterRender', () => { + if(next) { + next.set('prevArea', prev); + } + + if(prev) { + prev.set('nextArea', next); + } + + this.get('areas').removeObject(area); + }); + }, +}); diff --git a/addon/components/nf-area.js b/addon/components/nf-area.js new file mode 100644 index 0000000..e782227 --- /dev/null +++ b/addon/components/nf-area.js @@ -0,0 +1,157 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-area'; +import Selectable from 'ember-nf-graph/mixins/graph-selectable-graphic'; +import RegisteredGraphic from 'ember-nf-graph/mixins/graph-registered-graphic'; +import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; +import AreaUtils from 'ember-nf-graph/mixins/graph-area-utils'; +import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import LineUtils from 'ember-nf-graph/mixins/graph-line-utils'; + +/** + Adds an area graph to an `nf-graph` component. + + Optionally, if it's located within an `nf-area-stack` component, it will work with + sibling `nf-area` components to create a stacked graph. + @namespace components + @class nf-area + @extends Ember.Component + @uses mixins.graph-area-utils + @uses mixins.graph-selectable-graphic + @uses mixins.graph-registered-graphic + @uses mixins.graph-data-graphic + @uses mixins.graph-graphic-with-tracking-dot + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RegisteredGraphic, DataGraphic, Selectable, AreaUtils, GraphicWithTrackingDot, RequireScaleSource, LineUtils, { + layout, + tagName: 'g', + + classNameBindings: [':nf-area', 'selected', 'selectable'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The type of d3 interpolator to use to create the area + @property interpolator + @type String + @default 'linear' + */ + interpolator: 'linear', + + /** + The previous area in the stack, if this area is part of an `nf-area-stack` + @property prevArea + @type components.nf-area + @default null + */ + prevArea: null, + + /** + The next area in the stack, if this area is part of an `nf-area-stack` + @property nextArea + @type components.nf-area + @default null + */ + nextArea: null, + + stack: null, + + init() { + this._super(...arguments); + let stack = this.get('stack'); + if(stack) { + stack.registerArea(this); + this.set('stack', stack); + } + }, + + /** + Override from `graph-data-graphic` mixin + @method getActualTrackData + */ + getActualTrackData(renderX, renderY, data) { + return { + x: this.get('xPropFn')(data), + y: this.get('yPropFn')(data) + }; + }, + + _unregisterArea: Ember.on('willDestroyElement', function(){ + let stack = this.get('stack'); + if(stack) { + stack.unregisterArea(this); + } + }), + + /** + The computed set of next y values to use for the "bottom" of the graphed area. + If the area is part of a stack, this will be the "top" of the next area in the stack, + otherwise it will return an array of values at the "bottom" of the graph domain. + @property nextYData + @type Array + @readonly + */ + nextYData: Ember.computed('data.length', 'nextArea.data.[]', function(){ + let data = this.get('data'); + if(!Array.isArray(data)) { + return []; + } + let nextData = this.get('nextArea.mappedData'); + return data.map((d, i) => (nextData && nextData[i] && nextData[i][1]) || Number.MIN_VALUE); + }), + + /** + The current rendered data "zipped" together with the nextYData. + @property mappedData + @type Array + @readonly + */ + mappedData: Ember.computed('data.[]', 'xPropFn', 'yPropFn', 'nextYData.[]', 'stack.aggregate', function() { + let { data, xPropFn, yPropFn, nextYData } = this.getProperties('data', 'xPropFn', 'yPropFn', 'nextYData'); + let aggregate = this.get('stack.aggregate'); + if(Array.isArray(data)) { + return data.map((d, i) => { + let x = xPropFn(d); + let y = yPropFn(d); + let result = aggregate ? [x, y + nextYData[i], nextYData[i]] : [x, y, nextYData[i]]; + result.data = d; + return result; + }); + } else { + return []; + } + }), + + areaFn: Ember.computed('xScale', 'yScale', 'interpolator', function(){ + let { xScale, yScale, interpolator } = this.getProperties('xScale', 'yScale', 'interpolator'); + return this.createAreaFn(xScale, yScale, interpolator); + }), + + lineFn: Ember.computed('xScale', 'yScale', 'interpolator', function(){ + let { xScale, yScale, interpolator } = this.getProperties('xScale', 'yScale', 'interpolator'); + return this.createLineFn(xScale, yScale, interpolator); + }), + + d: Ember.computed('renderedData', 'areaFn', function(){ + let renderedData = this.get('renderedData'); + return this.get('areaFn')(renderedData); + }), + + dLine: Ember.computed('renderedData', 'lineFn', function(){ + let renderedData = this.get('renderedData'); + return this.get('lineFn')(renderedData); + }), + + click: function(){ + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + } + }); diff --git a/addon/components/nf-bars-group.js b/addon/components/nf-bars-group.js new file mode 100644 index 0000000..aeb721f --- /dev/null +++ b/addon/components/nf-bars-group.js @@ -0,0 +1,75 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-bars-group'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +export default Ember.Component.extend(RequiresScaleSource, { + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + groupPadding: 0.1, + + groupOuterPadding: 0, + + // either b-arses or fat, stupid hobbitses + barses: Ember.computed(function(){ + return Ember.A(); + }), + + registerBars: function(bars) { + Ember.run.schedule('afterRender', () => { + let barses = this.get('barses'); + barses.pushObject(bars); + bars.set('group', this); + bars.set('groupIndex', barses.length - 1); + }); + }, + + unregisterBars: function(bars) { + if(bars) { + Ember.run.schedule('afterRender', () => { + bars.set('group', undefined); + bars.set('groupIndex', undefined); + this.get('barses').removeObject(bars); + }); + } + }, + + groupWidth: Ember.computed('xScale', function(){ + let xScale = this.get('xScale'); + return xScale && xScale.rangeBand ? xScale.rangeBand() : NaN; + }), + + barsDomain: Ember.computed('barses.[]', function(){ + let len = this.get('barses.length') || 0; + return d3.range(len); + }), + + barScale: Ember.computed( + 'groupWidth', + 'barsDomain.[]', + 'groupPadding', + 'groupOuterPadding', + function(){ + let barsDomain = this.get('barsDomain'); + let groupWidth = this.get('groupWidth'); + let groupPadding = this.get('groupPadding'); + let groupOuterPadding = this.get('groupOuterPadding'); + return d3.scale.ordinal() + .domain(barsDomain) + .rangeBands([0, groupWidth], groupPadding, groupOuterPadding); + } + ), + + barsWidth: function() { + let scale = this.get('barScale'); + return scale && scale.rangeBand ? scale.rangeBand() : NaN; + }, +}); diff --git a/addon/components/nf-bars.js b/addon/components/nf-bars.js new file mode 100644 index 0000000..aaacc2a --- /dev/null +++ b/addon/components/nf-bars.js @@ -0,0 +1,183 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-bars'; +import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; +import RegisteredGraphic from 'ember-nf-graph/mixins/graph-registered-graphic'; +import parsePropExpr from 'ember-nf-graph/utils/parse-property-expression'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; +import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; +import { getRectPath } from 'ember-nf-graph/utils/nf/svg-dom'; + +/** + Adds a bar graph to an `nf-graph` component. + + **Requires the graph has `xScaleType === 'ordinal'`*** + + ** `showTrackingDot` defaults to `false` in this component ** + + @namespace components + @class nf-bars + @extends Ember.Component + @uses mixins.graph-registered-graphic + @uses mixins.graph-data-graphic + @uses mixins.graph-requires-scale-source + @uses mixins.graph-graphic-with-tracking-dot +*/ +export default Ember.Component.extend(RegisteredGraphic, DataGraphic, RequireScaleSource, GraphicWithTrackingDot, { + layout, + tagName: 'g', + + classNames: ['nf-bars'], + + _showTrackingDot: false, + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The name of the property on each data item containing the className for the bar rectangle + @property classprop + @type String + @default 'className' + */ + classprop: 'className', + + /** + Gets the function to get the classname from each data item. + @property getBarClass + @readonly + @private + */ + getBarClass: Ember.computed('classprop', function() { + let classprop = this.get('classprop'); + return classprop ? parsePropExpr(classprop) : null; + }), + + /** + The nf-bars-group this belongs to, if any. + @property group + @type components.nf-bars-group + @default null + */ + group: null, + + /** + The index of this component within the group, if any. + @property groupIndex + @type Number + @default null + */ + groupIndex: null, + + /** + The graph content height + @property graphHeight + @type Number + @readonly + */ + graphHeight: Ember.computed.oneWay('graph.graphHeight'), + + /** + A scale provided by nf-bars-group to offset the bar rectangle output + @property barScale + @type d3.scale + @readonly + */ + barScale: Ember.computed.oneWay('group.barScale'), + + /** + The width of each bar. + @property barWidth + @type Number + @readonly + */ + barWidth: Ember.computed('xScale', 'barScale', function(){ + let barScale = this.get('barScale'); + if(barScale) { + return barScale.rangeBand(); + } + let xScale = this.get('xScale'); + return xScale && xScale.rangeBand ? xScale.rangeBand() : 0; + }), + + groupOffsetX: Ember.computed('barScale', 'groupIndex', function(){ + let barScale = this.get('barScale'); + let groupIndex = this.get('groupIndex'); + return normalizeScale(barScale, groupIndex); + }), + + /** + The bar models used to render the bars. + @property bars + @readonly + */ + bars: Ember.computed( + 'xScale', + 'yScale', + 'renderedData.[]', + 'graphHeight', + 'getBarClass', + 'barWidth', + 'groupOffsetX', + function(){ + let { renderedData, xScale, yScale, barWidth, graphHeight, getBarClass, groupOffsetX } = + this.getProperties('renderedData', 'xScale', 'yScale', 'graphHeight', 'getBarClass', 'groupOffsetX', 'barWidth'); + + let getRectPath = this._getRectPath; + + if(!xScale || !yScale || !Ember.isArray(renderedData)) { + return null; + } + + let w = barWidth; + + return Ember.A(renderedData.map(function(data) { + let className = 'nf-bars-bar' + (getBarClass ? ' ' + getBarClass(data.data) : ''); + let x = normalizeScale(xScale, data[0]) + groupOffsetX; + let y = normalizeScale(yScale, data[1]); + let h = graphHeight - y; + let path = getRectPath(x, y, w, h); + + return { path, className, data }; + })); + } + ), + + _getRectPath: getRectPath, + + /** + The name of the action to fire when a bar is clicked. + @property barClick + @type String + @default null + */ + barClick: null, + + init() { + this._super(...arguments); + let group = this.get('group'); + if(group && group.registerBars) { + group.registerBars(this); + } + }, + + actions: { + nfBarClickBar: function(dataPoint) { + if(this.get('barClick')) { + this.sendAction('barClick', { + data: dataPoint.data, + x: dataPoint[0], + y: dataPoint[1], + source: this, + graph: this.get('graph'), + }); + } + } + } + +}); diff --git a/addon/components/nf-brush-selection.js b/addon/components/nf-brush-selection.js new file mode 100644 index 0000000..49d6db3 --- /dev/null +++ b/addon/components/nf-brush-selection.js @@ -0,0 +1,180 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-brush-selection'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +export default Ember.Component.extend(RequiresScaleSource, { + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + left: undefined, + + right: undefined, + + formatter: null, + + textPadding: 3, + + autoWireUp: true, + + _autoBrushHandler: function(e) { + this.set('left', Ember.get(e, 'left.x')); + this.set('right', Ember.get(e, 'right.x')); + }, + + _autoBrushEndHandler: function() { + this.set('left', undefined); + this.set('right', undefined); + }, + + _wireToGraph: function(){ + let graph = this.get('graph'); + let auto = this.get('autoWireUp'); + + if(auto) { + graph.on('didBrushStart', this, this._autoBrushHandler); + graph.on('didBrush', this, this._autoBrushHandler); + graph.on('didBrushEnd', this, this._autoBrushEndHandler); + } else { + graph.off('didBrushStart', this, this._autoBrushHandler); + graph.off('didBrush', this, this._autoBrushHandler); + graph.off('didBrushEnd', this, this._autoBrushEndHandler); + } + }, + + _autoWireUpChanged: Ember.on('didInsertElement', Ember.observer('autoWireUp', function(){ + Ember.run.scheduleOnce('afterRender', this, this._wireToGraph); + })), + + _updateLeftText: function(){ + let root = d3.select(this.element); + let g = root.select('.nf-brush-selection-left-display'); + let text = g.select('.nf-brush-selection-left-text'); + let bg = g.select('.nf-brush-selection-left-text-bg'); + + let display = this.get('leftDisplay'); + + if(!display) { + g.attr('hidden', true); + } else { + g.attr('hidden', null); + } + + text.text(display); + + let textPadding = this.get('textPadding'); + let leftX = this.get('leftX'); + let graphHeight = this.get('graphHeight'); + let bbox = text[0][0].getBBox(); + + let doublePad = textPadding * 2; + let width = bbox.width + doublePad; + let height = bbox.height + doublePad; + let x = Math.max(0, leftX - width); + let y = graphHeight - height; + + g.attr('transform', `translate(${x} ${y})`); + + text.attr('x', textPadding). + attr('y', textPadding); + + bg.attr('width', width). + attr('height', height); + }, + + _onLeftChange: Ember.on( + 'didInsertElement', + Ember.observer('left', 'graphHeight', 'textPadding', function(){ + Ember.run.scheduleOnce('afterRender', this, this._updateLeftText); + }) + ), + + _updateRightText: function(){ + let root = d3.select(this.element); + let g = root.select('.nf-brush-selection-right-display'); + let text = g.select('.nf-brush-selection-right-text'); + let bg = g.select('.nf-brush-selection-right-text-bg'); + + let display = this.get('rightDisplay'); + + if(!display) { + g.attr('hidden', true); + } else { + g.attr('hidden', null); + } + + text.text(display); + + let textPadding = this.get('textPadding'); + let rightX = this.get('rightX'); + let graphHeight = this.get('graphHeight'); + let graphWidth = this.get('graphWidth'); + let bbox = text[0][0].getBBox(); + + let doublePad = textPadding * 2; + let width = bbox.width + doublePad; + let height = bbox.height + doublePad; + let x = Math.min(graphWidth - width, rightX); + let y = graphHeight - height; + + g.attr('transform', `translate(${x} ${y})`); + + text.attr('x', textPadding). + attr('y', textPadding); + + bg.attr('width', width). + attr('height', height); + }, + + _onRightChange: Ember.on( + 'didInsertElement', + Ember.observer('right', 'graphHeight', 'graphWidth', 'textPadding', function(){ + Ember.run.scheduleOnce('afterRender', this, this._updateRightText); + }) + ), + + leftDisplay: Ember.computed('left', 'formatter', function(){ + let formatter = this.get('formatter'); + let left = this.get('left'); + return formatter ? formatter(left) : left; + }), + + rightDisplay: Ember.computed('right', 'formatter', function(){ + let formatter = this.get('formatter'); + let right = this.get('right'); + return formatter ? formatter(right) : right; + }), + + isVisible: Ember.computed('left', 'right', function(){ + let left = +this.get('left'); + let right = +this.get('right'); + return left === left && right === right; + }), + + leftX: Ember.computed('xScale', 'left', function() { + let left = this.get('left') || 0; + let scale = this.get('xScale'); + return scale ? scale(left) : 0; + }), + + rightX: Ember.computed('xScale', 'right', function() { + let right = this.get('right') || 0; + let scale = this.get('xScale'); + return scale ? scale(right) : 0; + }), + + graphWidth: Ember.computed.alias('graph.graphWidth'), + + graphHeight: Ember.computed.alias('graph.graphHeight'), + + rightWidth: Ember.computed('rightX', 'graphWidth', function() { + return Math.max(this.get('graphWidth') - this.get('rightX'), 0); + }), +}); diff --git a/app/components/nf-crosshair.js b/addon/components/nf-crosshairs.js similarity index 64% rename from app/components/nf-crosshair.js rename to addon/components/nf-crosshairs.js index 35f1126..8a46a8c 100644 --- a/app/components/nf-crosshair.js +++ b/addon/components/nf-crosshairs.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-crosshairs'; const { on, @@ -6,7 +7,7 @@ const { } = Ember; /** - A component that adds a "crosshair" to an `nf-graph` that follows the mouse + A component that adds "crosshairs" to an `nf-graph` that follows the mouse while it's hovering over the graph content. @namespace components @class nf-crosshair @@ -14,9 +15,10 @@ const { @uses mixins.graph-has-graph-parent */ export default Ember.Component.extend({ + layout, tagName: 'g', - classNames: ['nf-crosshair'], + classNames: ['nf-crosshairs'], /** The parent graph for a component. @@ -58,6 +60,22 @@ export default Ember.Component.extend({ */ y: 0, + /** + Whether to show the vertical line in the corsshairs + @property vertical + @type Boolean + @default true + */ + vertical: true, + + /** + Whether to show the horizontal line in the corsshairs + @property horizontal + @type Boolean + @default true + */ + horizontal: true, + /** The visibility of the component @property isVisible @@ -76,11 +94,13 @@ export default Ember.Component.extend({ this.set('isVisible', false); }, - _setupBindings: on('init', observer('graph.content', function() { + _setupBindings: on('didInsertElement', observer('graph.content', function() { let content = this.get('graph.content'); if(content) { - content.on('didHoverChange', this, this.didContentHoverChange); - content.on('didHoverEnd', this, this.didContentHoverEnd); + Ember.run.schedule('afterRender', () => { + content.on('didHoverChange', this, this.didContentHoverChange); + content.on('didHoverEnd', this, this.didContentHoverEnd); + }); } })), }); diff --git a/addon/components/nf-dot.js b/addon/components/nf-dot.js new file mode 100644 index 0000000..cb58488 --- /dev/null +++ b/addon/components/nf-dot.js @@ -0,0 +1,91 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-dot'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +/** + Plots a circle at a given x and y domain value on an `nf-graph`. + + @namespace components + @class nf-dot + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'circle', + + attributeBindings: ['r', 'cy', 'cx'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The x domain value at which to plot the circle + @property x + @type Number + @default null + */ + x: null, + + /** + The y domain value at which to plot the circle + @property x + @type Number + @default null + */ + y: null, + + /** + The radius of the circle plotted + @property r + @type Number + @default 2.5 + */ + r: 2.5, + + hasX: Ember.computed.notEmpty('x'), + + hasY: Ember.computed.notEmpty('y'), + + /** + The computed center x coordinate of the circle + @property cx + @type Number + @private + @readonly + */ + cx: Ember.computed('x', 'xScale', 'hasX', function(){ + let x = this.get('x'); + let xScale = this.get('xScale'); + let hasX = this.get('hasX'); + return hasX && xScale ? xScale(x) : -1; + }), + + /** + The computed center y coordinate of the circle + @property cy + @type Number + @private + @readonly + */ + cy: Ember.computed('y', 'yScale', 'hasY', function() { + let y = this.get('y'); + let yScale = this.get('yScale'); + let hasY = this.get('hasY'); + return hasY && yScale ? yScale(y) : -1; + }), + + /** + Toggles the visibility of the dot. If x or y are + not numbers, will return false. + @property isVisible + @private + @readonly + */ + isVisible: Ember.computed.and('hasX', 'hasY'), +}); diff --git a/addon/components/nf-graph-content.js b/addon/components/nf-graph-content.js new file mode 100644 index 0000000..b020925 --- /dev/null +++ b/addon/components/nf-graph-content.js @@ -0,0 +1,176 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-graph-content'; +import GraphMouseEvent from 'ember-nf-graph/utils/nf/graph-mouse-event'; + +/** + Container component for graphics to display in `nf-graph`. Represents + the area where the graphics, such as lines will display. + + Exists for layout purposes. + @namespace components + @class nf-graph-content +*/ +export default Ember.Component.extend({ + layout, + tagName: 'g', + + classNames: ['nf-graph-content'], + + attributeBindings: ['transform', 'clip-path'], + + 'clip-path': Ember.computed('graph.contentClipPathId', function(){ + let clipPathId = this.get('graph.contentClipPathId'); + return `url('#${clipPathId}')`; + }), + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The SVG transform for positioning the graph content + @property transform + @type String + @readonly + */ + transform: Ember.computed('x', 'y', function(){ + let x = this.get('x'); + let y = this.get('y'); + return `translate(${x} ${y})`; + }), + + /** + The x position of the graph content + @property x + @type Number + @readonly + */ + x: Ember.computed.alias('graph.graphX'), + + /** + The calculated y position of the graph content + @property y + @type Number + @readonly + */ + y: Ember.computed.alias('graph.graphY'), + + /** + The calculated width of the graph content + @property width + @type Number + @readonly + */ + width: Ember.computed.alias('graph.graphWidth'), + + /** + The calculated height of the graph content. + @property height + @type Number + @readonly + */ + height: Ember.computed.alias('graph.graphHeight'), + + + /** + An array containing models to render the grid lanes + @property gridLanes + @type Array + @readonly + */ + gridLanes: Ember.computed('graph.yAxis.ticks', 'width', 'height', function () { + let ticks = this.get('graph.yAxis.ticks'); + let width = this.get('width'); + let height = this.get('height'); + + if(!ticks || ticks.length === 0) { + return Ember.A(); + } + + let sorted = ticks.slice().sort(function(a, b) { + return a.y - b.y; + }); + + if(sorted[0].y !== 0) { + sorted.unshift({ y: 0 }); + } + + let lanes = sorted.reduce(function(lanes, tick, i) { + let y = tick.y; + let next = sorted[i+1] || { y: height }; + let h = Math.max(next.y - tick.y, 0); + lanes.push({ + x: 0, + y: y, + width: width, + height: h + }); + return lanes; + }, []); + + return Ember.A(lanes); + }), + + /** + The name of the hoverChange action to fire + @property hoverChange + @type String + @default null + */ + hoverChange: null, + + mouseMove: function(e) { + let context = GraphMouseEvent.create({ + originalEvent: e, + source: this, + graphContentElement: this.element, + }); + + this.trigger('didHoverChange', context); + + if(this.get('hoverChange')) { + this.sendAction('hoverChange', context); + } + }, + + /** + The name of the hoverEnd action to fire + @property hoverEnd + @type String + @default null + */ + hoverEnd: null, + + mouseLeave: function(e) { + let context = GraphMouseEvent.create({ + originalEvent: e, + source: this, + graphContentElement: this.element + }); + this.trigger('didHoverEnd', context); + + if(this.get('hoverEnd')) { + this.sendAction('hoverEnd', context); + } + }, + + /** + An array containing models to render fret lines + @property frets + @type Array + @readonly + */ + frets: Ember.computed.alias('graph.xAxis.ticks'), + + init(){ + this._super(...arguments); + + Ember.run.schedule('afterRender', () => { + this.set('graph.content', this); + }); + }, +}); diff --git a/addon/components/nf-graph-yieldables.js b/addon/components/nf-graph-yieldables.js new file mode 100644 index 0000000..a831bcb --- /dev/null +++ b/addon/components/nf-graph-yieldables.js @@ -0,0 +1,22 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-graph-yieldables'; + +export default Ember.Component.extend({ + layout, + tagName: '', + + /** + The parent graph for the components. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The scale source for the components + @property scaleSource + @default null + */ + scaleSource: null, +}); diff --git a/addon/components/nf-graph.js b/addon/components/nf-graph.js new file mode 100644 index 0000000..359c6bc --- /dev/null +++ b/addon/components/nf-graph.js @@ -0,0 +1,1289 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-graph'; +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'; +import computed from 'ember-new-computed'; + +const Observable = Rx.Observable; + +const computedBool = computed.bool; + +const { + isPresent, + observer +} = Ember; + +let minProperty = function(axis, defaultTickCount){ + let _DataExtent_ = axis + 'DataExtent'; + let _MinMode_ = axis + 'MinMode'; + let _Axis_tickCount_ = axis + 'Axis.tickCount'; + let _ScaleFactory_ = axis + 'ScaleFactory'; + let __Min_ = '_' + axis + 'Min'; + let _prop_ = axis + 'Min'; + let _autoScaleEvent_ = 'didAutoUpdateMin' + axis.toUpperCase(); + + return computed( + _MinMode_, + _DataExtent_, + _Axis_tickCount_, + _ScaleFactory_, + 'graphHeight', + 'graphWidth', + { + get() { + let mode = this.get(_MinMode_); + let ext; + + let change = val => { + this.set(_prop_, val); + this.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') { + let extent = this.get(_DataExtent_); + ext = extent[0]; + + if(!isNaN(ext) && ext < this[__Min_]) { + let tickCount = this.get(_Axis_tickCount_) || defaultTickCount; + let newDomain = this.get(_ScaleFactory_)().domain(extent).nice(tickCount).domain(); + change(newDomain[0]); + } + } + + return this[__Min_]; + }, + set(key, value) { + if (isPresent(value) && !isNaN(value)) { + this[__Min_] = value; + } + return this[__Min_]; + } + } + ); +}; + +let maxProperty = function(axis, defaultTickCount) { + let _DataExtent_ = axis + 'DataExtent'; + let _Axis_tickCount_ = axis + 'Axis.tickCount'; + let _ScaleFactory_ = axis + 'ScaleFactory'; + let _MaxMode_ = axis + 'MaxMode'; + let __Max_ = '_' + axis + 'Max'; + let _prop_ = axis + 'Max'; + let _autoScaleEvent_ = 'didAutoUpdateMax' + axis.toUpperCase(); + + return computed( + _MaxMode_, + _DataExtent_, + _ScaleFactory_, + _Axis_tickCount_, + 'graphHeight', + 'graphWidth', + { + get() { + let mode = this.get(_MaxMode_); + let ext; + + let change = val => { + this.set(_prop_, val); + this.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') { + let extent = this.get(_DataExtent_); + ext = extent[1]; + + if(!isNaN(ext) && this[__Max_] < ext) { + let tickCount = this.get(_Axis_tickCount_) || defaultTickCount; + let newDomain = this.get(_ScaleFactory_)().domain(extent).nice(tickCount).domain(); + change(newDomain[1]); + } + } + + return this[__Max_]; + }, + set(key, value) { + if (isPresent(value) && !isNaN(value)) { + this[__Max_] = value; + } + 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|}} + {{tick.value}} + {{/nf-x-axis}} + + {{#nf-y-axis width="120" as |tick|}} + {{tick.value}} + {{/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({ + layout, + 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, + + /** + @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: computed('graphics.@each.data', function(){ + let 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.scheduleOnce('afterRender', this, this._sendAutoUpdateXAction); + }, + + /** + Event handler that is fired for the `didAutoUpdateMinX` event + @method didAutoUpdateMinX + */ + didAutoUpdateMinX() { + Ember.run.scheduleOnce('afterRender', this, this._sendAutoUpdateXAction); + }, + + /** + Event handler that is fired for the `didAutoUpdateMaxY` event + @method didAutoUpdateMaxY + */ + didAutoUpdateMaxY() { + Ember.run.scheduleOnce('afterRender', this, this._sendAutoUpdateYAction); + }, + + /** + Event handler that is fired for the `didAutoUpdateMinY` event + @method didAutoUpdateMinY + */ + didAutoUpdateMinY() { + Ember.run.scheduleOnce('afterRender', 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: computed('dataExtents', function(){ + let { 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: computed('dataExtents', function(){ + let { yMin, yMax } = this.get('dataExtents'); + return [yMin, yMax]; + }), + + /** + @property xUniqueData + @type Array + @readonly + */ + xUniqueData: computed('graphics.@each.mappedData', function(){ + let graphics = this.get('graphics'); + let 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: computed('graphics.@each.mappedData', function(){ + let graphics = this.get('graphics'); + let 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: 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: computed(function(){ + return Ember.A(); + }), + + /** + An array of "selectable" graphics that have been selected within this graph. + @property selected + @type Array + @readonly + */ + selected: Ember.computed(function() { + return this.get('selectMultiple') ? Ember.A() : 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'), + xScaleFactory: Ember.computed(function() { + return this._scaleFactoryFor('x'); + }), + _scheduleXScaleFactory: observer('xScaleType', 'xPowerExponent', function() { + Ember.run.schedule('afterRender', () => { + this.set('xScaleFactory', this._scaleFactoryFor('x')); + }); + }), + + /** + Gets a function to create the yScale + @property yScaleFactory + @readonly + */ + // yScaleFactory: scaleFactoryProperty('y'), + yScaleFactory: Ember.computed(function() { + return this._scaleFactoryFor('y'); + }), + _scheduleYScaleFactory: observer('yScaleType', 'yPowerExponent', function() { + Ember.run.schedule('afterRender', () => { + this.set('yScaleFactory', this._scaleFactoryFor('y')); + }); + }), + + _scaleFactoryFor(axis) { + let type = this.get(`${axis}ScaleType`); + let powExp = this.get(`${axis}PowerExponent`); + + type = typeof type === 'string' ? type.toLowerCase() : ''; + + if(type === 'linear') { + return d3.scale.linear; + } + + else if(type === 'ordinal') { + return function(){ + let scale = d3.scale.ordinal(); + // ordinal scales don't have an invert function, so we need to add one + scale.invert = function(rv) { + let [min, max] = d3.extent(scale.range()); + let domain = scale.domain(); + let i = Math.round((domain.length - 1) * (rv - min) / (max - min)); + return domain[i]; + }; + return scale; + }; + } + + 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; + } + }, + + /** + Gets the domain of x values. + @property xDomain + @type Array + @readonly + */ + xDomain: Ember.computed(function() { + return this._domainFor('x'); + }), + _scheduleXDomain: observer( + 'xUniqueData.[]', + 'xMin', + 'xMax', + 'xScaleType', + 'xLogMin', + function() { + Ember.run.schedule('afterRender', () => { + this.set('xDomain', this._domainFor('x')); + }); + } + ), + + /** + Gets the domain of y values. + @property yDomain + @type Array + @readonly + */ + yDomain: Ember.computed(function() { + return this._domainFor('y'); + }), + + /* + NOTE: Although this can be done in a CP, we must compute + this value only `afterRender` to avoid double render deprecations. + */ + _scheduleYDomain: observer( + 'yUniqueData.[]', + 'yMin', + 'yMax', + 'yScaleType', + 'yLogMin', + function() { + Ember.run.schedule('afterRender', () => { + this.set('yDomain', this._domainFor('y')); + }); + } + ), + + _domainFor(axis) { + let data = this.get(`${axis}UniqueData`); + let min = this.get(`${axis}Min`); + let max = this.get(`${axis}Max`); + let scaleType = this.get(`${axis}ScaleType`); + let logMin = this.get(`${axis}LogMin`); + let domain = null; + + if(scaleType === 'ordinal') { + domain = data; + } else { + let extent = [min, max]; + + if(scaleType === 'log') { + if (extent[0] <= 0) { + extent[0] = logMin; + } + if (extent[1] <= 0) { + extent[1] = logMin; + } + } + + domain = extent; + } + + return domain; + }, + + /** + Gets the current xScale used to draw the graph. + @property xScale + @type Function + @readonly + */ + xScale: Ember.computed(function() { + return this._scaleFor('x'); + }), + + /* + NOTE: Although this can be done in a CP, we must compute + this value only `afterRender` to avoid double render deprecations. + */ + _scheduleXScale: observer( + 'xScaleFactory', + 'xRange', + 'xDomain', + 'xScaleType', + 'xOrdinalPadding', + 'xOrdinalOuterPadding', + function() { + Ember.run.schedule('afterRender', () => { + this.set('xScale', this._scaleFor('x')); + }); + } + ), + + /** + Gets the current yScale used to draw the graph. + @property yScale + @type Function + @readonly + */ + yScale: Ember.computed(function() { + return this._scaleFor('y'); + }), + + /* + NOTE: Although this can be done in a CP, we must compute + this value only `afterRender` to avoid double render deprecations. + */ + _scheduleYScale: observer( + 'yScaleFactory', + 'yRange', + 'yDomain', + 'yScaleType', + 'yOrdinalPadding', + 'yOrdinalOuterPadding', + function() { + Ember.run.schedule('afterRender', () => { + this.set('yScale', this._scaleFor('y')); + }); + } + ), + + _scaleFor(axis) { + let scaleFactory = this.get(`${axis}ScaleFactory`); + let range = this.get(`${axis}Range`); + let domain = this.get(`${axis}Domain`); + let scaleType = this.get(`${axis}ScaleType`); + let ordinalPadding = this.get(`${axis}OrdinalPadding`); + let ordinalOuterPadding = this.get(`${axis}OrdinalOuterPadding`); + + let scale = scaleFactory(); + + if(scaleType === 'ordinal') { + scale = scale.domain(domain).rangeBands(range, ordinalPadding, ordinalOuterPadding); + } else { + scale = scale.domain(domain).range(range).clamp(true); + } + + return scale; + }, + + /** + 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) { + Ember.run.schedule('afterRender', () => { + let graphics = this.get('graphics'); + graphic.on('hasData', this, this.updateExtents); + graphics.pushObject(graphic); + }); + }, + + /** + 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) { + Ember.run.schedule('afterRender', () => { + let graphics = this.get('graphics'); + graphic.off('hasData', this, this.updateExtents); + 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: 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: 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: computed.notEmpty('graphics'), + + /** + The x coordinate position of the graph content + @property graphX + @type Number + @readonly + */ + graphX: computed('paddingLeft', 'yAxis.width', 'yAxis.orient', function() { + let paddingLeft = this.get('paddingLeft'); + let yAxisWidth = this.get('yAxis.width') || 0; + let 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: computed('paddingTop', 'xAxis.orient', 'xAxis.height', function(){ + let paddingTop = this.get('paddingTop'); + let xAxisOrient = this.get('xAxis.orient'); + if(xAxisOrient === 'top') { + let 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: computed('width', 'paddingRight', 'paddingLeft', 'yAxis.width', function() { + let paddingRight = this.get('paddingRight') || 0; + let paddingLeft = this.get('paddingLeft') || 0; + let yAxisWidth = this.get('yAxis.width') || 0; + let 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: computed('height', 'paddingTop', 'paddingBottom', 'xAxis.height', function(){ + let paddingTop = this.get('paddingTop') || 0; + let paddingBottom = this.get('paddingBottom') || 0; + let xAxisHeight = this.get('xAxis.height') || 0; + let 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: computed('graphX', 'graphY', function(){ + let graphX = this.get('graphX'); + let graphY = this.get('graphY'); + return `translate(${graphX} ${graphY})`; + }), + + /** + Sets `hasRendered` to `true` on `willInsertElement`. + @method _notifyHasRendered + @private + */ + _notifyHasRendered: Ember.on('willInsertElement', function () { + Ember.run.schedule('afterRender', () => { + 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) { + let svg = container.ownerSVGElement || container; + if (svg.createSVGPoint) { + let point = svg.createSVGPoint(); + point.x = e.clientX; + point.y = e.clientY; + point = point.matrixTransform(container.getScreenCTM().inverse()); + return [ point.x, point.y ]; + } + let rect = container.getBoundingClientRect(); + return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; + }, + + /** + 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 { + let 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 { + let current = this.get('selected'); + if(current && current === graphic) { + this.set('selected', 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(){ + let content = this.$('.nf-graph-content'); + + let mouseMoves = Observable.fromEvent(content, 'mousemove'); + let mouseDowns = Observable.fromEvent(content, 'mousedown'); + let mouseUps = Observable.fromEvent(Ember.$(document), 'mouseup'); + let 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) { + let start = d[0]; + let currentEvent = d[1]; + let currentPoint = getMousePoint(currentEvent.currentTarget, d[1]); + + let startPosition = GraphPosition.create({ + originalEvent: start.originalEvent, + graph: this, + graphX: start.mousePoint.x, + graphY: start.mousePoint.y + }); + + let currentPosition = GraphPosition.create({ + originalEvent: currentEvent, + graph: this, + graphX: currentPoint.x, + graphY: currentPoint.y + }); + + let left = startPosition; + let right = currentPosition; + + if(start.originalEvent.clientX > currentEvent.clientX) { + left = currentPosition; + right = startPosition; + } + + return { + start: startPosition, + current: currentPosition, + left: left, + right: right + }; + }, + + _byBrushThreshold: function(d) { + let startEvent = d[0].originalEvent; + let 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(){ + this._super(...arguments); + + if(this._brushDisposable) { + this._brushDisposable.dispose(); + } + }, +}); diff --git a/addon/components/nf-group.js b/addon/components/nf-group.js new file mode 100644 index 0000000..bf35041 --- /dev/null +++ b/addon/components/nf-group.js @@ -0,0 +1,43 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-group'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; + +/** + A grouping tag that provides zooming and offset functionality to it's children. + + ## Example + + The following example will show a line of `someData` with a 2x zoom, offset by 30px in both x and y + directions: + + {{#nf-gg scaleZoomX="2" scaleZoomY="2" scaleOffsetX="30" scaleOffsetY="30"}} + {{nf-line data=someData}} + {{/nf-gg}} + + @namespace components + @class nf-gg + @extends Ember.Component + @uses mixins.graph-require-scale-source + @uses mixins.graph-selecteble-graphic +*/ +export default Ember.Component.extend(RequireScaleSource, SelectableGraphic, { + layout, + tagName: 'g', + + classNameBindings: [':nf-group', 'selectable', 'selected'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + click: function() { + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + } +}); diff --git a/addon/components/nf-horizontal-line.js b/addon/components/nf-horizontal-line.js new file mode 100644 index 0000000..7c60683 --- /dev/null +++ b/addon/components/nf-horizontal-line.js @@ -0,0 +1,67 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-horizontal-line'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +/** + Draws a horizontal line on the graph at a given y domain value + @namespace components + @class nf-horizontal-line + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'line', + + attributeBindings: ['lineY:y1', 'lineY:y2', 'x1', 'x2'], + + classNames: ['nf-horizontal-line'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The y domain value at which to draw the horizontal line + @property y + @type Number + @default null + */ + y: null, + + /** + The computed y coordinate of the line to draw + @property lineY + @type Number + @private + @readonly + */ + lineY: Ember.computed('y', 'yScale', function(){ + let y = this.get('y'); + let yScale = this.get('yScale'); + let py = yScale ? yScale(y) : -1; + return py && py > 0 ? py : 0; + }), + + /** + The left x coordinate of the line + @property x1 + @type Number + @default 0 + @private + */ + x1: 0, + + /** + The right x coordinate of the line + @property x2 + @type Number + @private + @readonly + */ + x2: Ember.computed.alias('graph.graphWidth'), +}); diff --git a/addon/components/nf-line.js b/addon/components/nf-line.js new file mode 100644 index 0000000..b008aed --- /dev/null +++ b/addon/components/nf-line.js @@ -0,0 +1,83 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-line'; +import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; +import LineUtils from 'ember-nf-graph/mixins/graph-line-utils'; +import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; +import RegisteredGraphic from 'ember-nf-graph/mixins/graph-registered-graphic'; +import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +/** + A line graphic for `nf-graph`. Displays a line for the data it's passed. + @namespace components + @class nf-line + @extends Ember.Component + @uses mixins.graph-line-utils + @uses mixins.graph-selectable-graphic + @uses mixins.graph-registered-graphic + @uses mixins.graph-data-graphic + @uses mixins.graph-graphic-with-tracking-dot + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(DataGraphic, SelectableGraphic, LineUtils, RegisteredGraphic, GraphicWithTrackingDot, RequireScaleSource, { + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The type of D3 interpolator to use to create the line. + @property interpolator + @type String + @default 'linear' + */ + interpolator: 'linear', + + classNameBindings: ['selected', 'selectable'], + + classNames: ['nf-line'], + + /** + The d3 line function to create the line path. + @method lineFn + @param data {Array} the array of coordinate arrays to plot as an SVG path + @private + @return {String} an SVG path data string + */ + lineFn: Ember.computed('xScale', 'yScale', 'interpolator', function() { + let xScale = this.get('xScale'); + let yScale = this.get('yScale'); + let interpolator = this.get('interpolator'); + return this.createLineFn(xScale, yScale, interpolator); + }), + + /** + The SVG path data string to render the line + @property d + @type String + @private + @readonly + */ + d: Ember.computed('renderedData.[]', 'lineFn', function(){ + let renderedData = this.get('renderedData'); + let lineFn = this.get('lineFn'); + return lineFn(renderedData); + }), + + /** + Event handler to toggle the `selected` property on click + @method _toggleSelected + @private + */ + _toggleSelected: Ember.on('click', function(){ + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + }), +}); diff --git a/addon/components/nf-plot.js b/addon/components/nf-plot.js new file mode 100644 index 0000000..a9019d4 --- /dev/null +++ b/addon/components/nf-plot.js @@ -0,0 +1,118 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-plot'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import GraphEvent from 'ember-nf-graph/utils/nf/graph-event'; + +/** + Plots a group tag on a graph at a given x and y domain coordinate. + @namespace components + @class nf-plot + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'g', + + attributeBindings: ['transform'], + + classNames: ['nf-plot'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The x domain value to set the plot at + @property x + @default null + */ + x: null, + + /** + The y domain value to set the plot at + @property x + @default null + */ + y: null, + + /** + True if an `x` value is present (defined, not null and non-empty) + @property hasX + @type Boolean + @readonly + */ + hasX: Ember.computed.notEmpty('x'), + + /** + True if an `y` value is present (defined, not null and non-empty) + @property hasY + @type Boolean + @readonly + */ + hasY: Ember.computed.notEmpty('y'), + + /** + The calculated visibility of the component + @property isVisible + @type Boolean + @readonly + */ + isVisible: Ember.computed.and('hasX', 'hasY'), + + /** + The calculated x coordinate + @property rangeX + @type Number + @readonly + */ + rangeX: Ember.computed('x', 'xScale', function(){ + let xScale = this.get('xScale'); + let x = this.get('x'); + let hasX = this.get('hasX'); + return (hasX && xScale ? xScale(x) : 0) || 0; + }), + + /** + The calculated y coordinate + @property rangeY + @type Number + @readonly + */ + rangeY: Ember.computed('y', 'yScale', function(){ + let yScale = this.get('yScale'); + let y = this.get('y'); + let hasY = this.get('hasY'); + return (hasY && yScale ? yScale(y) : 0) || 0; + }), + + /** + The SVG transform of the component's `` tag. + @property transform + @type String + @readonly + */ + transform: Ember.computed('rangeX', 'rangeY', function(){ + let rangeX = this.get('rangeX'); + let rangeY = this.get('rangeY'); + return `translate(${rangeX} ${rangeY})`; + }), + + data: null, + + click: function(e) { + let context = GraphEvent.create({ + x: this.get('x'), + y: this.get('y'), + data: this.get('data'), + source: this, + graph: this.get('graph'), + originalEvent: e, + }); + this.sendAction('action', context); + }, +}); diff --git a/addon/components/nf-plots.js b/addon/components/nf-plots.js new file mode 100644 index 0000000..8186d1d --- /dev/null +++ b/addon/components/nf-plots.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-plots'; +import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +export default Ember.Component.extend(DataGraphic, RequireScaleSource, { + layout, + tagName: 'g', + + classNames: ['nf-plots'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The model for adding plots to the graph + @property plotData + @readonly + @private + */ + plotData: Ember.computed('renderedData.[]', function(){ + let renderedData = this.get('renderedData'); + if(renderedData && Ember.isArray(renderedData)) { + return Ember.A(renderedData.map(function(d) { + return { + x: d[0], + y: d[1], + data: d.data, + }; + })); + } + }), + + + actions: { + itemClicked: function(e) { + this.sendAction('action', e); + }, + }, +}); diff --git a/addon/components/nf-range-marker.js b/addon/components/nf-range-marker.js new file mode 100644 index 0000000..59fd62d --- /dev/null +++ b/addon/components/nf-range-marker.js @@ -0,0 +1,198 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-range-marker'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +/** + Draws a rectangular strip with a templated label on an `nf-graph`. + Should always be used in conjunction with an `nf-range-markers` container component. + @namespace components + @class nf-range-marker + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'g', + + attributeBindings: ['transform'], + + classNames: ['nf-range-marker'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The parent `nf-range-markers` component. + @property container + @type {components.nf-range-markers} + @default null + */ + container: null, + + /** + The minimum domain value for the range to mark. + @property xMin + @default 0 + */ + xMin: 0, + + /** + The maximum domain value for the range to mark. + @property xMax + @default 0 + */ + xMax: 0, + + /** + The spacing above the range marker. + @property marginTop + @type Number + @default 10 + */ + marginTop: 10, + + /** + The spacing below the range marker. + @property marginBottom + @type Number + @default 3 + */ + marginBottom: 3, + + /** + The height of the range marker. + @property height + @type Number + @default 10 + */ + height: 10, + + /** + The computed x position of the range marker. + @property x + @type Number + @readonly + */ + x: Ember.computed('xMin', 'xScale', function(){ + let xScale = this.get('xScale'); + let xMin = this.get('xMin'); + return xScale(xMin); + }), + + /** + The computed width of the range marker. + @property width + @type Number + @readonly + */ + width: Ember.computed('xScale', 'xMin', 'xMax', function() { + let xScale = this.get('xScale'); + let xMax = this.get('xMax'); + let xMin = this.get('xMin'); + return xScale(xMax) - xScale(xMin); + }), + + /** + The computed y position of the range marker. + @property y + @type Number + @readonly + */ + y: Ember.computed( + 'container.orient', + 'prevMarker.bottom', + 'prevMarker.y', + 'graph.graphHeight', + 'totalHeight', + function() { + let orient = this.get('container.orient'); + let prevBottom = this.get('prevMarker.bottom'); + let prevY = this.get('prevMarker.y'); + let graphHeight = this.get('graph.graphHeight'); + let totalHeight = this.get('totalHeight'); + + prevBottom = prevBottom || 0; + + if(orient === 'bottom') { + return (prevY || graphHeight) - totalHeight; + } + + if(orient === 'top') { + return prevBottom; + } + } + ), + + /** + The computed total height of the range marker including its margins. + @property totalHeight + @type Number + @readonly + */ + totalHeight: Ember.computed('height', 'marginTop', 'marginBottom', function() { + let height = this.get('height'); + let marginTop = this.get('marginTop'); + let marginBottom = this.get('marginBottom'); + return height + marginTop + marginBottom; + }), + + /** + The computed bottom of the range marker, not including the bottom margin. + @property bottom + @type Number + @readonly + */ + bottom: Ember.computed('y', 'totalHeight', function(){ + let y = this.get('y'); + let totalHeight = this.get('totalHeight'); + return y + totalHeight; + }), + + /** + The computed SVG transform of the range marker container + @property transform + @type String + @readonly + */ + transform: Ember.computed('y', function(){ + let y = this.get('y') || 0; + return `translate(0 ${y})`; + }), + + /** + The computed SVG transform fo the range marker label container. + @property labelTransform + @type String + @readonly + */ + labelTransform: Ember.computed('x', function(){ + let x = this.get('x') || 0; + return `translate(${x} 0)`; + }), + + /** + Initialization function that registers the range marker with its parent + and populates the container property + @method _setup + @private + */ + init() { + this._super(...arguments); + let container = this.get('container'); + container.registerMarker(this); + }, + + /** + Unregisters the range marker from its parent when the range marker is destroyed. + @method _unregister + @private + */ + _unregisterMarker: Ember.on('willDestroyElement', function() { + this.get('container').unregisterMarker(this); + }) +}); diff --git a/addon/components/nf-range-markers.js b/addon/components/nf-range-markers.js new file mode 100644 index 0000000..92533b0 --- /dev/null +++ b/addon/components/nf-range-markers.js @@ -0,0 +1,94 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-range-markers'; + +/** + A container and manager for `nf-range-marker` components. + Used to draw an association between `nf-range-marker` components so they + can be laid out in a manner in which they don't collide. + @namespace components + @class nf-range-markers + @extends Ember.Component +*/ +export default Ember.Component.extend({ + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + Sets the orientation of the range markers. + + - `'bottom'` - Range markers start at the bottom and stack upward + - `'top'` - Range markers start at the top and stack downward + @property orient + @type String + @default 'bottom' + */ + orient: 'bottom', + + /** + The margin, in pixels, between the markers + @property markerMargin + @type Number + @default 10 + */ + markerMargin: 10, + + /** + The marker components registered with this container + @property markers + @type Array + @readonly + */ + markers: Ember.computed(function() { + return Ember.A(); + }), + + /** + Adds the passed marker to the `markers` list, and sets the `prevMarker` and `nextMarker` + properties on the marker component and it's neighbor. + @method registerMarker + @param marker {nf-range-marker} the range marker to register with this container + */ + registerMarker: function(marker) { + let markers = this.get('markers'); + let prevMarker = markers[markers.length - 1]; + + Ember.run.schedule('afterRender', () => { + if(prevMarker) { + marker.set('prevMarker', prevMarker); + prevMarker.set('nextMarker', marker); + } + + markers.pushObject(marker); + }); + }, + + /** + Removes the marker from the `markers` list. Also updates the `nextMarker` and `prevMarker` + properties of it's neighboring components. + @method unregisterMarker + @param marker {nf-range-marker} the range marker to remove from the `markers` list. + */ + unregisterMarker: function(marker) { + if(marker) { + Ember.run.schedule('afterRender', () => { + let next = marker.nextMarker; + let prev = marker.prevMarker; + if(prev) { + prev.set('nextMarker', next); + } + if(next) { + next.set('prevMarker', prev); + } + this.get('markers').removeObject(marker); + }); + } + }, +}); diff --git a/addon/components/nf-right-tick.js b/addon/components/nf-right-tick.js new file mode 100644 index 0000000..0f7889a --- /dev/null +++ b/addon/components/nf-right-tick.js @@ -0,0 +1,143 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-right-tick'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +/** + Draws a line and a chevron at the specified domain value + on the right side of an `nf-graph`. + + ### Tips + + - Adding `paddingRight` to `nf-graph` component will not affect `nf-right-tick`'s position. + + @namespace components + @class nf-right-tick + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'g', + + classNames: ['nf-right-tick'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The transition duration in milliseconds + @property duration + @type Number + @default 400 + */ + duration: 400, + + /** + The domain value at which to place the tick + @property value + @type Number + @default null + */ + value: null, + + /** + Sets the visibility of the component. Returns false if `y` is not + a numeric data type. + @property isVisible + @private + @readonly + */ + isVisible: Ember.computed('y', function(){ + return !isNaN(this.get('y')); + }), + + /** + The calculated y coordinate of the tick + @property y + @type Number + @readonly + */ + y: Ember.computed('value', 'yScale', 'graph.paddingTop', function() { + let value = this.get('value'); + let yScale = this.get('yScale'); + let paddingTop = this.get('graph.paddingTop'); + let vy = 0; + if(yScale) { + vy = yScale(value) || 0; + } + return vy + paddingTop; + }), + + /** + The SVG transform used to render the tick + @property transform + @type String + @private + @readonly + */ + transform: Ember.computed('y', 'graph.width', function(){ + let y = this.get('y'); + let graphWidth = this.get('graph.width'); + let x0 = graphWidth - 6; + let y0 = y - 3; + return `translate(${x0} ${y0})`; + }), + + /** + performs the D3 transition to move the tick to the proper position. + @method _transitionalUpdate + @private + */ + _transitionalUpdate: function(){ + let transform = this.get('transform'); + let path = this.get('path'); + let duration = this.get('duration'); + path.transition().duration(duration) + .attr('transform', transform); + }, + + /** + Schedules the transition when `value` changes on on init. + @method _triggerTransition + @private + */ + _triggerTransition: Ember.on('init', Ember.observer('value', function(){ + Ember.run.scheduleOnce('afterRender', this, this._transitionalUpdate); + })), + + /** + Updates the tick position without a transition. + @method _nonTransitionalUpdate + @private + */ + _nonTransitionalUpdate: function(){ + let transform = this.get('transform'); + let path = this.get('path'); + path.attr('transform', transform); + }, + + /** + Schedules the update of non-transitional positions + @method _triggerNonTransitionalUpdate + @private + */ + _triggerNonTransitionalUpdate: Ember.observer('graph.width', function(){ + Ember.run.scheduleOnce('afterRender', this, this._nonTransitionalUpdate); + }), + + /** + Gets the elements required to do the d3 transitions + @method _getElements + @private + */ + _getElements: Ember.on('didInsertElement', function(){ + let g = d3.select(this.$()[0]); + let path = g.selectAll('path').data([0]); + this.set('path', path); + }) +}); diff --git a/addon/components/nf-selection-box.js b/addon/components/nf-selection-box.js new file mode 100644 index 0000000..125ffeb --- /dev/null +++ b/addon/components/nf-selection-box.js @@ -0,0 +1,159 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-selection-box'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; + +/** + Draws a rectangle on an `nf-graph` given domain values `xMin`, `xMax`, `yMin` and `yMax`. + @namespace components + @class nf-selection-box + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The duration of the transition in ms + @property duration + @type Number + @default 400 + */ + duration: 400, + + /** + The minimum x domain value to encompass. + @property xMin + @default null + */ + xMin: null, + + /** + The maximum x domain value to encompoass. + @property xMax + @default null + */ + xMax: null, + + /** + The minimum y domain value to encompass. + @property yMin + @default null + */ + yMin: null, + + /** + The maximum y domain value to encompass + @property yMax + @default null + */ + yMax: null, + + classNames: ['nf-selection-box'], + + /** + The x pixel position of xMin + @property x0 + @type Number + */ + x0: Ember.computed('xMin', 'xScale', function(){ + return normalizeScale(this.get('xScale'), this.get('xMin')); + }), + + /** + The x pixel position of xMax + @property x1 + @type Number + */ + x1: Ember.computed('xMax', 'xScale', function(){ + return normalizeScale(this.get('xScale'), this.get('xMax')); + }), + + /** + The y pixel position of yMin + @property y0 + @type Number + */ + y0: Ember.computed('yMin', 'yScale', function(){ + return normalizeScale(this.get('yScale'), this.get('yMin')); + }), + + /** + The y pixel position of yMax + @property y1 + @type Number + */ + y1: Ember.computed('yMax', 'yScale', function(){ + return normalizeScale(this.get('yScale'), this.get('yMax')); + }), + + /** + The SVG path string for the box's rectangle. + @property rectPath + @type String + */ + rectPath: Ember.computed('x0', 'x1', 'y0', 'y1', function(){ + let x0 = this.get('x0'); + let x1 = this.get('x1'); + let y0 = this.get('y0'); + let y1 = this.get('y1'); + return `M${x0},${y0} L${x0},${y1} L${x1},${y1} L${x1},${y0} L${x0},${y0}`; + }), + + /** + Updates the position of the box with a transition + @method doUpdatePosition + */ + doUpdatePosition: function(){ + let boxRect = this.get('boxRectElement'); + let rectPath = this.get('rectPath'); + let duration = this.get('duration'); + + boxRect.transition().duration(duration) + .attr('d', rectPath); + }, + + doUpdatePositionStatic: function(){ + let boxRect = this.get('boxRectElement'); + let rectPath = this.get('rectPath'); + + boxRect.attr('d', rectPath); + }, + + /** + Schedules an update to the position of the box after render. + @method updatePosition + @private + */ + updatePosition: Ember.observer('xMin', 'xMax', 'yMin', 'yMax', function(){ + Ember.run.once(this, this.doUpdatePosition); + }), + + staticPositionChange: Ember.on('didInsertElement', Ember.observer('xScale', 'yScale', function(){ + Ember.run.once(this, this.doUpdatePositionStatic); + })), + + /** + Sets up the required d3 elements after component + is inserted into the DOM + @method didInsertElement + */ + didInsertElement: function(){ + let element = this.get('element'); + let g = d3.select(element); + let boxRect = g.append('path') + .attr('class', 'nf-selection-box-rect') + .attr('d', this.get('rectPath')); + + this.set('boxRectElement', boxRect); + }, +}); diff --git a/addon/components/nf-svg-image.js b/addon/components/nf-svg-image.js new file mode 100644 index 0000000..49925dd --- /dev/null +++ b/addon/components/nf-svg-image.js @@ -0,0 +1,155 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-svg-image'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; +import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; +import computed from 'ember-new-computed'; + +/** + An image to be displayed in a graph with that takes domain based measurements and + uses the scale of the graph. Creates an `` SVG element. + @namespace components + @class nf-svg-image + @extends Ember.Component + @uses mixins.graph-requires-scale-source + @uses mixins.graph-selectable-graphic +*/ +export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { + layout, + tagName: 'image', + + classNameBindings: [':nf-svg-image', 'selectable', 'selected'], + + attributeBindings: ['svgX:x', 'svgY:y', 'svgWidth:width', 'svgHeight:height', 'src:href'], + + click: function(){ + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + }, + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The domain x value to place the image at. + @property x + @default null + */ + x: null, + + /** + The domain y value to place the image at. + @property y + @default null + */ + y: null, + + _width: 0, + + /** + The width as a domain value. Does not handle ordinal + scales. To set a pixel value, set `svgWidth` directly. + @property width + @type Number + @default 0 + */ + width: computed({ + get() { + return this._width; + }, + set(key, value) { + return this._width = Math.max(0, +value) || 0; + } + }), + + _height: 0, + + /** + The height as a domain value. Does not + handle ordinal scales. To set a pixel value, just + set `svgHeight` directly. + @property height + @default null + */ + height: computed({ + get() { + return this._height; + }, + set(key, value) { + this._height = Math.max(0, +value) || 0; + } + }), + + /** + The image source url + @property src + @type String + */ + src: '', + + x0: computed('x', 'xScale', function(){ + return normalizeScale(this.get('xScale'), this.get('x')); + }), + + y0: computed('y', 'yScale', function(){ + return normalizeScale(this.get('yScale'), this.get('y')); + }), + + x1: computed('xScale', 'width', 'x', function(){ + let scale = this.get('xScale'); + if(scale.rangeBands) { + throw new Error('nf-image does not support ordinal scales'); + } + return normalizeScale(scale, this.get('width') + this.get('x')); + }), + + y1: computed('yScale', 'height', 'y', function(){ + let scale = this.get('yScale'); + if(scale.rangeBands) { + throw new Error('nf-image does not support ordinal scales'); + } + return normalizeScale(scale, this.get('height') + this.get('y')); + }), + + /** + The pixel value at which to plot the image. + @property svgX + @type Number + */ + svgX: computed('x0', 'x1', function(){ + return Math.min(this.get('x0'), this.get('x1')); + }), + + /** + The pixel value at which to plot the image. + @property svgY + @type Number + */ + svgY: computed('y0', 'y1', function(){ + return Math.min(this.get('y0'), this.get('y1')); + }), + + /** + The width, in pixels, of the image. + @property svgWidth + @type Number + */ + svgWidth: computed('x0', 'x1', function(){ + return Math.abs(this.get('x0') - this.get('x1')); + }), + + /** + The height, in pixels of the image. + @property svgHeight + @type Number + */ + svgHeight: computed('y0', 'y1', function(){ + return Math.abs(this.get('y0') - this.get('y1')); + }), +}); diff --git a/addon/components/nf-svg-line.js b/addon/components/nf-svg-line.js new file mode 100644 index 0000000..1ad4809 --- /dev/null +++ b/addon/components/nf-svg-line.js @@ -0,0 +1,100 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-svg-line'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; +import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; + +/** + Draws a basic line between two points on the graph. + @namespace components + @class nf-svg-line + @extends Ember.Component + @uses mixins.graph-requires-scale-source + @uses mixins.graph-selectable-graphic +*/ +export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { + layout, + tagName: 'line', + + classNameBindings: [':nf-svg-line', 'selectable', 'selected'], + + attributeBindings: ['svgX1:x1', 'svgX2:x2', 'svgY1:y1', 'svgY2:y2'], + + click: function(){ + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + }, + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The domain value to plot the SVGLineElement's x1 at. + @property x1 + @default null + */ + x1: null, + + /** + The domain value to plot the SVGLineElement's x2 at. + @property x2 + @default null + */ + x2: null, + + /** + The domain value to plot the SVGLineElement's y1 at. + @property y1 + @default null + */ + y1: null, + + /** + The domain value to plot the SVGLineElement's y2 at. + @property y2 + @default null + */ + y2: null, + + /** + The pixel value to plot the SVGLineElement's x1 at. + @property svgX1 + @type Number + */ + svgX1: Ember.computed('x1', 'xScale', function(){ + return normalizeScale(this.get('xScale'), this.get('x1')); + }), + + /** + The pixel value to plot the SVGLineElement's x2 at. + @property svgX2 + @type Number + */ + svgX2: Ember.computed('x2', 'xScale', function(){ + return normalizeScale(this.get('xScale'), this.get('x2')); + }), + + /** + The pixel value to plot the SVGLineElement's y1 at. + @property svgY1 + @type Number + */ + svgY1: Ember.computed('y1', 'yScale', function(){ + return normalizeScale(this.get('yScale'), this.get('y1')); + }), + + /** + The pixel value to plot the SVGLineElement's y2 at. + @property svgY2 + @type Number + */ + svgY2: Ember.computed('y2', 'yScale', function(){ + return normalizeScale(this.get('yScale'), this.get('y2')); + }), +}); diff --git a/addon/components/nf-svg-path.js b/addon/components/nf-svg-path.js new file mode 100644 index 0000000..46ee238 --- /dev/null +++ b/addon/components/nf-svg-path.js @@ -0,0 +1,95 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-svg-path'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; +import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; + +/** + An SVG path primitive that plots based on a graph's scale. + @namespace components + @class nf-svg-path + @extends Ember.Component + @uses mixins.graph-requires-scale-source + @uses mixins.graph-selectable-graphic +*/ +export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { + layout, + type: 'path', + + classNameBindings: [':nf-svg-path', 'selectable', 'selected'], + + attributeBindings: ['d'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The array of points to use to plot the path. This is an array of arrays, in the following format: + + // specify path pen commands + [ + [50, 50, 'L'], + [100, 100, 'L'] + ] + + // or they will default to 'L' + [ + [50, 50], + [100, 100] + ] + + @property points + @type Array + */ + points: null, + + /** + The data points mapped to scale + @property svgPoints + @type Array + */ + svgPoints: Ember.computed('points.[]', 'xScale', 'yScale', function(){ + let points = this.get('points'); + let xScale = this.get('xScale'); + let yScale = this.get('yScale'); + if(Ember.isArray(points) && points.length > 0) { + return points.map(function(v) { + let dx = normalizeScale(xScale, v[0]); + let dy = normalizeScale(yScale, v[1]); + let c = v.length > 2 ? v[2] : 'L'; + return [dx, dy, c]; + }); + } + }), + + click: function(){ + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + }, + + /** + The raw svg path d attribute output + @property d + @type String + */ + d: Ember.computed('svgPoints', function(){ + let svgPoints = this.get('svgPoints'); + if(Ember.isArray(svgPoints) && svgPoints.length > 0) { + return svgPoints.reduce(function(d, pt, i) { + if(i === 0) { + d += 'M' + pt[0] + ',' + pt[1]; + } + d += ' ' + pt[2] + pt[0] + ',' + pt[1]; + return d; + }, ''); + } else { + return 'M0,0'; + } + }), +}); diff --git a/addon/components/nf-svg-rect.js b/addon/components/nf-svg-rect.js new file mode 100644 index 0000000..cf35265 --- /dev/null +++ b/addon/components/nf-svg-rect.js @@ -0,0 +1,167 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-svg-rect'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; +import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; +import computed from 'ember-new-computed'; + +/** + A rectangle that plots using domain values from the graph. Uses an SVGPathElement + to plot the rectangle, to allow for rectangles with "negative" heights. + @namespace components + @class nf-svg-rect + @extends Ember.Component + @uses mixins.graph-requires-scale-source + @uses mixins.graph-selectable-graphic +*/ +export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { + layout, + tagName: 'path', + + attributeBindings: ['d'], + + classNameBindings: [':nf-svg-rect', 'selectable', 'selected'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The domain x value to place the rect at. + @property x + @default null + */ + x: null, + + /** + The domain y value to place the rect at. + @property y + @default null + */ + y: null, + + _width: 0, + + /** + The width as a domain value. If xScale is ordinal, + then this value is the indice offset to which to draw the + rectangle. In other words, if it's `2`, then draw the rectangle + to two ordinals past whatever `x` is set to. + @property width + @type Number + @default 0 + */ + width: computed({ + get() { + return this._width; + }, + set(key, value) { + return this._width = +value; + } + }), + + _height: 0, + + /** + The height as a domain value. If the yScale is ordinal, + this value is the indice offset to which to draw the rectangle. + For example, if the height is `3` then draw the rectangle + to two ordinals passed whatever `y` is set to. + @property height + @type Number + @default 0 + */ + height: computed({ + get() { + return this._height; + }, + set(key, value) { + return this._height = +value; + } + }), + + /** + The x value of the bottom right corner of the rectangle. + @property x1 + @type Number + */ + x1: computed('width', 'x', 'xScale', function(){ + let xScale = this.get('xScale'); + let w = this.get('width'); + let x = this.get('x'); + if(xScale.rangeBands) { + let domain = xScale.domain(); + let fromIndex = domain.indexOf(x); + let toIndex = fromIndex + w; + return normalizeScale(xScale, domain[toIndex]); + } else { + x = +x || 0; + return normalizeScale(xScale, w + x); + } + }), + + /** + The y value of the bottom right corner of the rectangle + @property y1 + @type Number + */ + y1: computed('height', 'y', 'yScale', function(){ + let yScale = this.get('yScale'); + let h = this.get('height'); + let y = this.get('y'); + if(yScale.rangeBands) { + let domain = yScale.domain(); + let fromIndex = domain.indexOf(y); + let toIndex = fromIndex + h; + return normalizeScale(yScale, domain[toIndex]); + } else { + y = +y || 0; + return normalizeScale(yScale, h + y); + } + }), + + /** + The x value of the top right corner of the rectangle + @property x0 + @type Number + */ + x0: computed('x', 'xScale', function(){ + return normalizeScale(this.get('xScale'), this.get('x')); + }), + + /** + The y value of the top right corner of the rectangle. + @property y0 + @type Number + */ + y0: computed('y', 'yScale', function() { + return normalizeScale(this.get('yScale'), this.get('y')); + }), + + /** + The SVG path data for the rectangle + @property d + @type String + */ + d: computed('x0', 'y0', 'x1', 'y1', function(){ + let x0 = this.get('x0'); + let y0 = this.get('y0'); + let x1 = this.get('x1'); + let y1 = this.get('y1'); + return `M${x0},${y0} L${x0},${y1} L${x1},${y1} L${x1},${y0} L${x0},${y0}`; + }), + + /** + Click event handler. Toggles selected if selectable. + @method click + */ + click: function(){ + if(this.get('selectable')) { + this.toggleProperty('selected'); + } + } +}); diff --git a/addon/components/nf-tick-label.js b/addon/components/nf-tick-label.js new file mode 100644 index 0000000..527fda6 --- /dev/null +++ b/addon/components/nf-tick-label.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-tick-label'; + +export default Ember.Component.extend({ + layout, + tagName: 'g', + + attributeBindings: ['transform'], + + transform: Ember.computed('x', 'y', function(){ + let x = this.get('x'); + let y = this.get('y'); + return `translate(${x} ${y})`; + }), + + className: 'nf-tick-label' +}); diff --git a/addon/components/nf-tracker.js b/addon/components/nf-tracker.js new file mode 100644 index 0000000..9a2ed4c --- /dev/null +++ b/addon/components/nf-tracker.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-tracker'; +import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; +import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; +import computed from 'ember-new-computed'; + +/** + A tracking graphic component used to do things like create tracking dots for nf-area or nf-line. + @namespace components + @class nf-tracker + @uses mixins.graph-data-graphic + @uses mixins.graph-requires-scale-source + @uses mixins.graph-graphic-with-tracking-dot + */ +export default Ember.Component.extend(DataGraphic, RequiresScaleSource, GraphicWithTrackingDot, { + layout, + tagName: 'g', + + classNameBindings: [':nf-tracker'], + + attributeBindings: ['transform'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + transform: computed('trackedData.x', 'trackedData.y', 'xScale', 'yScale', { + get() { + let xScale = this.get('xScale'); + let yScale = this.get('yScale'); + let x = xScale && xScale(this.get('trackedData.x') || 0); + let y = yScale && yScale(this.get('trackedData.y') || 0); + return 'translate(' + x + ',' + y + ')'; + } + }) +}); diff --git a/addon/components/nf-vertical-line.js b/addon/components/nf-vertical-line.js new file mode 100644 index 0000000..b547560 --- /dev/null +++ b/addon/components/nf-vertical-line.js @@ -0,0 +1,67 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-vertical-line'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; + +/** + Draws a vertical line on a graph at a given x domain value + @namespace components + @class nf-vertical-line + @extends Ember.Component + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'line', + + classNames: ['nf-vertical-line'], + + attributeBindings: ['lineX:x1', 'lineX:x2', 'y1', 'y2'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The top y coordinate of the line + @property y1 + @type Number + @default 0 + @private + */ + y1: 0, + + /** + The bottom y coordinate of the line + @property y2 + @type Number + @private + @readonly + */ + y2: Ember.computed.alias('graph.graphHeight'), + + /** + The x domain value at which to draw the vertical line on the graph + @property x + @type Number + @default null + */ + x: null, + + /** + The calculated x coordinate of the vertical line + @property lineX + @type Number + @private + @readonly + */ + lineX: Ember.computed('xScale', 'x', function(){ + let xScale = this.get('xScale'); + let x = this.get('x'); + let px = xScale ? xScale(x) : -1; + return px && px > 0 ? px : 0; + }), +}); diff --git a/addon/components/nf-x-axis.js b/addon/components/nf-x-axis.js new file mode 100644 index 0000000..2a0cf0a --- /dev/null +++ b/addon/components/nf-x-axis.js @@ -0,0 +1,300 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-x-axis'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import computed from 'ember-new-computed'; + +/** + A component for adding a templated x axis to an `nf-graph` component. + All items contained within this component are used to template each tick mark on the + rendered graph. Tick values are supplied to the inner scope of this component on the + view template via `tick`. + + ### Styling + + The main container will have a `nf-x-axis` class. + A `orient-top` or `orient-bottom` container will be applied to the container + depending on the `orient` setting. + + Ticks are positioned via a `` tag, that will contain whatever is passed into it via + templating, along with the tick line. `` tags within tick templates do have some + default styling applied to them to position them appropriately based off of orientation. + + ### Example + + {{#nf-graph width=500 height=300}} + {{#nf-x-axis height=40 as |tick|}} + x is {{tick.value}} + {{/nf-x-axis}} + {{/nf-graph}} + + + @namespace components + @class nf-x-axis + @extends Ember.Component + @uses mixins.graph-has-graph-parent + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'g', + + attributeBindings: ['transform'], + classNameBindings: ['orientClass'], + classNames: ['nf-x-axis'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The height of the x axis in pixels. + @property height + @type Number + @default 20 + */ + height: 20, + + /** + The number of ticks to display + @property tickCount + @type Number + @default 12 + */ + tickCount: 12, + + /** + The length of the tick line (the small vertical line indicating the tick) + @property tickLength + @type Number + @default 0 + */ + tickLength: 0, + + /** + The spacing between the end of the tick line and the origin of the templated + tick content + @property tickPadding + @type Number + @default 5 + */ + tickPadding: 5, + + /** + The orientation of the x axis. Value can be `'top'` or `'bottom'`. + @property orient + @type String + @default 'bottom' + */ + orient: 'bottom', + + _tickFilter: null, + + /** + An optional filtering function to allow more control over what tick marks are displayed. + The function should have exactly the same signature as the function you'd use for an + `Array.prototype.filter()`. + + @property tickFilter + @type Function + @default null + @example + + {{#nf-x-axis tickFilter=myFilter as |tick|}} + {{tick.value}} + {{/nf-x-axis}} + + And on your controller: + + myFilter: function(tick, index, ticks) { + return tick.value < 1000; + }, + + The above example will filter down the set of ticks to only those that are less than 1000. + */ + tickFilter: computed.alias('_tickFilter'), + + /** + The class applied due to orientation (e.g. `'orient-top'`) + @property orientClass + @type String + @readonly + */ + orientClass: computed('orient', function(){ + return 'orient-' + this.get('orient'); + }), + + /** + The SVG Transform applied to this component's container. + @property transform + @type String + @readonly + */ + transform: computed('x', 'y', function(){ + let x = this.get('x') || 0; + let y = this.get('y') || 0; + return `translate(${x} ${y})`; + }), + + /** + The y position of this component's container. + @property y + @type Number + @readonly + */ + y: computed( + 'orient', + 'graph.paddingTop', + 'graph.paddingBottom', + 'graph.height', + 'height', + function(){ + let orient = this.get('orient'); + let graphHeight = this.get('graph.height'); + let height = this.get('height'); + let paddingBottom = this.get('graph.paddingBottom'); + let paddingTop = this.get('graph.paddingTop'); + let y; + + if(orient === 'bottom') { + y = graphHeight - paddingBottom - height; + } else { + y = paddingTop; + } + + return y || 0; + } + ), + + /** + This x position of this component's container + @property x + @type Number + @readonly + */ + x: computed('graph.graphX', function(){ + return this.get('graph.graphX') || 0; + }), + + init() { + this._super(...arguments); + + Ember.run.schedule('afterRender', () => { + this.set('graph.xAxis', this); + }); + }, + + /** + The width of the component + @property width + @type Number + @readonly + */ + width: computed.alias('graph.graphWidth'), + + /** + A method to call to override the default behavior of how ticks are created. + + The function signature should match: + + // - scale: d3.Scale + // - tickCount: number of ticks + // - uniqueData: unique data points for the axis + // - scaleType: string of "linear" or "ordinal" + // returns: an array of tick values. + function(scale, tickCount, uniqueData, scaleType) { + return [100,200,300]; + } + + @property tickFactory + @type {Function} + @default null + */ + tickFactory: null, + + tickData: computed('xScale', 'graph.xScaleType', 'uniqueXData', 'tickCount', 'tickFactory', function(){ + let tickFactory = this.get('tickFactory'); + let scale = this.get('xScale'); + let uniqueData = this.get('uniqueXData'); + let tickCount = this.get('tickCount'); + let scaleType = this.get('graph.xScaleType'); + + if(tickFactory) { + return tickFactory(scale, tickCount, uniqueData, scaleType); + } + else if(scaleType === 'ordinal') { + return uniqueData; + } + else { + return scale.ticks(tickCount); + } + }), + + /** + A unique set of all x data on the graph + @property uniqueXData + @type Array + @readonly + */ + uniqueXData: computed.uniq('graph.xData'), + + /** + The models for the ticks to display on the axis. + @property ticks + @type Array + @readonly + */ + ticks: computed( + 'xScale', + 'tickPadding', + 'tickLength', + 'height', + 'orient', + 'tickFilter', + 'tickData', + 'graph.xScaleType', + function(){ + let xScale = this.get('xScale'); + let xScaleType = this.get('graph.xScaleType'); + let tickPadding = this.get('tickPadding'); + let tickLength = this.get('tickLength'); + let height = this.get('height'); + let orient = this.get('orient'); + let tickFilter = this.get('tickFilter'); + let ticks = this.get('tickData'); + let y1 = orient === 'top' ? height : 0; + let y2 = y1 + tickLength; + let labely = orient === 'top' ? (y1 - tickPadding) : (y1 + tickPadding); + let halfBandWidth = (xScaleType === 'ordinal') ? xScale.rangeBand() / 2 : 0; + let result = ticks.map(function(tick) { + return { + value: tick, + x: xScale(tick) + halfBandWidth, + y1: y1, + y2: y2, + labely: labely + }; + }); + + if(tickFilter) { + result = result.filter(tickFilter); + } + + return Ember.A(result); + } + ), + + /** + The y position, in pixels, of the axis line + @property axisLineY + @type Number + @readonly + */ + axisLineY: computed('orient', 'height', function(){ + return this.get('orient') === 'top' ? this.get('height') : 0; + }) + +}); diff --git a/addon/components/nf-y-axis.js b/addon/components/nf-y-axis.js new file mode 100644 index 0000000..68fb3e1 --- /dev/null +++ b/addon/components/nf-y-axis.js @@ -0,0 +1,291 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-y-axis'; +import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; +import computed from 'ember-new-computed'; + +/** + A component for adding a templated y axis to an `nf-graph` component. + All items contained within this component are used to template each tick mark on the + rendered graph. Tick values are supplied to the inner scope of this component on the + view template via `tick`. + + ### Styling + + The main container will have a `nf-y-axis` class. + A `orient-left` or `orient-right` container will be applied to the container + depending on the `orient` setting. + + Ticks are positioned via a `` tag, that will contain whatever is passed into it via + templating, along with the tick line. `` tags within tick templates do have some + default styling applied to them to position them appropriately based off of orientation. + + ### Example + + {{#nf-graph width=500 height=300}} + {{#nf-y-axis width=40 as |tick|}} + y is {{tick.value}} + {{/nf-y-axis}} + {{/nf-graph}} + + + @namespace components + @class nf-y-axis + @uses mixins.graph-has-graph-parent + @uses mixins.graph-requires-scale-source +*/ +export default Ember.Component.extend(RequireScaleSource, { + layout, + tagName: 'g', + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + The number of ticks to display + @property tickCount + @type Number + @default 5 + */ + tickCount: 5, + + /** + The length of the tick's accompanying line. + @property tickLength + @type Number + @default 5 + */ + tickLength: 5, + + /** + The distance between the tick line and the origin tick's templated output + @property tickPadding + @type Number + @default 3 + */ + tickPadding: 3, + + /** + The total width of the y axis + @property width + @type Number + @default 40 + */ + width: 40, + + /** + The orientation of the y axis. Possible values are `'left'` and `'right'` + @property orient + @type String + @default 'left' + */ + orient: 'left', + + attributeBindings: ['transform'], + + classNameBindings: [':nf-y-axis', 'isOrientRight:orient-right:orient-left'], + + _tickFilter: null, + + /** + An optional filtering function to allow more control over what tick marks are displayed. + The function should have exactly the same signature as the function you'd use for an + `Array.prototype.filter()`. + + @property tickFilter + @type Function + @default null + @example + + {{#nf-y-axis tickFilter=myFilter as |tick|}} + {{tick.value}} + {{/nf-y-axis}} + + And on your controller: + + myFilter: function(tick, index, ticks) { + return tick.value < 1000; + }, + + The above example will filter down the set of ticks to only those that are less than 1000. + */ + tickFilter: computed.alias('_tickFilter'), + + /** + computed property. returns true if `orient` is equal to `'right'`. + @property isOrientRight + @type Boolean + @readonly + */ + isOrientRight: computed.equal('orient', 'right'), + + + /** + The SVG transform for positioning the component. + @property transform + @type String + @readonly + */ + transform: computed('x', 'y', function(){ + let x = this.get('x') || 0; + let y = this.get('y') || 0; + return `translate(${x} ${y})`; + }), + + /** + The x position of the component + @property x + @type Number + @readonly + */ + x: computed( + 'orient', + 'graph.width', + 'width', + 'graph.paddingLeft', + 'graph.paddingRight', + function(){ + let orient = this.get('orient'); + if(orient !== 'left') { + return this.get('graph.width') - this.get('width') - this.get('graph.paddingRight'); + } + return this.get('graph.paddingLeft'); + } + ), + + /** + The y position of the component + @property y + @type Number + @readonly + */ + y: computed.alias('graph.graphY'), + + /** + the height of the component + @property height + @type Number + @readonly + */ + height: computed.alias('graph.graphHeight'), + + init() { + this._super(...arguments); + + Ember.run.schedule('afterRender', () => { + this.set('graph.yAxis', this); + }); + }, + + /** + A method to call to override the default behavior of how ticks are created. + + The function signature should match: + + // - scale: d3.Scale + // - tickCount: number of ticks + // - uniqueData: unique data points for the axis + // - scaleType: string of "linear" or "ordinal" + // returns: an array of tick values. + function(scale, tickCount, uniqueData, scaleType) { + return [100,200,300]; + } + + @property tickFactory + @type {Function} + @default null + */ + tickFactory: null, + + tickData: computed('graph.yScaleType', 'uniqueYData', 'yScale', 'tickCount', 'tickFactory', function(){ + let tickFactory = this.get('tickFactory'); + let scale = this.get('yScale'); + let uniqueData = this.get('uniqueYData'); + let scaleType = this.get('graph.yScaleType'); + let tickCount = this.get('tickCount'); + + if(tickFactory) { + return tickFactory(scale, tickCount, uniqueData, scaleType); + } + else if(scaleType === 'ordinal') { + return uniqueData; + } + else { + let ticks = scale.ticks(tickCount); + if (scaleType === 'log') { + let step = Math.round(ticks.length / tickCount); + ticks = ticks.filter(function (tick, i) { + return i % step === 0; + }); + } + return ticks; + } + }), + + /** + All y data from the graph, filtered to unique values. + @property uniqueYData + @type Array + @readonly + */ + uniqueYData: computed.uniq('graph.yData'), + + /** + The ticks to be displayed. + @property ticks + @type Array + @readonly + */ + ticks: computed( + 'yScale', + 'tickPadding', + 'axisLineX', + 'tickLength', + 'isOrientRight', + 'tickFilter', + 'tickData', + function() { + let yScale = this.get('yScale'); + let tickPadding = this.get('tickPadding'); + let axisLineX = this.get('axisLineX'); + let tickLength = this.get('tickLength'); + let isOrientRight = this.get('isOrientRight'); + let tickFilter = this.get('tickFilter'); + let ticks = this.get('tickData'); + let x1 = isOrientRight ? axisLineX + tickLength : axisLineX - tickLength; + let x2 = axisLineX; + let labelx = isOrientRight ? (tickLength + tickPadding) : (axisLineX - tickLength - tickPadding); + + let result = ticks.map(function (tick) { + return { + value: tick, + y: yScale(tick), + x1: x1, + x2: x2, + labelx: labelx, + }; + }); + + if(tickFilter) { + result = result.filter(tickFilter); + } + + return Ember.A(result); + } + ), + + + /** + The x position of the axis line. + @property axisLineX + @type Number + @readonly + */ + axisLineX: computed('isOrientRight', 'width', function(){ + return this.get('isOrientRight') ? 0 : this.get('width'); + }), +}); diff --git a/addon/components/nf-y-diff.js b/addon/components/nf-y-diff.js new file mode 100644 index 0000000..2e23c5a --- /dev/null +++ b/addon/components/nf-y-diff.js @@ -0,0 +1,264 @@ +import Ember from 'ember'; +import layout from 'ember-nf-graph/templates/components/nf-y-diff'; +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, `` 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(RequireScaleSource, { + layout, + tagName: 'g', + + attributeBindings: ['transform'], + + classNameBindings: [':nf-y-diff', 'isPositive:positive:negative', 'isOrientRight:orient-right:orient-left'], + + /** + The parent graph for a component. + @property graph + @type components.nf-graph + @default null + */ + graph: null, + + /** + 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(){ + let yA = +this.get('yA') || 0; + let 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 x pixel coordinate of the content container. + @property contentX + @type Number + @readonly + */ + contentX: Ember.computed('isOrientRight', 'width', 'contentPadding', function(){ + let contentPadding = this.get('contentPadding'); + let width = this.get('width'); + return this.get('isOrientRight') ? width - contentPadding : contentPadding; + }), + + rectPath: Ember.computed('yA', 'yB', 'width', function(){ + let x = 0; + let w = +this.get('width') || 0; + let x2 = x + w; + let yA = +this.get('yA') || 0; + let 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(){ + let contentX = this.get('contentX'); + let 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(){ + let element = this.get('element'); + let g = d3.select(element); + + let rectPath = this.get('rectPath'); + let rect = g.insert('path', ':first-child') + .attr('class', 'nf-y-diff-rect') + .attr('d', rectPath); + + let contentTransform = this.get('contentTransform'); + let 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(){ + let duration = this.get('duration'); + let rectElement = this.get('rectElement'); + let 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(){ + let contentElement = this.get('contentElement'); + if(contentElement) { + let contentTransform = this.get('contentTransform'); + contentElement.attr('transform', contentTransform); + } + }, + + adjustGraphHeight: Ember.on('didInsertElement', Ember.observer('graph.graphHeight', function(){ + let rectElement = this.get('rectElement'); + let 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); + }) + ), +}); diff --git a/app/templates/components/nf-area-stack.hbs b/addon/templates/components/nf-area-stack.hbs similarity index 100% rename from app/templates/components/nf-area-stack.hbs rename to addon/templates/components/nf-area-stack.hbs diff --git a/app/templates/components/nf-area.hbs b/addon/templates/components/nf-area.hbs similarity index 100% rename from app/templates/components/nf-area.hbs rename to addon/templates/components/nf-area.hbs diff --git a/app/templates/components/nf-bars-group.hbs b/addon/templates/components/nf-bars-group.hbs similarity index 100% rename from app/templates/components/nf-bars-group.hbs rename to addon/templates/components/nf-bars-group.hbs diff --git a/app/templates/components/nf-bars.hbs b/addon/templates/components/nf-bars.hbs similarity index 100% rename from app/templates/components/nf-bars.hbs rename to addon/templates/components/nf-bars.hbs diff --git a/app/templates/components/nf-brush-selection.hbs b/addon/templates/components/nf-brush-selection.hbs similarity index 100% rename from app/templates/components/nf-brush-selection.hbs rename to addon/templates/components/nf-brush-selection.hbs diff --git a/addon/templates/components/nf-crosshairs.hbs b/addon/templates/components/nf-crosshairs.hbs new file mode 100644 index 0000000..2dc341d --- /dev/null +++ b/addon/templates/components/nf-crosshairs.hbs @@ -0,0 +1,7 @@ +{{#if vertical}} + +{{/if}} + +{{#if horizontal}} + +{{/if}} diff --git a/app/templates/components/nf-tick-label.hbs b/addon/templates/components/nf-dot.hbs similarity index 100% rename from app/templates/components/nf-tick-label.hbs rename to addon/templates/components/nf-dot.hbs diff --git a/app/templates/components/nf-graph-content.hbs b/addon/templates/components/nf-graph-content.hbs similarity index 100% rename from app/templates/components/nf-graph-content.hbs rename to addon/templates/components/nf-graph-content.hbs diff --git a/app/templates/components/nf-graph-yieldables.hbs b/addon/templates/components/nf-graph-yieldables.hbs similarity index 95% rename from app/templates/components/nf-graph-yieldables.hbs rename to addon/templates/components/nf-graph-yieldables.hbs index 87b5ae3..ac326b1 100644 --- a/app/templates/components/nf-graph-yieldables.hbs +++ b/addon/templates/components/nf-graph-yieldables.hbs @@ -1,6 +1,6 @@ {{yield (hash group=(component 'nf-group' graph=graph) - crosshair=(component 'nf-crosshair' graph=graph) + crosshairs=(component 'nf-crosshairs' graph=graph) selection-box=(component 'nf-selection-box' graph=graph scaleSource=scaleSource) svg-image=(component 'nf-svg-image' graph=graph scaleSource=scaleSource) svg-line=(component 'nf-svg-line' graph=graph scaleSource=scaleSource) diff --git a/app/templates/components/nf-graph.hbs b/addon/templates/components/nf-graph.hbs similarity index 100% rename from app/templates/components/nf-graph.hbs rename to addon/templates/components/nf-graph.hbs diff --git a/app/templates/components/nf-group.hbs b/addon/templates/components/nf-group.hbs similarity index 100% rename from app/templates/components/nf-group.hbs rename to addon/templates/components/nf-group.hbs diff --git a/addon/templates/components/nf-horizontal-line.hbs b/addon/templates/components/nf-horizontal-line.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-horizontal-line.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/templates/components/nf-line.hbs b/addon/templates/components/nf-line.hbs similarity index 100% rename from app/templates/components/nf-line.hbs rename to addon/templates/components/nf-line.hbs diff --git a/addon/templates/components/nf-plot.hbs b/addon/templates/components/nf-plot.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-plot.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/templates/components/nf-plots.hbs b/addon/templates/components/nf-plots.hbs similarity index 100% rename from app/templates/components/nf-plots.hbs rename to addon/templates/components/nf-plots.hbs diff --git a/app/templates/components/nf-range-marker.hbs b/addon/templates/components/nf-range-marker.hbs similarity index 100% rename from app/templates/components/nf-range-marker.hbs rename to addon/templates/components/nf-range-marker.hbs diff --git a/app/templates/components/nf-range-markers.hbs b/addon/templates/components/nf-range-markers.hbs similarity index 100% rename from app/templates/components/nf-range-markers.hbs rename to addon/templates/components/nf-range-markers.hbs diff --git a/app/templates/components/nf-right-tick.hbs b/addon/templates/components/nf-right-tick.hbs similarity index 100% rename from app/templates/components/nf-right-tick.hbs rename to addon/templates/components/nf-right-tick.hbs diff --git a/addon/templates/components/nf-svg-image.hbs b/addon/templates/components/nf-svg-image.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-svg-image.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/addon/templates/components/nf-svg-line.hbs b/addon/templates/components/nf-svg-line.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-svg-line.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/addon/templates/components/nf-svg-path.hbs b/addon/templates/components/nf-svg-path.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-svg-path.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/addon/templates/components/nf-svg-rect.hbs b/addon/templates/components/nf-svg-rect.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-svg-rect.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/addon/templates/components/nf-tick-label.hbs b/addon/templates/components/nf-tick-label.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-tick-label.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/templates/components/nf-tracker.hbs b/addon/templates/components/nf-tracker.hbs similarity index 100% rename from app/templates/components/nf-tracker.hbs rename to addon/templates/components/nf-tracker.hbs diff --git a/addon/templates/components/nf-vertical-line.hbs b/addon/templates/components/nf-vertical-line.hbs new file mode 100644 index 0000000..889d9ee --- /dev/null +++ b/addon/templates/components/nf-vertical-line.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/templates/components/nf-x-axis.hbs b/addon/templates/components/nf-x-axis.hbs similarity index 100% rename from app/templates/components/nf-x-axis.hbs rename to addon/templates/components/nf-x-axis.hbs diff --git a/app/templates/components/nf-y-axis.hbs b/addon/templates/components/nf-y-axis.hbs similarity index 100% rename from app/templates/components/nf-y-axis.hbs rename to addon/templates/components/nf-y-axis.hbs diff --git a/app/templates/components/nf-y-diff.hbs b/addon/templates/components/nf-y-diff.hbs similarity index 100% rename from app/templates/components/nf-y-diff.hbs rename to addon/templates/components/nf-y-diff.hbs diff --git a/app/components/nf-area-stack.js b/app/components/nf-area-stack.js index 6696fec..60f0ce5 100644 --- a/app/components/nf-area-stack.js +++ b/app/components/nf-area-stack.js @@ -1,105 +1 @@ -import Ember from 'ember'; -import computed from 'ember-new-computed'; - -/** - A component for grouping and stacking `nf-area` components in an `nf-graph`. - - This component looks at the order of the `nf-area` components underneath it - and uses the ydata of the next sibling `nf-area` component to determine the bottom - of each `nf-area` components path to be drawn. - - ### Example - - {{#nf-graph width=300 height=100}} - {{#nf-graph-content}} - {{#nf-area-stack}} - {{nf-area data=myData xprop="time" yprop="high"}} - {{nf-area data=myData xprop="time" yprop="med"}} - {{nf-area data=myData xprop="time" yprop="low"}} - {{/nf-area-stack}} - {{/nf-graph-content}} - {{/nf-graph}} - - @namespace components - @class nf-area-stack -*/ -export default Ember.Component.extend({ - tagName: 'g', - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - Whether or not to add the values together to create the stacked area - @property aggregate - @type {boolean} - @default false - */ - aggregate: computed({ - get() { - Ember.warn('nf-area-stack.aggregate must be set. Currently defaulting to `false` but will default to `true` in the future.'); - return this._aggregate = false; - }, - set(key, value) { - return this._aggregate = value; - } - }), - - /** - The collection of `nf-area` components under this stack. - @property areas - @type Array - @readonly - */ - areas: computed(function(){ - return Ember.A(); - }), - - /** - Registers an area component with this stack. Also links areas to one - another by setting `nextArea` on each area component. - @method registerArea - @param area {Ember.Component} The area component to register. - */ - registerArea: function(area) { - let areas = this.get('areas'); - let prev = areas[areas.length - 1]; - - Ember.run.schedule('afterRender', () => { - if(prev) { - prev.set('nextArea', area); - area.set('prevArea', prev); - } - - areas.pushObject(area); - }); - }, - - /** - Unregisters an area component from this stack. Also updates next - and previous links. - @method unregisterArea - @param area {Ember.Component} the area to unregister - */ - unregisterArea: function(area) { - let prev = area.get('prevArea'); - let next = area.get('nextArea'); - - Ember.run.schedule('afterRender', () => { - if(next) { - next.set('prevArea', prev); - } - - if(prev) { - prev.set('nextArea', next); - } - - this.get('areas').removeObject(area); - }); - }, -}); +export { default } from 'ember-nf-graph/components/nf-area-stack'; diff --git a/app/components/nf-area.js b/app/components/nf-area.js index d36d818..5e373b1 100644 --- a/app/components/nf-area.js +++ b/app/components/nf-area.js @@ -1,157 +1 @@ -import Ember from 'ember'; -import Selectable from 'ember-nf-graph/mixins/graph-selectable-graphic'; -import RegisteredGraphic from 'ember-nf-graph/mixins/graph-registered-graphic'; -import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; -import AreaUtils from 'ember-nf-graph/mixins/graph-area-utils'; -import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import LineUtils from 'ember-nf-graph/mixins/graph-line-utils'; - -/** - Adds an area graph to an `nf-graph` component. - - Optionally, if it's located within an `nf-area-stack` component, it will work with - sibling `nf-area` components to create a stacked graph. - @namespace components - @class nf-area - @extends Ember.Component - @uses mixins.graph-area-utils - @uses mixins.graph-selectable-graphic - @uses mixins.graph-registered-graphic - @uses mixins.graph-data-graphic - @uses mixins.graph-graphic-with-tracking-dot - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RegisteredGraphic, DataGraphic, - Selectable, AreaUtils, GraphicWithTrackingDot, RequireScaleSource, LineUtils, { - - tagName: 'g', - - classNameBindings: [':nf-area', 'selected', 'selectable'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The type of d3 interpolator to use to create the area - @property interpolator - @type String - @default 'linear' - */ - interpolator: 'linear', - - /** - The previous area in the stack, if this area is part of an `nf-area-stack` - @property prevArea - @type components.nf-area - @default null - */ - prevArea: null, - - /** - The next area in the stack, if this area is part of an `nf-area-stack` - @property nextArea - @type components.nf-area - @default null - */ - nextArea: null, - - stack: null, - - init() { - this._super(...arguments); - let stack = this.get('stack'); - if(stack) { - stack.registerArea(this); - this.set('stack', stack); - } - }, - - /** - Override from `graph-data-graphic` mixin - @method getActualTrackData - */ - getActualTrackData(renderX, renderY, data) { - return { - x: this.get('xPropFn')(data), - y: this.get('yPropFn')(data) - }; - }, - - _unregisterArea: Ember.on('willDestroyElement', function(){ - let stack = this.get('stack'); - if(stack) { - stack.unregisterArea(this); - } - }), - - /** - The computed set of next y values to use for the "bottom" of the graphed area. - If the area is part of a stack, this will be the "top" of the next area in the stack, - otherwise it will return an array of values at the "bottom" of the graph domain. - @property nextYData - @type Array - @readonly - */ - nextYData: Ember.computed('data.length', 'nextArea.data.[]', function(){ - let data = this.get('data'); - if(!Array.isArray(data)) { - return []; - } - let nextData = this.get('nextArea.mappedData'); - return data.map((d, i) => (nextData && nextData[i] && nextData[i][1]) || Number.MIN_VALUE); - }), - - /** - The current rendered data "zipped" together with the nextYData. - @property mappedData - @type Array - @readonly - */ - mappedData: Ember.computed('data.[]', 'xPropFn', 'yPropFn', 'nextYData.[]', 'stack.aggregate', function() { - let { data, xPropFn, yPropFn, nextYData } = this.getProperties('data', 'xPropFn', 'yPropFn', 'nextYData'); - let aggregate = this.get('stack.aggregate'); - if(Array.isArray(data)) { - return data.map((d, i) => { - let x = xPropFn(d); - let y = yPropFn(d); - let result = aggregate ? [x, y + nextYData[i], nextYData[i]] : [x, y, nextYData[i]]; - result.data = d; - return result; - }); - } else { - return []; - } - }), - - areaFn: Ember.computed('xScale', 'yScale', 'interpolator', function(){ - let { xScale, yScale, interpolator } = this.getProperties('xScale', 'yScale', 'interpolator'); - return this.createAreaFn(xScale, yScale, interpolator); - }), - - lineFn: Ember.computed('xScale', 'yScale', 'interpolator', function(){ - let { xScale, yScale, interpolator } = this.getProperties('xScale', 'yScale', 'interpolator'); - return this.createLineFn(xScale, yScale, interpolator); - }), - - d: Ember.computed('renderedData', 'areaFn', function(){ - let renderedData = this.get('renderedData'); - return this.get('areaFn')(renderedData); - }), - - dLine: Ember.computed('renderedData', 'lineFn', function(){ - let renderedData = this.get('renderedData'); - return this.get('lineFn')(renderedData); - }), - - click: function(){ - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - } - }); +export { default } from 'ember-nf-graph/components/nf-area'; diff --git a/app/components/nf-bars-group.js b/app/components/nf-bars-group.js index 034682d..91d9ea5 100644 --- a/app/components/nf-bars-group.js +++ b/app/components/nf-bars-group.js @@ -1,73 +1 @@ -import Ember from 'ember'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -export default Ember.Component.extend(RequiresScaleSource, { - tagName: 'g', - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - groupPadding: 0.1, - - groupOuterPadding: 0, - - // either b-arses or fat, stupid hobbitses - barses: Ember.computed(function(){ - return Ember.A(); - }), - - registerBars: function(bars) { - Ember.run.schedule('afterRender', () => { - let barses = this.get('barses'); - barses.pushObject(bars); - bars.set('group', this); - bars.set('groupIndex', barses.length - 1); - }); - }, - - unregisterBars: function(bars) { - if(bars) { - Ember.run.schedule('afterRender', () => { - bars.set('group', undefined); - bars.set('groupIndex', undefined); - this.get('barses').removeObject(bars); - }); - } - }, - - groupWidth: Ember.computed('xScale', function(){ - let xScale = this.get('xScale'); - return xScale && xScale.rangeBand ? xScale.rangeBand() : NaN; - }), - - barsDomain: Ember.computed('barses.[]', function(){ - let len = this.get('barses.length') || 0; - return d3.range(len); - }), - - barScale: Ember.computed( - 'groupWidth', - 'barsDomain.[]', - 'groupPadding', - 'groupOuterPadding', - function(){ - let barsDomain = this.get('barsDomain'); - let groupWidth = this.get('groupWidth'); - let groupPadding = this.get('groupPadding'); - let groupOuterPadding = this.get('groupOuterPadding'); - return d3.scale.ordinal() - .domain(barsDomain) - .rangeBands([0, groupWidth], groupPadding, groupOuterPadding); - } - ), - - barsWidth: function() { - let scale = this.get('barScale'); - return scale && scale.rangeBand ? scale.rangeBand() : NaN; - }, -}); +export { default } from 'ember-nf-graph/components/nf-bars-group'; diff --git a/app/components/nf-bars.js b/app/components/nf-bars.js index 456d376..4f189ab 100644 --- a/app/components/nf-bars.js +++ b/app/components/nf-bars.js @@ -1,181 +1 @@ -import Ember from 'ember'; -import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; -import RegisteredGraphic from 'ember-nf-graph/mixins/graph-registered-graphic'; -import parsePropExpr from 'ember-nf-graph/utils/parse-property-expression'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; -import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; -import { getRectPath } from 'ember-nf-graph/utils/nf/svg-dom'; - -/** - Adds a bar graph to an `nf-graph` component. - - **Requires the graph has `xScaleType === 'ordinal'`*** - - ** `showTrackingDot` defaults to `false` in this component ** - - @namespace components - @class nf-bars - @extends Ember.Component - @uses mixins.graph-registered-graphic - @uses mixins.graph-data-graphic - @uses mixins.graph-requires-scale-source - @uses mixins.graph-graphic-with-tracking-dot -*/ -export default Ember.Component.extend(RegisteredGraphic, DataGraphic, RequireScaleSource, GraphicWithTrackingDot, { - tagName: 'g', - - classNames: ['nf-bars'], - - _showTrackingDot: false, - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The name of the property on each data item containing the className for the bar rectangle - @property classprop - @type String - @default 'className' - */ - classprop: 'className', - - /** - Gets the function to get the classname from each data item. - @property getBarClass - @readonly - @private - */ - getBarClass: Ember.computed('classprop', function() { - let classprop = this.get('classprop'); - return classprop ? parsePropExpr(classprop) : null; - }), - - /** - The nf-bars-group this belongs to, if any. - @property group - @type components.nf-bars-group - @default null - */ - group: null, - - /** - The index of this component within the group, if any. - @property groupIndex - @type Number - @default null - */ - groupIndex: null, - - /** - The graph content height - @property graphHeight - @type Number - @readonly - */ - graphHeight: Ember.computed.oneWay('graph.graphHeight'), - - /** - A scale provided by nf-bars-group to offset the bar rectangle output - @property barScale - @type d3.scale - @readonly - */ - barScale: Ember.computed.oneWay('group.barScale'), - - /** - The width of each bar. - @property barWidth - @type Number - @readonly - */ - barWidth: Ember.computed('xScale', 'barScale', function(){ - let barScale = this.get('barScale'); - if(barScale) { - return barScale.rangeBand(); - } - let xScale = this.get('xScale'); - return xScale && xScale.rangeBand ? xScale.rangeBand() : 0; - }), - - groupOffsetX: Ember.computed('barScale', 'groupIndex', function(){ - let barScale = this.get('barScale'); - let groupIndex = this.get('groupIndex'); - return normalizeScale(barScale, groupIndex); - }), - - /** - The bar models used to render the bars. - @property bars - @readonly - */ - bars: Ember.computed( - 'xScale', - 'yScale', - 'renderedData.[]', - 'graphHeight', - 'getBarClass', - 'barWidth', - 'groupOffsetX', - function(){ - let { renderedData, xScale, yScale, barWidth, graphHeight, getBarClass, groupOffsetX } = - this.getProperties('renderedData', 'xScale', 'yScale', 'graphHeight', 'getBarClass', 'groupOffsetX', 'barWidth'); - - let getRectPath = this._getRectPath; - - if(!xScale || !yScale || !Ember.isArray(renderedData)) { - return null; - } - - let w = barWidth; - - return Ember.A(renderedData.map(function(data) { - let className = 'nf-bars-bar' + (getBarClass ? ' ' + getBarClass(data.data) : ''); - let x = normalizeScale(xScale, data[0]) + groupOffsetX; - let y = normalizeScale(yScale, data[1]); - let h = graphHeight - y; - let path = getRectPath(x, y, w, h); - - return { path, className, data }; - })); - } - ), - - _getRectPath: getRectPath, - - /** - The name of the action to fire when a bar is clicked. - @property barClick - @type String - @default null - */ - barClick: null, - - init() { - this._super(...arguments); - let group = this.get('group'); - if(group && group.registerBars) { - group.registerBars(this); - } - }, - - actions: { - nfBarClickBar: function(dataPoint) { - if(this.get('barClick')) { - this.sendAction('barClick', { - data: dataPoint.data, - x: dataPoint[0], - y: dataPoint[1], - source: this, - graph: this.get('graph'), - }); - } - } - } - -}); +export { default } from 'ember-nf-graph/components/nf-bars'; diff --git a/app/components/nf-brush-selection.js b/app/components/nf-brush-selection.js index 21d221c..19abdb1 100644 --- a/app/components/nf-brush-selection.js +++ b/app/components/nf-brush-selection.js @@ -1,178 +1 @@ -import Ember from 'ember'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -export default Ember.Component.extend(RequiresScaleSource, { - tagName: 'g', - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - left: undefined, - - right: undefined, - - formatter: null, - - textPadding: 3, - - autoWireUp: true, - - _autoBrushHandler: function(e) { - this.set('left', Ember.get(e, 'left.x')); - this.set('right', Ember.get(e, 'right.x')); - }, - - _autoBrushEndHandler: function() { - this.set('left', undefined); - this.set('right', undefined); - }, - - _wireToGraph: function(){ - let graph = this.get('graph'); - let auto = this.get('autoWireUp'); - - if(auto) { - graph.on('didBrushStart', this, this._autoBrushHandler); - graph.on('didBrush', this, this._autoBrushHandler); - graph.on('didBrushEnd', this, this._autoBrushEndHandler); - } else { - graph.off('didBrushStart', this, this._autoBrushHandler); - graph.off('didBrush', this, this._autoBrushHandler); - graph.off('didBrushEnd', this, this._autoBrushEndHandler); - } - }, - - _autoWireUpChanged: Ember.on('didInsertElement', Ember.observer('autoWireUp', function(){ - Ember.run.scheduleOnce('afterRender', this, this._wireToGraph); - })), - - _updateLeftText: function(){ - let root = d3.select(this.element); - let g = root.select('.nf-brush-selection-left-display'); - let text = g.select('.nf-brush-selection-left-text'); - let bg = g.select('.nf-brush-selection-left-text-bg'); - - let display = this.get('leftDisplay'); - - if(!display) { - g.attr('hidden', true); - } else { - g.attr('hidden', null); - } - - text.text(display); - - let textPadding = this.get('textPadding'); - let leftX = this.get('leftX'); - let graphHeight = this.get('graphHeight'); - let bbox = text[0][0].getBBox(); - - let doublePad = textPadding * 2; - let width = bbox.width + doublePad; - let height = bbox.height + doublePad; - let x = Math.max(0, leftX - width); - let y = graphHeight - height; - - g.attr('transform', `translate(${x} ${y})`); - - text.attr('x', textPadding). - attr('y', textPadding); - - bg.attr('width', width). - attr('height', height); - }, - - _onLeftChange: Ember.on( - 'didInsertElement', - Ember.observer('left', 'graphHeight', 'textPadding', function(){ - Ember.run.scheduleOnce('afterRender', this, this._updateLeftText); - }) - ), - - _updateRightText: function(){ - let root = d3.select(this.element); - let g = root.select('.nf-brush-selection-right-display'); - let text = g.select('.nf-brush-selection-right-text'); - let bg = g.select('.nf-brush-selection-right-text-bg'); - - let display = this.get('rightDisplay'); - - if(!display) { - g.attr('hidden', true); - } else { - g.attr('hidden', null); - } - - text.text(display); - - let textPadding = this.get('textPadding'); - let rightX = this.get('rightX'); - let graphHeight = this.get('graphHeight'); - let graphWidth = this.get('graphWidth'); - let bbox = text[0][0].getBBox(); - - let doublePad = textPadding * 2; - let width = bbox.width + doublePad; - let height = bbox.height + doublePad; - let x = Math.min(graphWidth - width, rightX); - let y = graphHeight - height; - - g.attr('transform', `translate(${x} ${y})`); - - text.attr('x', textPadding). - attr('y', textPadding); - - bg.attr('width', width). - attr('height', height); - }, - - _onRightChange: Ember.on( - 'didInsertElement', - Ember.observer('right', 'graphHeight', 'graphWidth', 'textPadding', function(){ - Ember.run.scheduleOnce('afterRender', this, this._updateRightText); - }) - ), - - leftDisplay: Ember.computed('left', 'formatter', function(){ - let formatter = this.get('formatter'); - let left = this.get('left'); - return formatter ? formatter(left) : left; - }), - - rightDisplay: Ember.computed('right', 'formatter', function(){ - let formatter = this.get('formatter'); - let right = this.get('right'); - return formatter ? formatter(right) : right; - }), - - isVisible: Ember.computed('left', 'right', function(){ - let left = +this.get('left'); - let right = +this.get('right'); - return left === left && right === right; - }), - - leftX: Ember.computed('xScale', 'left', function() { - let left = this.get('left') || 0; - let scale = this.get('xScale'); - return scale ? scale(left) : 0; - }), - - rightX: Ember.computed('xScale', 'right', function() { - let right = this.get('right') || 0; - let scale = this.get('xScale'); - return scale ? scale(right) : 0; - }), - - graphWidth: Ember.computed.alias('graph.graphWidth'), - - graphHeight: Ember.computed.alias('graph.graphHeight'), - - rightWidth: Ember.computed('rightX', 'graphWidth', function() { - return Math.max(this.get('graphWidth') - this.get('rightX'), 0); - }), -}); +export { default } from 'ember-nf-graph/components/nf-brush-selection'; diff --git a/app/components/nf-crosshairs.js b/app/components/nf-crosshairs.js new file mode 100644 index 0000000..00ef8de --- /dev/null +++ b/app/components/nf-crosshairs.js @@ -0,0 +1 @@ +export { default } from 'ember-nf-graph/components/nf-crosshairs'; diff --git a/app/components/nf-dot.js b/app/components/nf-dot.js index 2743ab0..f22ed44 100644 --- a/app/components/nf-dot.js +++ b/app/components/nf-dot.js @@ -1,89 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -/** - Plots a circle at a given x and y domain value on an `nf-graph`. - - @namespace components - @class nf-dot - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'circle', - - attributeBindings: ['r', 'cy', 'cx'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The x domain value at which to plot the circle - @property x - @type Number - @default null - */ - x: null, - - /** - The y domain value at which to plot the circle - @property x - @type Number - @default null - */ - y: null, - - /** - The radius of the circle plotted - @property r - @type Number - @default 2.5 - */ - r: 2.5, - - hasX: Ember.computed.notEmpty('x'), - - hasY: Ember.computed.notEmpty('y'), - - /** - The computed center x coordinate of the circle - @property cx - @type Number - @private - @readonly - */ - cx: Ember.computed('x', 'xScale', 'hasX', function(){ - let x = this.get('x'); - let xScale = this.get('xScale'); - let hasX = this.get('hasX'); - return hasX && xScale ? xScale(x) : -1; - }), - - /** - The computed center y coordinate of the circle - @property cy - @type Number - @private - @readonly - */ - cy: Ember.computed('y', 'yScale', 'hasY', function() { - let y = this.get('y'); - let yScale = this.get('yScale'); - let hasY = this.get('hasY'); - return hasY && yScale ? yScale(y) : -1; - }), - - /** - Toggles the visibility of the dot. If x or y are - not numbers, will return false. - @property isVisible - @private - @readonly - */ - isVisible: Ember.computed.and('hasX', 'hasY'), -}); +export { default } from 'ember-nf-graph/components/nf-dot'; diff --git a/app/components/nf-graph-content.js b/app/components/nf-graph-content.js index 20a3bee..0383846 100644 --- a/app/components/nf-graph-content.js +++ b/app/components/nf-graph-content.js @@ -1,174 +1 @@ -import Ember from 'ember'; -import GraphMouseEvent from 'ember-nf-graph/utils/nf/graph-mouse-event'; - -/** - Container component for graphics to display in `nf-graph`. Represents - the area where the graphics, such as lines will display. - - Exists for layout purposes. - @namespace components - @class nf-graph-content -*/ -export default Ember.Component.extend({ - tagName: 'g', - - classNames: ['nf-graph-content'], - - attributeBindings: ['transform', 'clip-path'], - - 'clip-path': Ember.computed('graph.contentClipPathId', function(){ - let clipPathId = this.get('graph.contentClipPathId'); - return `url('#${clipPathId}')`; - }), - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The SVG transform for positioning the graph content - @property transform - @type String - @readonly - */ - transform: Ember.computed('x', 'y', function(){ - let x = this.get('x'); - let y = this.get('y'); - return `translate(${x} ${y})`; - }), - - /** - The x position of the graph content - @property x - @type Number - @readonly - */ - x: Ember.computed.alias('graph.graphX'), - - /** - The calculated y position of the graph content - @property y - @type Number - @readonly - */ - y: Ember.computed.alias('graph.graphY'), - - /** - The calculated width of the graph content - @property width - @type Number - @readonly - */ - width: Ember.computed.alias('graph.graphWidth'), - - /** - The calculated height of the graph content. - @property height - @type Number - @readonly - */ - height: Ember.computed.alias('graph.graphHeight'), - - - /** - An array containing models to render the grid lanes - @property gridLanes - @type Array - @readonly - */ - gridLanes: Ember.computed('graph.yAxis.ticks', 'width', 'height', function () { - let ticks = this.get('graph.yAxis.ticks'); - let width = this.get('width'); - let height = this.get('height'); - - if(!ticks || ticks.length === 0) { - return Ember.A(); - } - - let sorted = ticks.slice().sort(function(a, b) { - return a.y - b.y; - }); - - if(sorted[0].y !== 0) { - sorted.unshift({ y: 0 }); - } - - let lanes = sorted.reduce(function(lanes, tick, i) { - let y = tick.y; - let next = sorted[i+1] || { y: height }; - let h = Math.max(next.y - tick.y, 0); - lanes.push({ - x: 0, - y: y, - width: width, - height: h - }); - return lanes; - }, []); - - return Ember.A(lanes); - }), - - /** - The name of the hoverChange action to fire - @property hoverChange - @type String - @default null - */ - hoverChange: null, - - mouseMove: function(e) { - let context = GraphMouseEvent.create({ - originalEvent: e, - source: this, - graphContentElement: this.element, - }); - - this.trigger('didHoverChange', context); - - if(this.get('hoverChange')) { - this.sendAction('hoverChange', context); - } - }, - - /** - The name of the hoverEnd action to fire - @property hoverEnd - @type String - @default null - */ - hoverEnd: null, - - mouseLeave: function(e) { - let context = GraphMouseEvent.create({ - originalEvent: e, - source: this, - graphContentElement: this.element - }); - this.trigger('didHoverEnd', context); - - if(this.get('hoverEnd')) { - this.sendAction('hoverEnd', context); - } - }, - - /** - An array containing models to render fret lines - @property frets - @type Array - @readonly - */ - frets: Ember.computed.alias('graph.xAxis.ticks'), - - init(){ - this._super(...arguments); - - Ember.run.schedule('afterRender', () => { - this.set('graph.content', this); - }); - }, -}); +export { default } from 'ember-nf-graph/components/nf-graph-content'; diff --git a/app/components/nf-graph-yieldables.js b/app/components/nf-graph-yieldables.js index c161a09..a022c8f 100644 --- a/app/components/nf-graph-yieldables.js +++ b/app/components/nf-graph-yieldables.js @@ -1,20 +1 @@ -import Ember from 'ember'; - -export default Ember.Component.extend({ - tagName: '', - - /** - The parent graph for the components. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The scale source for the components - @property scaleSource - @default null - */ - scaleSource: null, -}); +export { default } from 'ember-nf-graph/components/nf-graph-yieldables'; diff --git a/app/components/nf-graph.js b/app/components/nf-graph.js index 3111f86..0ec9a7b 100644 --- a/app/components/nf-graph.js +++ b/app/components/nf-graph.js @@ -1,1272 +1 @@ -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'; -import computed from 'ember-new-computed'; - -let Observable = Rx.Observable; - -let computedBool = computed.bool; - -const { - isPresent, - observer -} = Ember; - -let minProperty = function(axis, defaultTickCount){ - let _DataExtent_ = axis + 'DataExtent'; - let _MinMode_ = axis + 'MinMode'; - let _Axis_tickCount_ = axis + 'Axis.tickCount'; - let _ScaleFactory_ = axis + 'ScaleFactory'; - let __Min_ = '_' + axis + 'Min'; - let _prop_ = axis + 'Min'; - let _autoScaleEvent_ = 'didAutoUpdateMin' + axis.toUpperCase(); - - return computed( - _MinMode_, - _DataExtent_, - _Axis_tickCount_, - _ScaleFactory_, - 'graphHeight', - 'graphWidth', - { - get() { - let mode = this.get(_MinMode_); - let ext; - - let change = val => { - this.set(_prop_, val); - this.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') { - let extent = this.get(_DataExtent_); - ext = extent[0]; - - if(!isNaN(ext) && ext < this[__Min_]) { - let tickCount = this.get(_Axis_tickCount_) || defaultTickCount; - let newDomain = this.get(_ScaleFactory_)().domain(extent).nice(tickCount).domain(); - change(newDomain[0]); - } - } - - return this[__Min_]; - }, - set(key, value) { - if (isPresent(value) && !isNaN(value)) { - this[__Min_] = value; - } - return this[__Min_]; - } - } - ); -}; - -let maxProperty = function(axis, defaultTickCount) { - let _DataExtent_ = axis + 'DataExtent'; - let _Axis_tickCount_ = axis + 'Axis.tickCount'; - let _ScaleFactory_ = axis + 'ScaleFactory'; - let _MaxMode_ = axis + 'MaxMode'; - let __Max_ = '_' + axis + 'Max'; - let _prop_ = axis + 'Max'; - let _autoScaleEvent_ = 'didAutoUpdateMax' + axis.toUpperCase(); - - return computed( - _MaxMode_, - _DataExtent_, - _ScaleFactory_, - _Axis_tickCount_, - 'graphHeight', - 'graphWidth', - { - get() { - let mode = this.get(_MaxMode_); - let ext; - - let change = val => { - this.set(_prop_, val); - this.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') { - let extent = this.get(_DataExtent_); - ext = extent[1]; - - if(!isNaN(ext) && this[__Max_] < ext) { - let tickCount = this.get(_Axis_tickCount_) || defaultTickCount; - let newDomain = this.get(_ScaleFactory_)().domain(extent).nice(tickCount).domain(); - change(newDomain[1]); - } - } - - return this[__Max_]; - }, - set(key, value) { - if (isPresent(value) && !isNaN(value)) { - this[__Max_] = value; - } - 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|}} - {{tick.value}} - {{/nf-x-axis}} - - {{#nf-y-axis width="120" as |tick|}} - {{tick.value}} - {{/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, - - /** - @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: computed('graphics.@each.data', function(){ - let 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.scheduleOnce('afterRender', this, this._sendAutoUpdateXAction); - }, - - /** - Event handler that is fired for the `didAutoUpdateMinX` event - @method didAutoUpdateMinX - */ - didAutoUpdateMinX() { - Ember.run.scheduleOnce('afterRender', this, this._sendAutoUpdateXAction); - }, - - /** - Event handler that is fired for the `didAutoUpdateMaxY` event - @method didAutoUpdateMaxY - */ - didAutoUpdateMaxY() { - Ember.run.scheduleOnce('afterRender', this, this._sendAutoUpdateYAction); - }, - - /** - Event handler that is fired for the `didAutoUpdateMinY` event - @method didAutoUpdateMinY - */ - didAutoUpdateMinY() { - Ember.run.scheduleOnce('afterRender', 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: computed('dataExtents', function(){ - let { 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: computed('dataExtents', function(){ - let { yMin, yMax } = this.get('dataExtents'); - return [yMin, yMax]; - }), - - /** - @property xUniqueData - @type Array - @readonly - */ - xUniqueData: computed('graphics.@each.mappedData', function(){ - let graphics = this.get('graphics'); - let 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: computed('graphics.@each.mappedData', function(){ - let graphics = this.get('graphics'); - let 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: 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: computed(function(){ - return Ember.A(); - }), - - /** - An array of "selectable" graphics that have been selected within this graph. - @property selected - @type Array - @readonly - */ - selected: Ember.computed(function() { - return this.get('selectMultiple') ? Ember.A() : 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'), - xScaleFactory: Ember.computed(function() { - return this._scaleFactoryFor('x'); - }), - _scheduleXScaleFactory: observer('xScaleType', 'xPowerExponent', function() { - Ember.run.schedule('afterRender', () => { - this.set('xScaleFactory', this._scaleFactoryFor('x')); - }); - }), - - /** - Gets a function to create the yScale - @property yScaleFactory - @readonly - */ - // yScaleFactory: scaleFactoryProperty('y'), - yScaleFactory: Ember.computed(function() { - return this._scaleFactoryFor('y'); - }), - _scheduleYScaleFactory: observer('yScaleType', 'yPowerExponent', function() { - Ember.run.schedule('afterRender', () => { - this.set('yScaleFactory', this._scaleFactoryFor('y')); - }); - }), - - _scaleFactoryFor(axis) { - let type = this.get(`${axis}ScaleType`); - let powExp = this.get(`${axis}PowerExponent`); - - type = typeof type === 'string' ? type.toLowerCase() : ''; - - if(type === 'linear') { - return d3.scale.linear; - } - - else if(type === 'ordinal') { - return function(){ - let scale = d3.scale.ordinal(); - // ordinal scales don't have an invert function, so we need to add one - scale.invert = function(rv) { - let [min, max] = d3.extent(scale.range()); - let domain = scale.domain(); - let i = Math.round((domain.length - 1) * (rv - min) / (max - min)); - return domain[i]; - }; - return scale; - }; - } - - 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; - } - }, - - /** - Gets the domain of x values. - @property xDomain - @type Array - @readonly - */ - xDomain: Ember.computed(function() { - return this._domainFor('x'); - }), - _scheduleXDomain: observer( - 'xUniqueData.[]', - 'xMin', - 'xMax', - 'xScaleType', - 'xLogMin', - function() { - Ember.run.schedule('afterRender', () => { - this.set('xDomain', this._domainFor('x')); - }); - } - ), - - /** - Gets the domain of y values. - @property yDomain - @type Array - @readonly - */ - yDomain: Ember.computed(function() { - return this._domainFor('y'); - }), - _scheduleYDomain: observer( - 'yUniqueData.[]', - 'yMin', - 'yMax', - 'yScaleType', - 'yLogMin', - function() { - Ember.run.schedule('afterRender', () => { - this.set('yDomain', this._domainFor('y')); - }); - } - ), - - _domainFor(axis) { - let data = this.get(`${axis}UniqueData`); - let min = this.get(`${axis}Min`); - let max = this.get(`${axis}Max`); - let scaleType = this.get(`${axis}ScaleType`); - let logMin = this.get(`${axis}LogMin`); - let domain = null; - - if(scaleType === 'ordinal') { - domain = data; - } else { - let extent = [min, max]; - - if(scaleType === 'log') { - if (extent[0] <= 0) { - extent[0] = logMin; - } - if (extent[1] <= 0) { - extent[1] = logMin; - } - } - - domain = extent; - } - - return domain; - }, - - /** - Gets the current xScale used to draw the graph. - @property xScale - @type Function - @readonly - */ - xScale: Ember.computed(function() { - return this._scaleFor('x'); - }), - _scheduleXScale: observer( - 'xScaleFactory', - 'xRange', - 'xDomain', - 'xScaleType', - 'xOrdinalPadding', - 'xOrdinalOuterPadding', - function() { - Ember.run.schedule('afterRender', () => { - this.set('xScale', this._scaleFor('x')); - }); - } - ), - - /** - Gets the current yScale used to draw the graph. - @property yScale - @type Function - @readonly - */ - yScale: Ember.computed(function() { - return this._scaleFor('y'); - }), - _scheduleYScale: observer( - 'yScaleFactory', - 'yRange', - 'yDomain', - 'yScaleType', - 'yOrdinalPadding', - 'yOrdinalOuterPadding', - function() { - Ember.run.schedule('afterRender', () => { - this.set('yScale', this._scaleFor('y')); - }); - } - ), - - _scaleFor(axis) { - let scaleFactory = this.get(`${axis}ScaleFactory`); - let range = this.get(`${axis}Range`); - let domain = this.get(`${axis}Domain`); - let scaleType = this.get(`${axis}ScaleType`); - let ordinalPadding = this.get(`${axis}OrdinalPadding`); - let ordinalOuterPadding = this.get(`${axis}OrdinalOuterPadding`); - - let scale = scaleFactory(); - - if(scaleType === 'ordinal') { - scale = scale.domain(domain).rangeBands(range, ordinalPadding, ordinalOuterPadding); - } else { - scale = scale.domain(domain).range(range).clamp(true); - } - - return scale; - }, - - /** - 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) { - Ember.run.schedule('afterRender', () => { - let graphics = this.get('graphics'); - graphic.on('hasData', this, this.updateExtents); - graphics.pushObject(graphic); - }); - }, - - /** - 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) { - Ember.run.schedule('afterRender', () => { - let graphics = this.get('graphics'); - graphic.off('hasData', this, this.updateExtents); - 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: 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: 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: computed.notEmpty('graphics'), - - /** - The x coordinate position of the graph content - @property graphX - @type Number - @readonly - */ - graphX: computed('paddingLeft', 'yAxis.width', 'yAxis.orient', function() { - let paddingLeft = this.get('paddingLeft'); - let yAxisWidth = this.get('yAxis.width') || 0; - let 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: computed('paddingTop', 'xAxis.orient', 'xAxis.height', function(){ - let paddingTop = this.get('paddingTop'); - let xAxisOrient = this.get('xAxis.orient'); - if(xAxisOrient === 'top') { - let 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: computed('width', 'paddingRight', 'paddingLeft', 'yAxis.width', function() { - let paddingRight = this.get('paddingRight') || 0; - let paddingLeft = this.get('paddingLeft') || 0; - let yAxisWidth = this.get('yAxis.width') || 0; - let 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: computed('height', 'paddingTop', 'paddingBottom', 'xAxis.height', function(){ - let paddingTop = this.get('paddingTop') || 0; - let paddingBottom = this.get('paddingBottom') || 0; - let xAxisHeight = this.get('xAxis.height') || 0; - let 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: computed('graphX', 'graphY', function(){ - let graphX = this.get('graphX'); - let graphY = this.get('graphY'); - return `translate(${graphX} ${graphY})`; - }), - - /** - Sets `hasRendered` to `true` on `willInsertElement`. - @method _notifyHasRendered - @private - */ - _notifyHasRendered: Ember.on('willInsertElement', function () { - Ember.run.schedule('afterRender', () => { - 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) { - let svg = container.ownerSVGElement || container; - if (svg.createSVGPoint) { - let point = svg.createSVGPoint(); - point.x = e.clientX; - point.y = e.clientY; - point = point.matrixTransform(container.getScreenCTM().inverse()); - return [ point.x, point.y ]; - } - let rect = container.getBoundingClientRect(); - return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; - }, - - /** - 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 { - let 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 { - let current = this.get('selected'); - if(current && current === graphic) { - this.set('selected', 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(){ - let content = this.$('.nf-graph-content'); - - let mouseMoves = Observable.fromEvent(content, 'mousemove'); - let mouseDowns = Observable.fromEvent(content, 'mousedown'); - let mouseUps = Observable.fromEvent(Ember.$(document), 'mouseup'); - let 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) { - let start = d[0]; - let currentEvent = d[1]; - let currentPoint = getMousePoint(currentEvent.currentTarget, d[1]); - - let startPosition = GraphPosition.create({ - originalEvent: start.originalEvent, - graph: this, - graphX: start.mousePoint.x, - graphY: start.mousePoint.y - }); - - let currentPosition = GraphPosition.create({ - originalEvent: currentEvent, - graph: this, - graphX: currentPoint.x, - graphY: currentPoint.y - }); - - let left = startPosition; - let right = currentPosition; - - if(start.originalEvent.clientX > currentEvent.clientX) { - left = currentPosition; - right = startPosition; - } - - return { - start: startPosition, - current: currentPosition, - left: left, - right: right - }; - }, - - _byBrushThreshold: function(d) { - let startEvent = d[0].originalEvent; - let 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(){ - this._super(...arguments); - - if(this._brushDisposable) { - this._brushDisposable.dispose(); - } - }, -}); +export { default } from 'ember-nf-graph/components/nf-graph'; diff --git a/app/components/nf-group.js b/app/components/nf-group.js index a681090..85d8e10 100644 --- a/app/components/nf-group.js +++ b/app/components/nf-group.js @@ -1,41 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; - -/** - A grouping tag that provides zooming and offset functionality to it's children. - - ## Example - - The following example will show a line of `someData` with a 2x zoom, offset by 30px in both x and y - directions: - - {{#nf-gg scaleZoomX="2" scaleZoomY="2" scaleOffsetX="30" scaleOffsetY="30"}} - {{nf-line data=someData}} - {{/nf-gg}} - - @namespace components - @class nf-gg - @extends Ember.Component - @uses mixins.graph-require-scale-source - @uses mixins.graph-selecteble-graphic -*/ -export default Ember.Component.extend(RequireScaleSource, SelectableGraphic, { - tagName: 'g', - - classNameBindings: [':nf-group', 'selectable', 'selected'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - click: function() { - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - } -}); +export { default } from 'ember-nf-graph/components/nf-group'; diff --git a/app/components/nf-horizontal-line.js b/app/components/nf-horizontal-line.js index 08ba2da..a020d0d 100644 --- a/app/components/nf-horizontal-line.js +++ b/app/components/nf-horizontal-line.js @@ -1,65 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -/** - Draws a horizontal line on the graph at a given y domain value - @namespace components - @class nf-horizontal-line - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'line', - - attributeBindings: ['lineY:y1', 'lineY:y2', 'x1', 'x2'], - - classNames: ['nf-horizontal-line'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The y domain value at which to draw the horizontal line - @property y - @type Number - @default null - */ - y: null, - - /** - The computed y coordinate of the line to draw - @property lineY - @type Number - @private - @readonly - */ - lineY: Ember.computed('y', 'yScale', function(){ - let y = this.get('y'); - let yScale = this.get('yScale'); - let py = yScale ? yScale(y) : -1; - return py && py > 0 ? py : 0; - }), - - /** - The left x coordinate of the line - @property x1 - @type Number - @default 0 - @private - */ - x1: 0, - - /** - The right x coordinate of the line - @property x2 - @type Number - @private - @readonly - */ - x2: Ember.computed.alias('graph.graphWidth'), -}); +export { default } from 'ember-nf-graph/components/nf-horizontal-line'; diff --git a/app/components/nf-line.js b/app/components/nf-line.js index 2688fa3..9ff2cb0 100644 --- a/app/components/nf-line.js +++ b/app/components/nf-line.js @@ -1,83 +1 @@ -import Ember from 'ember'; -import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; -import LineUtils from 'ember-nf-graph/mixins/graph-line-utils'; -import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; -import RegisteredGraphic from 'ember-nf-graph/mixins/graph-registered-graphic'; -import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -/** - A line graphic for `nf-graph`. Displays a line for the data it's passed. - @namespace components - @class nf-line - @extends Ember.Component - @uses mixins.graph-line-utils - @uses mixins.graph-selectable-graphic - @uses mixins.graph-registered-graphic - @uses mixins.graph-data-graphic - @uses mixins.graph-graphic-with-tracking-dot - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(DataGraphic, SelectableGraphic, - LineUtils, RegisteredGraphic, GraphicWithTrackingDot, RequireScaleSource, { - - tagName: 'g', - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The type of D3 interpolator to use to create the line. - @property interpolator - @type String - @default 'linear' - */ - interpolator: 'linear', - - classNameBindings: ['selected', 'selectable'], - - classNames: ['nf-line'], - - /** - The d3 line function to create the line path. - @method lineFn - @param data {Array} the array of coordinate arrays to plot as an SVG path - @private - @return {String} an SVG path data string - */ - lineFn: Ember.computed('xScale', 'yScale', 'interpolator', function() { - let xScale = this.get('xScale'); - let yScale = this.get('yScale'); - let interpolator = this.get('interpolator'); - return this.createLineFn(xScale, yScale, interpolator); - }), - - /** - The SVG path data string to render the line - @property d - @type String - @private - @readonly - */ - d: Ember.computed('renderedData.[]', 'lineFn', function(){ - let renderedData = this.get('renderedData'); - let lineFn = this.get('lineFn'); - return lineFn(renderedData); - }), - - /** - Event handler to toggle the `selected` property on click - @method _toggleSelected - @private - */ - _toggleSelected: Ember.on('click', function(){ - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - }), -}); +export { default } from 'ember-nf-graph/components/nf-line'; diff --git a/app/components/nf-plot.js b/app/components/nf-plot.js index feb42c5..b08d246 100644 --- a/app/components/nf-plot.js +++ b/app/components/nf-plot.js @@ -1,116 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import GraphEvent from 'ember-nf-graph/utils/nf/graph-event'; - -/** - Plots a group tag on a graph at a given x and y domain coordinate. - @namespace components - @class nf-plot - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'g', - - attributeBindings: ['transform'], - - classNames: ['nf-plot'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The x domain value to set the plot at - @property x - @default null - */ - x: null, - - /** - The y domain value to set the plot at - @property x - @default null - */ - y: null, - - /** - True if an `x` value is present (defined, not null and non-empty) - @property hasX - @type Boolean - @readonly - */ - hasX: Ember.computed.notEmpty('x'), - - /** - True if an `y` value is present (defined, not null and non-empty) - @property hasY - @type Boolean - @readonly - */ - hasY: Ember.computed.notEmpty('y'), - - /** - The calculated visibility of the component - @property isVisible - @type Boolean - @readonly - */ - isVisible: Ember.computed.and('hasX', 'hasY'), - - /** - The calculated x coordinate - @property rangeX - @type Number - @readonly - */ - rangeX: Ember.computed('x', 'xScale', function(){ - let xScale = this.get('xScale'); - let x = this.get('x'); - let hasX = this.get('hasX'); - return (hasX && xScale ? xScale(x) : 0) || 0; - }), - - /** - The calculated y coordinate - @property rangeY - @type Number - @readonly - */ - rangeY: Ember.computed('y', 'yScale', function(){ - let yScale = this.get('yScale'); - let y = this.get('y'); - let hasY = this.get('hasY'); - return (hasY && yScale ? yScale(y) : 0) || 0; - }), - - /** - The SVG transform of the component's `` tag. - @property transform - @type String - @readonly - */ - transform: Ember.computed('rangeX', 'rangeY', function(){ - let rangeX = this.get('rangeX'); - let rangeY = this.get('rangeY'); - return `translate(${rangeX} ${rangeY})`; - }), - - data: null, - - click: function(e) { - let context = GraphEvent.create({ - x: this.get('x'), - y: this.get('y'), - data: this.get('data'), - source: this, - graph: this.get('graph'), - originalEvent: e, - }); - this.sendAction('action', context); - }, -}); +export { default } from 'ember-nf-graph/components/nf-plot'; diff --git a/app/components/nf-plots.js b/app/components/nf-plots.js index 96666d6..c4f59d2 100644 --- a/app/components/nf-plots.js +++ b/app/components/nf-plots.js @@ -1,43 +1 @@ -import Ember from 'ember'; -import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -export default Ember.Component.extend(DataGraphic, RequireScaleSource, { - tagName: 'g', - - classNames: ['nf-plots'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The model for adding plots to the graph - @property plotData - @readonly - @private - */ - plotData: Ember.computed('renderedData.[]', function(){ - let renderedData = this.get('renderedData'); - if(renderedData && Ember.isArray(renderedData)) { - return Ember.A(renderedData.map(function(d) { - return { - x: d[0], - y: d[1], - data: d.data, - }; - })); - } - }), - - - actions: { - itemClicked: function(e) { - this.sendAction('action', e); - }, - }, -}); +export { default } from 'ember-nf-graph/components/nf-plots'; diff --git a/app/components/nf-range-marker.js b/app/components/nf-range-marker.js index d3028bf..bf6f536 100644 --- a/app/components/nf-range-marker.js +++ b/app/components/nf-range-marker.js @@ -1,196 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -/** - Draws a rectangular strip with a templated label on an `nf-graph`. - Should always be used in conjunction with an `nf-range-markers` container component. - @namespace components - @class nf-range-marker - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'g', - - attributeBindings: ['transform'], - - classNames: ['nf-range-marker'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The parent `nf-range-markers` component. - @property container - @type {components.nf-range-markers} - @default null - */ - container: null, - - /** - The minimum domain value for the range to mark. - @property xMin - @default 0 - */ - xMin: 0, - - /** - The maximum domain value for the range to mark. - @property xMax - @default 0 - */ - xMax: 0, - - /** - The spacing above the range marker. - @property marginTop - @type Number - @default 10 - */ - marginTop: 10, - - /** - The spacing below the range marker. - @property marginBottom - @type Number - @default 3 - */ - marginBottom: 3, - - /** - The height of the range marker. - @property height - @type Number - @default 10 - */ - height: 10, - - /** - The computed x position of the range marker. - @property x - @type Number - @readonly - */ - x: Ember.computed('xMin', 'xScale', function(){ - let xScale = this.get('xScale'); - let xMin = this.get('xMin'); - return xScale(xMin); - }), - - /** - The computed width of the range marker. - @property width - @type Number - @readonly - */ - width: Ember.computed('xScale', 'xMin', 'xMax', function() { - let xScale = this.get('xScale'); - let xMax = this.get('xMax'); - let xMin = this.get('xMin'); - return xScale(xMax) - xScale(xMin); - }), - - /** - The computed y position of the range marker. - @property y - @type Number - @readonly - */ - y: Ember.computed( - 'container.orient', - 'prevMarker.bottom', - 'prevMarker.y', - 'graph.graphHeight', - 'totalHeight', - function() { - let orient = this.get('container.orient'); - let prevBottom = this.get('prevMarker.bottom'); - let prevY = this.get('prevMarker.y'); - let graphHeight = this.get('graph.graphHeight'); - let totalHeight = this.get('totalHeight'); - - prevBottom = prevBottom || 0; - - if(orient === 'bottom') { - return (prevY || graphHeight) - totalHeight; - } - - if(orient === 'top') { - return prevBottom; - } - } - ), - - /** - The computed total height of the range marker including its margins. - @property totalHeight - @type Number - @readonly - */ - totalHeight: Ember.computed('height', 'marginTop', 'marginBottom', function() { - let height = this.get('height'); - let marginTop = this.get('marginTop'); - let marginBottom = this.get('marginBottom'); - return height + marginTop + marginBottom; - }), - - /** - The computed bottom of the range marker, not including the bottom margin. - @property bottom - @type Number - @readonly - */ - bottom: Ember.computed('y', 'totalHeight', function(){ - let y = this.get('y'); - let totalHeight = this.get('totalHeight'); - return y + totalHeight; - }), - - /** - The computed SVG transform of the range marker container - @property transform - @type String - @readonly - */ - transform: Ember.computed('y', function(){ - let y = this.get('y') || 0; - return `translate(0 ${y})`; - }), - - /** - The computed SVG transform fo the range marker label container. - @property labelTransform - @type String - @readonly - */ - labelTransform: Ember.computed('x', function(){ - let x = this.get('x') || 0; - return `translate(${x} 0)`; - }), - - /** - Initialization function that registers the range marker with its parent - and populates the container property - @method _setup - @private - */ - init() { - this._super(...arguments); - let container = this.get('container'); - container.registerMarker(this); - }, - - /** - Unregisters the range marker from its parent when the range marker is destroyed. - @method _unregister - @private - */ - _unregisterMarker: Ember.on('willDestroyElement', function() { - this.get('container').unregisterMarker(this); - }) -}); +export { default } from 'ember-nf-graph/components/nf-range-marker'; diff --git a/app/components/nf-range-markers.js b/app/components/nf-range-markers.js index f3cbb71..ccfbc9e 100644 --- a/app/components/nf-range-markers.js +++ b/app/components/nf-range-markers.js @@ -1,92 +1 @@ -import Ember from 'ember'; - -/** - A container and manager for `nf-range-marker` components. - Used to draw an association between `nf-range-marker` components so they - can be laid out in a manner in which they don't collide. - @namespace components - @class nf-range-markers - @extends Ember.Component -*/ -export default Ember.Component.extend({ - tagName: 'g', - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - Sets the orientation of the range markers. - - - `'bottom'` - Range markers start at the bottom and stack upward - - `'top'` - Range markers start at the top and stack downward - @property orient - @type String - @default 'bottom' - */ - orient: 'bottom', - - /** - The margin, in pixels, between the markers - @property markerMargin - @type Number - @default 10 - */ - markerMargin: 10, - - /** - The marker components registered with this container - @property markers - @type Array - @readonly - */ - markers: Ember.computed(function() { - return Ember.A(); - }), - - /** - Adds the passed marker to the `markers` list, and sets the `prevMarker` and `nextMarker` - properties on the marker component and it's neighbor. - @method registerMarker - @param marker {nf-range-marker} the range marker to register with this container - */ - registerMarker: function(marker) { - let markers = this.get('markers'); - let prevMarker = markers[markers.length - 1]; - - Ember.run.schedule('afterRender', () => { - if(prevMarker) { - marker.set('prevMarker', prevMarker); - prevMarker.set('nextMarker', marker); - } - - markers.pushObject(marker); - }); - }, - - /** - Removes the marker from the `markers` list. Also updates the `nextMarker` and `prevMarker` - properties of it's neighboring components. - @method unregisterMarker - @param marker {nf-range-marker} the range marker to remove from the `markers` list. - */ - unregisterMarker: function(marker) { - if(marker) { - Ember.run.schedule('afterRender', () => { - let next = marker.nextMarker; - let prev = marker.prevMarker; - if(prev) { - prev.set('nextMarker', next); - } - if(next) { - next.set('prevMarker', prev); - } - this.get('markers').removeObject(marker); - }); - } - }, -}); +export { default } from 'ember-nf-graph/components/nf-range-markers'; diff --git a/app/components/nf-right-tick.js b/app/components/nf-right-tick.js index acc456e..732559b 100644 --- a/app/components/nf-right-tick.js +++ b/app/components/nf-right-tick.js @@ -1,141 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -/** - Draws a line and a chevron at the specified domain value - on the right side of an `nf-graph`. - - ### Tips - - - Adding `paddingRight` to `nf-graph` component will not affect `nf-right-tick`'s position. - - @namespace components - @class nf-right-tick - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'g', - - classNames: ['nf-right-tick'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The transition duration in milliseconds - @property duration - @type Number - @default 400 - */ - duration: 400, - - /** - The domain value at which to place the tick - @property value - @type Number - @default null - */ - value: null, - - /** - Sets the visibility of the component. Returns false if `y` is not - a numeric data type. - @property isVisible - @private - @readonly - */ - isVisible: Ember.computed('y', function(){ - return !isNaN(this.get('y')); - }), - - /** - The calculated y coordinate of the tick - @property y - @type Number - @readonly - */ - y: Ember.computed('value', 'yScale', 'graph.paddingTop', function() { - let value = this.get('value'); - let yScale = this.get('yScale'); - let paddingTop = this.get('graph.paddingTop'); - let vy = 0; - if(yScale) { - vy = yScale(value) || 0; - } - return vy + paddingTop; - }), - - /** - The SVG transform used to render the tick - @property transform - @type String - @private - @readonly - */ - transform: Ember.computed('y', 'graph.width', function(){ - let y = this.get('y'); - let graphWidth = this.get('graph.width'); - let x0 = graphWidth - 6; - let y0 = y - 3; - return `translate(${x0} ${y0})`; - }), - - /** - performs the D3 transition to move the tick to the proper position. - @method _transitionalUpdate - @private - */ - _transitionalUpdate: function(){ - let transform = this.get('transform'); - let path = this.get('path'); - let duration = this.get('duration'); - path.transition().duration(duration) - .attr('transform', transform); - }, - - /** - Schedules the transition when `value` changes on on init. - @method _triggerTransition - @private - */ - _triggerTransition: Ember.on('init', Ember.observer('value', function(){ - Ember.run.scheduleOnce('afterRender', this, this._transitionalUpdate); - })), - - /** - Updates the tick position without a transition. - @method _nonTransitionalUpdate - @private - */ - _nonTransitionalUpdate: function(){ - let transform = this.get('transform'); - let path = this.get('path'); - path.attr('transform', transform); - }, - - /** - Schedules the update of non-transitional positions - @method _triggerNonTransitionalUpdate - @private - */ - _triggerNonTransitionalUpdate: Ember.observer('graph.width', function(){ - Ember.run.scheduleOnce('afterRender', this, this._nonTransitionalUpdate); - }), - - /** - Gets the elements required to do the d3 transitions - @method _getElements - @private - */ - _getElements: Ember.on('didInsertElement', function(){ - let g = d3.select(this.$()[0]); - let path = g.selectAll('path').data([0]); - this.set('path', path); - }) -}); +export { default } from 'ember-nf-graph/components/nf-right-tick'; diff --git a/app/components/nf-selection-box.js b/app/components/nf-selection-box.js index b9055e4..415a3ad 100644 --- a/app/components/nf-selection-box.js +++ b/app/components/nf-selection-box.js @@ -1,157 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; - -/** - Draws a rectangle on an `nf-graph` given domain values `xMin`, `xMax`, `yMin` and `yMax`. - @namespace components - @class nf-selection-box - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'g', - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The duration of the transition in ms - @property duration - @type Number - @default 400 - */ - duration: 400, - - /** - The minimum x domain value to encompass. - @property xMin - @default null - */ - xMin: null, - - /** - The maximum x domain value to encompoass. - @property xMax - @default null - */ - xMax: null, - - /** - The minimum y domain value to encompass. - @property yMin - @default null - */ - yMin: null, - - /** - The maximum y domain value to encompass - @property yMax - @default null - */ - yMax: null, - - classNames: ['nf-selection-box'], - - /** - The x pixel position of xMin - @property x0 - @type Number - */ - x0: Ember.computed('xMin', 'xScale', function(){ - return normalizeScale(this.get('xScale'), this.get('xMin')); - }), - - /** - The x pixel position of xMax - @property x1 - @type Number - */ - x1: Ember.computed('xMax', 'xScale', function(){ - return normalizeScale(this.get('xScale'), this.get('xMax')); - }), - - /** - The y pixel position of yMin - @property y0 - @type Number - */ - y0: Ember.computed('yMin', 'yScale', function(){ - return normalizeScale(this.get('yScale'), this.get('yMin')); - }), - - /** - The y pixel position of yMax - @property y1 - @type Number - */ - y1: Ember.computed('yMax', 'yScale', function(){ - return normalizeScale(this.get('yScale'), this.get('yMax')); - }), - - /** - The SVG path string for the box's rectangle. - @property rectPath - @type String - */ - rectPath: Ember.computed('x0', 'x1', 'y0', 'y1', function(){ - let x0 = this.get('x0'); - let x1 = this.get('x1'); - let y0 = this.get('y0'); - let y1 = this.get('y1'); - return `M${x0},${y0} L${x0},${y1} L${x1},${y1} L${x1},${y0} L${x0},${y0}`; - }), - - /** - Updates the position of the box with a transition - @method doUpdatePosition - */ - doUpdatePosition: function(){ - let boxRect = this.get('boxRectElement'); - let rectPath = this.get('rectPath'); - let duration = this.get('duration'); - - boxRect.transition().duration(duration) - .attr('d', rectPath); - }, - - doUpdatePositionStatic: function(){ - let boxRect = this.get('boxRectElement'); - let rectPath = this.get('rectPath'); - - boxRect.attr('d', rectPath); - }, - - /** - Schedules an update to the position of the box after render. - @method updatePosition - @private - */ - updatePosition: Ember.observer('xMin', 'xMax', 'yMin', 'yMax', function(){ - Ember.run.once(this, this.doUpdatePosition); - }), - - staticPositionChange: Ember.on('didInsertElement', Ember.observer('xScale', 'yScale', function(){ - Ember.run.once(this, this.doUpdatePositionStatic); - })), - - /** - Sets up the required d3 elements after component - is inserted into the DOM - @method didInsertElement - */ - didInsertElement: function(){ - let element = this.get('element'); - let g = d3.select(element); - let boxRect = g.append('path') - .attr('class', 'nf-selection-box-rect') - .attr('d', this.get('rectPath')); - - this.set('boxRectElement', boxRect); - }, -}); +export { default } from 'ember-nf-graph/components/nf-selection-box'; diff --git a/app/components/nf-svg-image.js b/app/components/nf-svg-image.js index adea286..7b48d76 100644 --- a/app/components/nf-svg-image.js +++ b/app/components/nf-svg-image.js @@ -1,153 +1 @@ -import Ember from 'ember'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; -import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; -import computed from 'ember-new-computed'; - -/** - An image to be displayed in a graph with that takes domain based measurements and - uses the scale of the graph. Creates an `` SVG element. - @namespace components - @class nf-svg-image - @extends Ember.Component - @uses mixins.graph-requires-scale-source - @uses mixins.graph-selectable-graphic -*/ -export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { - tagName: 'image', - - classNameBindings: [':nf-svg-image', 'selectable', 'selected'], - - attributeBindings: ['svgX:x', 'svgY:y', 'svgWidth:width', 'svgHeight:height', 'src:href'], - - click: function(){ - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - }, - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The domain x value to place the image at. - @property x - @default null - */ - x: null, - - /** - The domain y value to place the image at. - @property y - @default null - */ - y: null, - - _width: 0, - - /** - The width as a domain value. Does not handle ordinal - scales. To set a pixel value, set `svgWidth` directly. - @property width - @type Number - @default 0 - */ - width: computed({ - get() { - return this._width; - }, - set(key, value) { - return this._width = Math.max(0, +value) || 0; - } - }), - - _height: 0, - - /** - The height as a domain value. Does not - handle ordinal scales. To set a pixel value, just - set `svgHeight` directly. - @property height - @default null - */ - height: computed({ - get() { - return this._height; - }, - set(key, value) { - this._height = Math.max(0, +value) || 0; - } - }), - - /** - The image source url - @property src - @type String - */ - src: '', - - x0: computed('x', 'xScale', function(){ - return normalizeScale(this.get('xScale'), this.get('x')); - }), - - y0: computed('y', 'yScale', function(){ - return normalizeScale(this.get('yScale'), this.get('y')); - }), - - x1: computed('xScale', 'width', 'x', function(){ - let scale = this.get('xScale'); - if(scale.rangeBands) { - throw new Error('nf-image does not support ordinal scales'); - } - return normalizeScale(scale, this.get('width') + this.get('x')); - }), - - y1: computed('yScale', 'height', 'y', function(){ - let scale = this.get('yScale'); - if(scale.rangeBands) { - throw new Error('nf-image does not support ordinal scales'); - } - return normalizeScale(scale, this.get('height') + this.get('y')); - }), - - /** - The pixel value at which to plot the image. - @property svgX - @type Number - */ - svgX: computed('x0', 'x1', function(){ - return Math.min(this.get('x0'), this.get('x1')); - }), - - /** - The pixel value at which to plot the image. - @property svgY - @type Number - */ - svgY: computed('y0', 'y1', function(){ - return Math.min(this.get('y0'), this.get('y1')); - }), - - /** - The width, in pixels, of the image. - @property svgWidth - @type Number - */ - svgWidth: computed('x0', 'x1', function(){ - return Math.abs(this.get('x0') - this.get('x1')); - }), - - /** - The height, in pixels of the image. - @property svgHeight - @type Number - */ - svgHeight: computed('y0', 'y1', function(){ - return Math.abs(this.get('y0') - this.get('y1')); - }), -}); +export { default } from 'ember-nf-graph/components/nf-svg-image'; diff --git a/app/components/nf-svg-line.js b/app/components/nf-svg-line.js index d58459c..ed5f17f 100644 --- a/app/components/nf-svg-line.js +++ b/app/components/nf-svg-line.js @@ -1,98 +1 @@ -import Ember from 'ember'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; -import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; - -/** - Draws a basic line between two points on the graph. - @namespace components - @class nf-svg-line - @extends Ember.Component - @uses mixins.graph-requires-scale-source - @uses mixins.graph-selectable-graphic -*/ -export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { - tagName: 'line', - - classNameBindings: [':nf-svg-line', 'selectable', 'selected'], - - attributeBindings: ['svgX1:x1', 'svgX2:x2', 'svgY1:y1', 'svgY2:y2'], - - click: function(){ - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - }, - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The domain value to plot the SVGLineElement's x1 at. - @property x1 - @default null - */ - x1: null, - - /** - The domain value to plot the SVGLineElement's x2 at. - @property x2 - @default null - */ - x2: null, - - /** - The domain value to plot the SVGLineElement's y1 at. - @property y1 - @default null - */ - y1: null, - - /** - The domain value to plot the SVGLineElement's y2 at. - @property y2 - @default null - */ - y2: null, - - /** - The pixel value to plot the SVGLineElement's x1 at. - @property svgX1 - @type Number - */ - svgX1: Ember.computed('x1', 'xScale', function(){ - return normalizeScale(this.get('xScale'), this.get('x1')); - }), - - /** - The pixel value to plot the SVGLineElement's x2 at. - @property svgX2 - @type Number - */ - svgX2: Ember.computed('x2', 'xScale', function(){ - return normalizeScale(this.get('xScale'), this.get('x2')); - }), - - /** - The pixel value to plot the SVGLineElement's y1 at. - @property svgY1 - @type Number - */ - svgY1: Ember.computed('y1', 'yScale', function(){ - return normalizeScale(this.get('yScale'), this.get('y1')); - }), - - /** - The pixel value to plot the SVGLineElement's y2 at. - @property svgY2 - @type Number - */ - svgY2: Ember.computed('y2', 'yScale', function(){ - return normalizeScale(this.get('yScale'), this.get('y2')); - }), -}); +export { default } from 'ember-nf-graph/components/nf-svg-line'; diff --git a/app/components/nf-svg-path.js b/app/components/nf-svg-path.js index 08ee530..0fd9647 100644 --- a/app/components/nf-svg-path.js +++ b/app/components/nf-svg-path.js @@ -1,93 +1 @@ -import Ember from 'ember'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; -import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; - -/** - An SVG path primitive that plots based on a graph's scale. - @namespace components - @class nf-svg-path - @extends Ember.Component - @uses mixins.graph-requires-scale-source - @uses mixins.graph-selectable-graphic -*/ -export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { - type: 'path', - - classNameBindings: [':nf-svg-path', 'selectable', 'selected'], - - attributeBindings: ['d'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The array of points to use to plot the path. This is an array of arrays, in the following format: - - // specify path pen commands - [ - [50, 50, 'L'], - [100, 100, 'L'] - ] - - // or they will default to 'L' - [ - [50, 50], - [100, 100] - ] - - @property points - @type Array - */ - points: null, - - /** - The data points mapped to scale - @property svgPoints - @type Array - */ - svgPoints: Ember.computed('points.[]', 'xScale', 'yScale', function(){ - let points = this.get('points'); - let xScale = this.get('xScale'); - let yScale = this.get('yScale'); - if(Ember.isArray(points) && points.length > 0) { - return points.map(function(v) { - let dx = normalizeScale(xScale, v[0]); - let dy = normalizeScale(yScale, v[1]); - let c = v.length > 2 ? v[2] : 'L'; - return [dx, dy, c]; - }); - } - }), - - click: function(){ - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - }, - - /** - The raw svg path d attribute output - @property d - @type String - */ - d: Ember.computed('svgPoints', function(){ - let svgPoints = this.get('svgPoints'); - if(Ember.isArray(svgPoints) && svgPoints.length > 0) { - return svgPoints.reduce(function(d, pt, i) { - if(i === 0) { - d += 'M' + pt[0] + ',' + pt[1]; - } - d += ' ' + pt[2] + pt[0] + ',' + pt[1]; - return d; - }, ''); - } else { - return 'M0,0'; - } - }), -}); +export { default } from 'ember-nf-graph/components/nf-svg-path'; diff --git a/app/components/nf-svg-rect.js b/app/components/nf-svg-rect.js index 670bf41..7726693 100644 --- a/app/components/nf-svg-rect.js +++ b/app/components/nf-svg-rect.js @@ -1,165 +1 @@ -import Ember from 'ember'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import { normalizeScale } from 'ember-nf-graph/utils/nf/scale-utils'; -import SelectableGraphic from 'ember-nf-graph/mixins/graph-selectable-graphic'; -import computed from 'ember-new-computed'; - -/** - A rectangle that plots using domain values from the graph. Uses an SVGPathElement - to plot the rectangle, to allow for rectangles with "negative" heights. - @namespace components - @class nf-svg-rect - @extends Ember.Component - @uses mixins.graph-requires-scale-source - @uses mixins.graph-selectable-graphic -*/ -export default Ember.Component.extend(RequiresScaleSource, SelectableGraphic, { - tagName: 'path', - - attributeBindings: ['d'], - - classNameBindings: [':nf-svg-rect', 'selectable', 'selected'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The domain x value to place the rect at. - @property x - @default null - */ - x: null, - - /** - The domain y value to place the rect at. - @property y - @default null - */ - y: null, - - _width: 0, - - /** - The width as a domain value. If xScale is ordinal, - then this value is the indice offset to which to draw the - rectangle. In other words, if it's `2`, then draw the rectangle - to two ordinals past whatever `x` is set to. - @property width - @type Number - @default 0 - */ - width: computed({ - get() { - return this._width; - }, - set(key, value) { - return this._width = +value; - } - }), - - _height: 0, - - /** - The height as a domain value. If the yScale is ordinal, - this value is the indice offset to which to draw the rectangle. - For example, if the height is `3` then draw the rectangle - to two ordinals passed whatever `y` is set to. - @property height - @type Number - @default 0 - */ - height: computed({ - get() { - return this._height; - }, - set(key, value) { - return this._height = +value; - } - }), - - /** - The x value of the bottom right corner of the rectangle. - @property x1 - @type Number - */ - x1: computed('width', 'x', 'xScale', function(){ - let xScale = this.get('xScale'); - let w = this.get('width'); - let x = this.get('x'); - if(xScale.rangeBands) { - let domain = xScale.domain(); - let fromIndex = domain.indexOf(x); - let toIndex = fromIndex + w; - return normalizeScale(xScale, domain[toIndex]); - } else { - x = +x || 0; - return normalizeScale(xScale, w + x); - } - }), - - /** - The y value of the bottom right corner of the rectangle - @property y1 - @type Number - */ - y1: computed('height', 'y', 'yScale', function(){ - let yScale = this.get('yScale'); - let h = this.get('height'); - let y = this.get('y'); - if(yScale.rangeBands) { - let domain = yScale.domain(); - let fromIndex = domain.indexOf(y); - let toIndex = fromIndex + h; - return normalizeScale(yScale, domain[toIndex]); - } else { - y = +y || 0; - return normalizeScale(yScale, h + y); - } - }), - - /** - The x value of the top right corner of the rectangle - @property x0 - @type Number - */ - x0: computed('x', 'xScale', function(){ - return normalizeScale(this.get('xScale'), this.get('x')); - }), - - /** - The y value of the top right corner of the rectangle. - @property y0 - @type Number - */ - y0: computed('y', 'yScale', function() { - return normalizeScale(this.get('yScale'), this.get('y')); - }), - - /** - The SVG path data for the rectangle - @property d - @type String - */ - d: computed('x0', 'y0', 'x1', 'y1', function(){ - let x0 = this.get('x0'); - let y0 = this.get('y0'); - let x1 = this.get('x1'); - let y1 = this.get('y1'); - return `M${x0},${y0} L${x0},${y1} L${x1},${y1} L${x1},${y0} L${x0},${y0}`; - }), - - /** - Click event handler. Toggles selected if selectable. - @method click - */ - click: function(){ - if(this.get('selectable')) { - this.toggleProperty('selected'); - } - } -}); +export { default } from 'ember-nf-graph/components/nf-svg-rect'; diff --git a/app/components/nf-tick-label.js b/app/components/nf-tick-label.js index 8e075fc..77ee943 100644 --- a/app/components/nf-tick-label.js +++ b/app/components/nf-tick-label.js @@ -1,15 +1 @@ -import Ember from 'ember'; - -export default Ember.Component.extend({ - tagName: 'g', - - attributeBindings: ['transform'], - - transform: Ember.computed('x', 'y', function(){ - let x = this.get('x'); - let y = this.get('y'); - return `translate(${x} ${y})`; - }), - - className: 'nf-tick-label' -}); +export { default } from 'ember-nf-graph/components/nf-tick-label'; diff --git a/app/components/nf-tracker.js b/app/components/nf-tracker.js index 28487fb..3e2a0aa 100644 --- a/app/components/nf-tracker.js +++ b/app/components/nf-tracker.js @@ -1,39 +1 @@ -import Ember from 'ember'; -import DataGraphic from 'ember-nf-graph/mixins/graph-data-graphic'; -import RequiresScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import GraphicWithTrackingDot from 'ember-nf-graph/mixins/graph-graphic-with-tracking-dot'; -import computed from 'ember-new-computed'; - -/** - A tracking graphic component used to do things like create tracking dots for nf-area or nf-line. - @namespace components - @class nf-tracker - @uses mixins.graph-data-graphic - @uses mixins.graph-requires-scale-source - @uses mixins.graph-graphic-with-tracking-dot - */ -export default Ember.Component.extend(DataGraphic, RequiresScaleSource, GraphicWithTrackingDot, { - tagName: 'g', - - classNameBindings: [':nf-tracker'], - - attributeBindings: ['transform'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - transform: computed('trackedData.x', 'trackedData.y', 'xScale', 'yScale', { - get() { - let xScale = this.get('xScale'); - let yScale = this.get('yScale'); - let x = xScale && xScale(this.get('trackedData.x') || 0); - let y = yScale && yScale(this.get('trackedData.y') || 0); - return 'translate(' + x + ',' + y + ')'; - } - }) -}); +export { default } from 'ember-nf-graph/components/nf-tracker'; diff --git a/app/components/nf-vertical-line.js b/app/components/nf-vertical-line.js index 9e31940..0cdf3e9 100644 --- a/app/components/nf-vertical-line.js +++ b/app/components/nf-vertical-line.js @@ -1,65 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; - -/** - Draws a vertical line on a graph at a given x domain value - @namespace components - @class nf-vertical-line - @extends Ember.Component - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'line', - - classNames: ['nf-vertical-line'], - - attributeBindings: ['lineX:x1', 'lineX:x2', 'y1', 'y2'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The top y coordinate of the line - @property y1 - @type Number - @default 0 - @private - */ - y1: 0, - - /** - The bottom y coordinate of the line - @property y2 - @type Number - @private - @readonly - */ - y2: Ember.computed.alias('graph.graphHeight'), - - /** - The x domain value at which to draw the vertical line on the graph - @property x - @type Number - @default null - */ - x: null, - - /** - The calculated x coordinate of the vertical line - @property lineX - @type Number - @private - @readonly - */ - lineX: Ember.computed('xScale', 'x', function(){ - let xScale = this.get('xScale'); - let x = this.get('x'); - let px = xScale ? xScale(x) : -1; - return px && px > 0 ? px : 0; - }), -}); +export { default } from 'ember-nf-graph/components/nf-vertical-line'; diff --git a/app/components/nf-x-axis.js b/app/components/nf-x-axis.js index 97b964f..62de997 100644 --- a/app/components/nf-x-axis.js +++ b/app/components/nf-x-axis.js @@ -1,302 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import computed from 'ember-new-computed'; -import layout from '../templates/components/nf-x-axis'; - -/** - A component for adding a templated x axis to an `nf-graph` component. - All items contained within this component are used to template each tick mark on the - rendered graph. Tick values are supplied to the inner scope of this component on the - view template via `tick`. - - ### Styling - - The main container will have a `nf-x-axis` class. - A `orient-top` or `orient-bottom` container will be applied to the container - depending on the `orient` setting. - - Ticks are positioned via a `` tag, that will contain whatever is passed into it via - templating, along with the tick line. `` tags within tick templates do have some - default styling applied to them to position them appropriately based off of orientation. - - ### Example - - {{#nf-graph width=500 height=300}} - {{#nf-x-axis height=40 as |tick|}} - x is {{tick.value}} - {{/nf-x-axis}} - {{/nf-graph}} - - - @namespace components - @class nf-x-axis - @extends Ember.Component - @uses mixins.graph-has-graph-parent - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'g', - - layout: layout, - template: null, - - attributeBindings: ['transform'], - classNameBindings: ['orientClass'], - classNames: ['nf-x-axis'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The height of the x axis in pixels. - @property height - @type Number - @default 20 - */ - height: 20, - - /** - The number of ticks to display - @property tickCount - @type Number - @default 12 - */ - tickCount: 12, - - /** - The length of the tick line (the small vertical line indicating the tick) - @property tickLength - @type Number - @default 0 - */ - tickLength: 0, - - /** - The spacing between the end of the tick line and the origin of the templated - tick content - @property tickPadding - @type Number - @default 5 - */ - tickPadding: 5, - - /** - The orientation of the x axis. Value can be `'top'` or `'bottom'`. - @property orient - @type String - @default 'bottom' - */ - orient: 'bottom', - - _tickFilter: null, - - /** - An optional filtering function to allow more control over what tick marks are displayed. - The function should have exactly the same signature as the function you'd use for an - `Array.prototype.filter()`. - - @property tickFilter - @type Function - @default null - @example - - {{#nf-x-axis tickFilter=myFilter as |tick|}} - {{tick.value}} - {{/nf-x-axis}} - - And on your controller: - - myFilter: function(tick, index, ticks) { - return tick.value < 1000; - }, - - The above example will filter down the set of ticks to only those that are less than 1000. - */ - tickFilter: computed.alias('_tickFilter'), - - /** - The class applied due to orientation (e.g. `'orient-top'`) - @property orientClass - @type String - @readonly - */ - orientClass: computed('orient', function(){ - return 'orient-' + this.get('orient'); - }), - - /** - The SVG Transform applied to this component's container. - @property transform - @type String - @readonly - */ - transform: computed('x', 'y', function(){ - let x = this.get('x') || 0; - let y = this.get('y') || 0; - return `translate(${x} ${y})`; - }), - - /** - The y position of this component's container. - @property y - @type Number - @readonly - */ - y: computed( - 'orient', - 'graph.paddingTop', - 'graph.paddingBottom', - 'graph.height', - 'height', - function(){ - let orient = this.get('orient'); - let graphHeight = this.get('graph.height'); - let height = this.get('height'); - let paddingBottom = this.get('graph.paddingBottom'); - let paddingTop = this.get('graph.paddingTop'); - let y; - - if(orient === 'bottom') { - y = graphHeight - paddingBottom - height; - } else { - y = paddingTop; - } - - return y || 0; - } - ), - - /** - This x position of this component's container - @property x - @type Number - @readonly - */ - x: computed('graph.graphX', function(){ - return this.get('graph.graphX') || 0; - }), - - init() { - this._super(...arguments); - - Ember.run.schedule('afterRender', () => { - this.set('graph.xAxis', this); - }); - }, - - /** - The width of the component - @property width - @type Number - @readonly - */ - width: computed.alias('graph.graphWidth'), - - /** - A method to call to override the default behavior of how ticks are created. - - The function signature should match: - - // - scale: d3.Scale - // - tickCount: number of ticks - // - uniqueData: unique data points for the axis - // - scaleType: string of "linear" or "ordinal" - // returns: an array of tick values. - function(scale, tickCount, uniqueData, scaleType) { - return [100,200,300]; - } - - @property tickFactory - @type {Function} - @default null - */ - tickFactory: null, - - tickData: computed('xScale', 'graph.xScaleType', 'uniqueXData', 'tickCount', 'tickFactory', function(){ - let tickFactory = this.get('tickFactory'); - let scale = this.get('xScale'); - let uniqueData = this.get('uniqueXData'); - let tickCount = this.get('tickCount'); - let scaleType = this.get('graph.xScaleType'); - - if(tickFactory) { - return tickFactory(scale, tickCount, uniqueData, scaleType); - } - else if(scaleType === 'ordinal') { - return uniqueData; - } - else { - return scale.ticks(tickCount); - } - }), - - /** - A unique set of all x data on the graph - @property uniqueXData - @type Array - @readonly - */ - uniqueXData: computed.uniq('graph.xData'), - - /** - The models for the ticks to display on the axis. - @property ticks - @type Array - @readonly - */ - ticks: computed( - 'xScale', - 'tickPadding', - 'tickLength', - 'height', - 'orient', - 'tickFilter', - 'tickData', - 'graph.xScaleType', - function(){ - let xScale = this.get('xScale'); - let xScaleType = this.get('graph.xScaleType'); - let tickPadding = this.get('tickPadding'); - let tickLength = this.get('tickLength'); - let height = this.get('height'); - let orient = this.get('orient'); - let tickFilter = this.get('tickFilter'); - let ticks = this.get('tickData'); - let y1 = orient === 'top' ? height : 0; - let y2 = y1 + tickLength; - let labely = orient === 'top' ? (y1 - tickPadding) : (y1 + tickPadding); - let halfBandWidth = (xScaleType === 'ordinal') ? xScale.rangeBand() / 2 : 0; - let result = ticks.map(function(tick) { - return { - value: tick, - x: xScale(tick) + halfBandWidth, - y1: y1, - y2: y2, - labely: labely - }; - }); - - if(tickFilter) { - result = result.filter(tickFilter); - } - - return Ember.A(result); - } - ), - - /** - The y position, in pixels, of the axis line - @property axisLineY - @type Number - @readonly - */ - axisLineY: computed('orient', 'height', function(){ - return this.get('orient') === 'top' ? this.get('height') : 0; - }) - -}); +export { default } from 'ember-nf-graph/components/nf-x-axis'; diff --git a/app/components/nf-y-axis.js b/app/components/nf-y-axis.js index 4e64f87..9f53832 100644 --- a/app/components/nf-y-axis.js +++ b/app/components/nf-y-axis.js @@ -1,293 +1 @@ -import Ember from 'ember'; -import RequireScaleSource from 'ember-nf-graph/mixins/graph-requires-scale-source'; -import computed from 'ember-new-computed'; -import layout from '../templates/components/nf-y-axis'; - -/** - A component for adding a templated y axis to an `nf-graph` component. - All items contained within this component are used to template each tick mark on the - rendered graph. Tick values are supplied to the inner scope of this component on the - view template via `tick`. - - ### Styling - - The main container will have a `nf-y-axis` class. - A `orient-left` or `orient-right` container will be applied to the container - depending on the `orient` setting. - - Ticks are positioned via a `` tag, that will contain whatever is passed into it via - templating, along with the tick line. `` tags within tick templates do have some - default styling applied to them to position them appropriately based off of orientation. - - ### Example - - {{#nf-graph width=500 height=300}} - {{#nf-y-axis width=40 as |tick|}} - y is {{tick.value}} - {{/nf-y-axis}} - {{/nf-graph}} - - - @namespace components - @class nf-y-axis - @uses mixins.graph-has-graph-parent - @uses mixins.graph-requires-scale-source -*/ -export default Ember.Component.extend(RequireScaleSource, { - tagName: 'g', - - layout: layout, - template: null, - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - The number of ticks to display - @property tickCount - @type Number - @default 5 - */ - tickCount: 5, - - /** - The length of the tick's accompanying line. - @property tickLength - @type Number - @default 5 - */ - tickLength: 5, - - /** - The distance between the tick line and the origin tick's templated output - @property tickPadding - @type Number - @default 3 - */ - tickPadding: 3, - - /** - The total width of the y axis - @property width - @type Number - @default 40 - */ - width: 40, - - /** - The orientation of the y axis. Possible values are `'left'` and `'right'` - @property orient - @type String - @default 'left' - */ - orient: 'left', - - attributeBindings: ['transform'], - - classNameBindings: [':nf-y-axis', 'isOrientRight:orient-right:orient-left'], - - _tickFilter: null, - - /** - An optional filtering function to allow more control over what tick marks are displayed. - The function should have exactly the same signature as the function you'd use for an - `Array.prototype.filter()`. - - @property tickFilter - @type Function - @default null - @example - - {{#nf-y-axis tickFilter=myFilter as |tick|}} - {{tick.value}} - {{/nf-y-axis}} - - And on your controller: - - myFilter: function(tick, index, ticks) { - return tick.value < 1000; - }, - - The above example will filter down the set of ticks to only those that are less than 1000. - */ - tickFilter: computed.alias('_tickFilter'), - - /** - computed property. returns true if `orient` is equal to `'right'`. - @property isOrientRight - @type Boolean - @readonly - */ - isOrientRight: computed.equal('orient', 'right'), - - - /** - The SVG transform for positioning the component. - @property transform - @type String - @readonly - */ - transform: computed('x', 'y', function(){ - let x = this.get('x') || 0; - let y = this.get('y') || 0; - return `translate(${x} ${y})`; - }), - - /** - The x position of the component - @property x - @type Number - @readonly - */ - x: computed( - 'orient', - 'graph.width', - 'width', - 'graph.paddingLeft', - 'graph.paddingRight', - function(){ - let orient = this.get('orient'); - if(orient !== 'left') { - return this.get('graph.width') - this.get('width') - this.get('graph.paddingRight'); - } - return this.get('graph.paddingLeft'); - } - ), - - /** - The y position of the component - @property y - @type Number - @readonly - */ - y: computed.alias('graph.graphY'), - - /** - the height of the component - @property height - @type Number - @readonly - */ - height: computed.alias('graph.graphHeight'), - - init() { - this._super(...arguments); - - Ember.run.schedule('afterRender', () => { - this.set('graph.yAxis', this); - }); - }, - - /** - A method to call to override the default behavior of how ticks are created. - - The function signature should match: - - // - scale: d3.Scale - // - tickCount: number of ticks - // - uniqueData: unique data points for the axis - // - scaleType: string of "linear" or "ordinal" - // returns: an array of tick values. - function(scale, tickCount, uniqueData, scaleType) { - return [100,200,300]; - } - - @property tickFactory - @type {Function} - @default null - */ - tickFactory: null, - - tickData: computed('graph.yScaleType', 'uniqueYData', 'yScale', 'tickCount', 'tickFactory', function(){ - let tickFactory = this.get('tickFactory'); - let scale = this.get('yScale'); - let uniqueData = this.get('uniqueYData'); - let scaleType = this.get('graph.yScaleType'); - let tickCount = this.get('tickCount'); - - if(tickFactory) { - return tickFactory(scale, tickCount, uniqueData, scaleType); - } - else if(scaleType === 'ordinal') { - return uniqueData; - } - else { - let ticks = scale.ticks(tickCount); - if (scaleType === 'log') { - let step = Math.round(ticks.length / tickCount); - ticks = ticks.filter(function (tick, i) { - return i % step === 0; - }); - } - return ticks; - } - }), - - /** - All y data from the graph, filtered to unique values. - @property uniqueYData - @type Array - @readonly - */ - uniqueYData: computed.uniq('graph.yData'), - - /** - The ticks to be displayed. - @property ticks - @type Array - @readonly - */ - ticks: computed( - 'yScale', - 'tickPadding', - 'axisLineX', - 'tickLength', - 'isOrientRight', - 'tickFilter', - 'tickData', - function() { - let yScale = this.get('yScale'); - let tickPadding = this.get('tickPadding'); - let axisLineX = this.get('axisLineX'); - let tickLength = this.get('tickLength'); - let isOrientRight = this.get('isOrientRight'); - let tickFilter = this.get('tickFilter'); - let ticks = this.get('tickData'); - let x1 = isOrientRight ? axisLineX + tickLength : axisLineX - tickLength; - let x2 = axisLineX; - let labelx = isOrientRight ? (tickLength + tickPadding) : (axisLineX - tickLength - tickPadding); - - let result = ticks.map(function (tick) { - return { - value: tick, - y: yScale(tick), - x1: x1, - x2: x2, - labelx: labelx, - }; - }); - - if(tickFilter) { - result = result.filter(tickFilter); - } - - return Ember.A(result); - } - ), - - - /** - The x position of the axis line. - @property axisLineX - @type Number - @readonly - */ - axisLineX: computed('isOrientRight', 'width', function(){ - return this.get('isOrientRight') ? 0 : this.get('width'); - }), -}); +export { default } from 'ember-nf-graph/components/nf-y-axis'; diff --git a/app/components/nf-y-diff.js b/app/components/nf-y-diff.js index 6273a80..5619216 100644 --- a/app/components/nf-y-diff.js +++ b/app/components/nf-y-diff.js @@ -1,262 +1 @@ -import Ember from 'ember'; -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, `` 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(RequireScaleSource, { - tagName: 'g', - - attributeBindings: ['transform'], - - classNameBindings: [':nf-y-diff', 'isPositive:positive:negative', 'isOrientRight:orient-right:orient-left'], - - /** - The parent graph for a component. - @property graph - @type components.nf-graph - @default null - */ - graph: null, - - /** - 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(){ - let yA = +this.get('yA') || 0; - let 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 x pixel coordinate of the content container. - @property contentX - @type Number - @readonly - */ - contentX: Ember.computed('isOrientRight', 'width', 'contentPadding', function(){ - let contentPadding = this.get('contentPadding'); - let width = this.get('width'); - return this.get('isOrientRight') ? width - contentPadding : contentPadding; - }), - - rectPath: Ember.computed('yA', 'yB', 'width', function(){ - let x = 0; - let w = +this.get('width') || 0; - let x2 = x + w; - let yA = +this.get('yA') || 0; - let 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(){ - let contentX = this.get('contentX'); - let 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(){ - let element = this.get('element'); - let g = d3.select(element); - - let rectPath = this.get('rectPath'); - let rect = g.insert('path', ':first-child') - .attr('class', 'nf-y-diff-rect') - .attr('d', rectPath); - - let contentTransform = this.get('contentTransform'); - let 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(){ - let duration = this.get('duration'); - let rectElement = this.get('rectElement'); - let 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(){ - let contentElement = this.get('contentElement'); - if(contentElement) { - let contentTransform = this.get('contentTransform'); - contentElement.attr('transform', contentTransform); - } - }, - - adjustGraphHeight: Ember.on('didInsertElement', Ember.observer('graph.graphHeight', function(){ - let rectElement = this.get('rectElement'); - let 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); - }) - ), -}); +export { default } from 'ember-nf-graph/components/nf-y-diff'; diff --git a/app/templates/components/nf-crosshair.hbs b/app/templates/components/nf-crosshair.hbs deleted file mode 100644 index 72f68e1..0000000 --- a/app/templates/components/nf-crosshair.hbs +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/app/templates/components/nf-table-manager.hbs b/app/templates/components/nf-table-manager.hbs deleted file mode 100644 index 8a52fdf..0000000 --- a/app/templates/components/nf-table-manager.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
- - - - {{yield}} - - -
-
diff --git a/package.json b/package.json index 32aea4c..b5385a4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "license": "Apache-2.0", "dependencies": { "ember-cli-babel": "^5.1.7", + "ember-cli-htmlbars": "^1.1.1", "ember-new-computed": "^1.0.3" }, "devDependencies": { @@ -37,7 +38,6 @@ "ember-cli-app-version": "^1.0.0", "ember-cli-dependency-checker": "^1.3.0", "ember-cli-eslint": "^3.0.3", - "ember-cli-htmlbars": "^1.1.1", "ember-cli-htmlbars-inline-precompile": "^0.3.6", "ember-cli-inject-live-reload": "^1.4.1", "ember-cli-qunit": "^3.1.2", diff --git a/tests/dummy/app/controllers/nf-graph/index.js b/tests/dummy/app/controllers/nf-graph/index.js index b4d4222..f92069d 100644 --- a/tests/dummy/app/controllers/nf-graph/index.js +++ b/tests/dummy/app/controllers/nf-graph/index.js @@ -32,9 +32,16 @@ function range(count) { export default Ember.Controller.extend({ graphWidth: 400, graphHeight: 300, + diffA: 100, + diffB: 200, queryParams: Ember.A(['graphWidth' , 'graphHeight']), + init(){ + this._super(...arguments); + this.send('updateLine'); + }, + xTickFilter: function() { return true; }, @@ -45,20 +52,15 @@ export default Ember.Controller.extend({ return ticks; }, - diffA: 100, - diffB: 200, - - fooData: null, - actions: { updateAreas() { this.set('model.area1', generateLineData(0, 0, 50, 20, 10)); - this.set('model.area2', generateLineData(0, 51, 100, 20, 11)); + this.set('model.area2', generateLineData(0, 51, 100, 20, 10)); this.set('model.area3', generateLineData(0, 101, 150, 20, 10)); }, - loadNewData: function(){ - this.set('lineData', generateLineData(0, 0, 2000, 200, 240, 500)); + updateLine: function(){ + this.set('lineData', generateLineData(0, 0, 200, 50, 10, 10)); }, brushStart: function(e) { diff --git a/tests/dummy/app/templates/nf-graph/index.hbs b/tests/dummy/app/templates/nf-graph/index.hbs index f44608b..e8f493b 100644 --- a/tests/dummy/app/templates/nf-graph/index.hbs +++ b/tests/dummy/app/templates/nf-graph/index.hbs @@ -1,13 +1,17 @@

Graph - Basics

- {{graphWidth}}px
{{graphHeight}}px
{{diffA}}
{{diffB}}
{{multiY}}
-{{graphWidth}}px +
+ + +
+ +{{graphWidth}}px X {{graphHeight}}px {{#nf-graph yMaxMode="push-tick" - yMax=1 xLink=groupX selectMultiple=true brushStartAction=(action "brushStart") @@ -45,16 +48,17 @@ {{#nf.graph as |graph|}} {{#graph.area-stack aggregate=1 as |stack|}} - {{stack.area class="area3" selectable=true interpolator="linear" data=model.area3 trackingMode="snap-last"}} - {{stack.area class="area2" selectable=true interpolator="linear" data=model.area2 trackingMode="snap-last"}} - {{stack.area class="area1" selectable=true interpolator="linear" data=model.area1 trackingMode="snap-last"}} + {{stack.area class="area3" interpolator="linear" data=model.area3 trackingMode="snap-last"}} + {{stack.area class="area2" interpolator="linear" data=model.area2 trackingMode="snap-last" trackedData=(mut tracked2)}} + {{stack.area class="area1" interpolator="linear" data=model.area1 trackingMode="snap-last"}} {{/graph.area-stack}} {{#graph.group scaleZoomX="2" scaleZoomY="2" as |group|}} - {{group.line data=model.area1 class="line1" trackingMode="snap-last" trackedData=(mut tracked2)}} + {{group.line data=lineData class="line1" trackingMode="snap-last"}} {{/graph.group}} {{graph.brush-selection left=(mut brushLeft) right=(mut brushRight)}} + {{graph.crosshairs}} {{/nf.graph}} {{#nf.x-axis as |tick|}} @@ -70,5 +74,3 @@ Tracked Data {{tracked2.y}} {{tracked2.renderY}} - -