Porting Netflix ember-nf-graph to Ember 2.7.0.
Last active
August 9, 2016 16:55
-
-
Save gtb104/959ddc4ce89d5094ba9f73be33538e33 to your computer and use it in GitHub Desktop.
Declarative D3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
import HasChartParent from '../has-chart-parent'; | |
const { | |
Component, | |
computed | |
} = Ember; | |
export default Component.extend(HasChartParent, { | |
tagName: 'g', | |
classNames: ['graph-content'], | |
attributeBindings: ['transform', 'clip-path'], | |
x: computed.alias('graph.graphX'), | |
y: computed.alias('graph.graphY'), | |
width: computed.alias('graph.graphWidth'), | |
height: computed.alias('graph.graphHeight'), | |
'clip-path': computed('graph.contentClipPathId', function() { | |
var clipPathId = this.get('graph.contentClipPathId'); | |
return `url(#${clipPathId})`; | |
}), | |
transform: computed('x', 'y', function() { | |
var x = this.get('x'); | |
var y = this.get('y'); | |
return `translate(${x} ${y})`; | |
}), | |
init() { | |
this._super(...arguments); | |
this.set('graph.content', this); | |
}, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
import HasChartParent from '../has-chart-parent'; | |
import RequiresScaleSource from '../requires-scale-source'; | |
const { | |
Component, | |
computed, | |
run | |
} = Ember; | |
export default Component.extend(HasChartParent, RequiresScaleSource, { | |
tagName: 'g', | |
data: null, | |
lineFn: computed('xScale', 'yScale', function() { | |
var xScale = this.get('xScale'); | |
var yScale = this.get('yScale'); | |
return d3.line() | |
.x(d => xScale(d.x)) | |
.y(d => yScale(d.y)) | |
.curve(d3.curveLinear); | |
}), | |
init() { | |
this._super(...arguments); | |
var graph = this.get('graph'); | |
if (graph) { | |
graph.registerGraphic(this); | |
} | |
}, | |
didUpdateAttrs() { | |
this._super(...arguments); | |
this.trigger('hasData', this.getAttr('data')); | |
this.update(); | |
}, | |
didInsertElement() { | |
this._super(...arguments); | |
this.draw(); | |
}, | |
willDestroyElement() { | |
this._super(...arguments); | |
var graph = this.get('graph'); | |
if (graph) { | |
graph.unregisterGraphic(this); | |
} | |
}, | |
draw() { | |
let path = d3.select(this.element).append('path') | |
.datum(this.getAttr('data')) | |
.attr('class', 'chart-line') | |
.attr('d', this.get('lineFn')); | |
this.set('path', path); | |
}, | |
update() { | |
this.get('path') | |
.datum(this.getAttr('data')) | |
.transition().duration(750) | |
.attr('d', this.get('lineFn')); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
const { | |
Component, | |
computed | |
} = Ember; | |
export default Component.extend({ | |
tagName: 'g', | |
className: 'chart-tick-label', | |
attributeBindings: ['transform'], | |
transform: computed('x', 'y', function() { | |
var x = this.get('x'); | |
var y = this.get('y'); | |
return `translate(${x} ${y})`; | |
}) | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
import HasChartParent from '../has-chart-parent'; | |
import RequireScaleSource from '../requires-scale-source'; | |
const { | |
Component, | |
computed | |
} = Ember; | |
const log = console.log; | |
export default Component.extend(HasChartParent, RequireScaleSource, { | |
tagName: 'g', | |
attributeBindings: ['transform'], | |
tickCount: 12, | |
width: 0, | |
height: 20, | |
axis: computed('xScale', function() { | |
let scale = this.get('xScale'); | |
let tics = this.get('tickCount'); | |
return d3.axisBottom(scale).ticks(tics); | |
}), | |
x: computed('graph.graphX', function() { | |
return this.get('graph.graphX') || 0; | |
}), | |
y: computed('graph.height', 'graph.paddingBottom', 'height', function() { | |
let graphHeight = this.get('graph.height'); | |
let padding = this.get('graph.paddingBottom'); | |
let height = this.get('height'); | |
return (graphHeight - padding - height) || 0; | |
}), | |
transform: computed('x', 'y', function() { | |
let x = this.get('x') || 0; | |
let y = this.get('y') || 0; | |
return `translate(${x} ${y})`; | |
}), | |
init() { | |
this._super(...arguments); | |
// The following line causes the deprecation warning | |
// "You modified (no label) twice in a single render." | |
// because we end up executing rsa-chart.graphHeight() | |
// twice due to xAxis.height changing. | |
// https://github.com/Netflix/ember-nf-graph/issues/104#issuecomment-157817609 | |
this.set('graph.xAxis', this); | |
}, | |
didInsertElement() { | |
let axis = this.get('axis'); | |
d3.select(this.element).call(axis); | |
}, | |
rescale: Ember.observer('axis', function() { | |
Ember.run.next(this, () => { | |
// We have to run in the next loop because if we don't, the scale's | |
// domain will be updated after we request to get the scale. | |
let g = d3.select(this.element); | |
let axis = this.get('axis'); | |
g.transition().duration(750).call(axis); | |
}); | |
}) | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
import HasChartParent from '../has-chart-parent'; | |
import RequireScaleSource from '../requires-scale-source'; | |
const { | |
Component, | |
computed | |
} = Ember; | |
const log = console.log; | |
export default Component.extend(HasChartParent, RequireScaleSource, { | |
tagName: 'g', | |
attributeBindings: ['transform'], | |
classNames: ['chart-x-axis', 'orient-bottom'], | |
width: computed.alias('graph.graphWidth'), | |
height: 20, | |
x: computed('graph.graphX', function() { | |
return this.get('graph.graphX') || 0; | |
}), | |
y: computed('graph.height', 'graph.paddingBottom', 'height', function() { | |
let graphHeight = this.get('graph.height'); | |
let padding = this.get('graph.paddingBottom'); | |
let height = this.get('height'); | |
return (graphHeight - padding - height) || 0; | |
}), | |
transform: computed('x', 'y', function() { | |
let x = this.get('x') || 0; | |
let y = this.get('y') || 0; | |
return `translate(${x} ${y})`; | |
}), | |
tickCount: 12, | |
tickLength: 10, | |
tickPadding: 5, | |
tickData: computed('xScale', 'tickCount', function() { | |
var scale = this.get('xScale'); | |
var tickCount = this.get('tickCount'); | |
return scale.ticks(tickCount); | |
}), | |
ticks: computed('xScale', 'tickPadding', 'tickLength', 'height', 'tickData', 'graph.xScaleType', function() { | |
var xScale = this.get('xScale'); | |
var xScaleType = this.get('graph.xScaleType'); | |
var tickPadding = this.get('tickPadding'); | |
var tickLength = this.get('tickLength'); | |
var height = this.get('height'); | |
var ticks = this.get('tickData'); | |
var y2 = tickLength; | |
var labely = y2 + tickPadding + tickLength; | |
var halfBandWidth = (xScaleType === 'ordinal') ? xScale.rangeBand() / 2 : 0; | |
return ticks.map(tick => { | |
return { | |
value: tick, | |
x: xScale(tick) + halfBandWidth, | |
y1: 0, | |
y2: y2, | |
labely: labely | |
} | |
}); | |
} | |
), | |
init() { | |
this._super(...arguments); | |
this.set('graph.xAxis', this); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
export default Ember.Controller.extend({ | |
appName: 'Declarative D3', | |
graphData: [ | |
{x:1469385570980, y:3}, | |
{x:1469731166074, y:6}, | |
{x:1469903961473, y:5}, | |
{x:1470076750392, y:2} | |
], | |
init() { | |
this._super(...arguments); | |
Ember.run.later(this, () => { | |
console.log('ADD DATA!!!!!'); | |
this.set('graphData', [ | |
{x:1469385570980, y:3}, | |
{x:1469731166074, y:6}, | |
{x:1469903961473, y:5}, | |
{x:1470076750392, y:2}, | |
{x:new Date().getTime(), y:7} | |
]); | |
}, 3000); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
export default Ember.Mixin.create({ | |
graph: null, | |
init() { | |
this._super(...arguments); | |
var graph = this.nearestWithProperty('isParentChart'); | |
this.set('graph', graph); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
const format = d3.timeFormat('%m/%d'); | |
export function formatDate([date]) { | |
return format(date); | |
} | |
export default Ember.Helper.helper(formatDate); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
const { | |
Mixin, | |
computed | |
} = Ember; | |
var scaleProperty = function(scaleKey, zoomKey, offsetKey) { | |
return computed(scaleKey, zoomKey, offsetKey, { | |
get() { | |
var scale = this.get(scaleKey); | |
var zoom = this.get(zoomKey); | |
var offset = this.get(offsetKey); | |
if (zoom === 1 && offset === 0) { | |
return scale; | |
} | |
var copy = scale.copy(); | |
var domain = copy.domain(); | |
copy.domain([domain[0] / zoom, domain[1] / zoom]); | |
var range = copy.range(); | |
copy.range([range[0] - offset, range[1] - offset]); | |
return copy; | |
} | |
}); | |
}; | |
export default Mixin.create({ | |
xScale: scaleProperty('scaleSource.xScale', 'scaleZoomX', 'scaleOffsetX'), | |
yScale: scaleProperty('scaleSource.yScale', 'scaleZoomY', 'scaleOffsetY'), | |
_scaleOffsetX: 0, | |
_scaleOffsetY: 0, | |
_scaleZoomX: 1, | |
_scaleZoomY: 1, | |
scaleZoomX: computed({ | |
get() { | |
return this._scaleZoomX || 1; | |
}, | |
set(key, value) { | |
return this._scaleZoomX = +value || 1; | |
} | |
}), | |
scaleZoomY: computed({ | |
get() { | |
return this._scaleZoomY || 1; | |
}, | |
set(key, value) { | |
return this._scaleZoomY = +value || 1; | |
} | |
}), | |
scaleOffsetX: computed({ | |
get() { | |
return this._scaleOffsetX || 0; | |
}, | |
set(key, value) { | |
return this._scaleOffsetX = +value || 0; | |
} | |
}), | |
scaleOffsetY: computed({ | |
get() { | |
return this._scaleOffsetY || 0; | |
}, | |
set(key, value) { | |
return this._scaleOffsetY = +value || 0; | |
} | |
}), | |
init() { | |
this._super(...arguments); | |
var scaleSource = this.nearestWithProperty('isScaleSource'); | |
this.set('scaleSource', scaleSource); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
const { | |
Component, | |
computed, | |
warn | |
} = Ember; | |
const scaleFactoryProperty = function(axis) { | |
let scaleTypeKey = axis + 'ScaleType'; | |
let powExponentKey = axis + 'PowerExponent'; | |
return computed(scaleTypeKey, powExponentKey, function() { | |
let type = this.get(scaleTypeKey).toLowerCase(); | |
let powExp = this.get(powExponentKey); | |
if (type === 'linear') { | |
return d3.scaleLinear; | |
} | |
else if (type === 'time') { | |
return d3.scaleTime; | |
} | |
else if (type === 'ordinal') { | |
return function() { | |
let scale = d3.scaleOrdinal(); | |
// 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.scalePow().exponent(powExp); | |
}; | |
} | |
else if (type === 'log') { | |
return d3.scaleLog; | |
} | |
else { | |
warn('unknown scale type: ' + type); | |
return d3.scaleLinear; | |
} | |
}); | |
}; | |
const scaleProperty = function(axis) { | |
let scaleFactoryKey = axis + 'ScaleFactory'; | |
let rangeKey = axis + 'Range'; | |
let domainKey = axis + 'Domain'; | |
let scaleTypeKey = axis + 'ScaleType'; | |
let ordinalPaddingKey = axis + 'OrdinalPadding'; | |
let ordinalOuterPaddingKey = axis + 'OrdinalOuterPadding'; | |
return computed(scaleFactoryKey, rangeKey, scaleTypeKey, ordinalPaddingKey, domainKey, ordinalOuterPaddingKey, function() { | |
let scaleFactory = this.get(scaleFactoryKey); | |
let range = this.get(rangeKey); | |
let domain = this.get(domainKey); | |
let scaleType = this.get(scaleTypeKey); | |
let ordinalPadding = this.get(ordinalPaddingKey); | |
let ordinalOuterPadding = this.get(ordinalOuterPaddingKey); | |
let scale = scaleFactory().domain(domain); | |
if (scaleType === 'ordinal') { | |
scale.rangeBands(range, ordinalPadding, ordinalOuterPadding); | |
} else { | |
scale.range(range).clamp(true); | |
} | |
return scale; | |
}); | |
}; | |
const domainProperty = function(axis) { | |
let minKey = axis + 'Min'; | |
let maxKey = axis + 'Max'; | |
return computed(minKey, maxKey, function() { | |
let min = this.get(minKey); | |
let max = this.get(maxKey); | |
return [min, max]; | |
}); | |
}; | |
const minProperty = function(axis) { | |
var _DataExtent = axis + 'DataExtent'; | |
return computed(_DataExtent, function() { | |
return this.get(_DataExtent)[0]; | |
}); | |
}; | |
const maxProperty = function(axis) { | |
var _DataExtent = axis + 'DataExtent'; | |
return computed(_DataExtent, function() { | |
return this.get(_DataExtent)[1]; | |
}); | |
}; | |
export default Component.extend({ | |
tagName: 'svg', | |
classNames: ['rsa-chart'], | |
attributeBindings: ['width', 'height'], | |
isParentChart: true, | |
isScaleSource: true, | |
width: 400, | |
height: 200, | |
paddingTop: 0, | |
paddingRight: 0, | |
paddingBottom: 0, | |
paddingLeft: 0, | |
xOrdinalPadding: 0.1, | |
yOrdinalPadding: 0.1, | |
xOrdinalOuterPadding: 0.1, | |
yOrdinalOuterPadding: 0.1, | |
yAxis: null, | |
xAxis: null, | |
showXAxis: computed.bool('xAxis'), | |
showYAxis: computed.bool('yAxis'), | |
graphics: [], | |
hasData: computed('graphics', function() { | |
return (this.get('graphics').length === 0); | |
}), | |
contentClipPathId: computed('elementId', function() { | |
return this.get('elementId') + '-content-mask'; | |
}), | |
graphX: computed('paddingLeft', 'yAxis.width', function() { | |
let paddingLeft = this.get('paddingLeft'); | |
let yAxisWidth = this.get('yAxis.width') || 0; | |
return paddingLeft + yAxisWidth; | |
}), | |
graphY: computed('paddingTop', function() { | |
return this.get('paddingTop'); | |
}), | |
graphWidth: computed('width', 'paddingRight', 'paddingLeft', 'yAxis.width', function() { | |
var paddingRight = this.get('paddingRight') || 0; | |
var paddingLeft = this.get('paddingLeft') || 0; | |
var yAxisWidth = this.get('yAxis.width') || 0; | |
var width = this.get('width') || 0; | |
return Math.max(0, width - paddingRight - paddingLeft - yAxisWidth); | |
}), | |
graphHeight: computed('height', 'paddingTop', 'paddingBottom', 'xAxis.height', function(){ | |
var paddingTop = this.get('paddingTop') || 0; | |
var paddingBottom = this.get('paddingBottom') || 0; | |
var xAxisHeight = this.get('xAxis.height') || 0; | |
var height = this.get('height') || 0; | |
return Math.max(0, height - paddingTop - paddingBottom - xAxisHeight); | |
}), | |
graphTransform: computed('graphX', 'graphY', function() { | |
var graphX = this.get('graphX'); | |
var graphY = this.get('graphY'); | |
return `translate(${graphX} ${graphY})`; | |
}), | |
didInsertElement() { | |
this._super(...arguments); | |
this.set('svg', this.element); | |
}, | |
registerGraphic: function(graphic) { | |
var graphics = this.get('graphics'); | |
graphics.pushObject(graphic); | |
this.updateExtents(); | |
graphic.on('hasData', this, this.updateExtents); | |
}, | |
unregisterGraphic: function(graphic) { | |
graphic.off('hasData', this, this.updateExtents); | |
var graphics = this.get('graphics'); | |
graphics.removeObject(graphic); | |
}, | |
dataExtents: { | |
xMin: Number.MAX_VALUE, | |
xMax: Number.MIN_VALUE, | |
yMin: Number.MAX_VALUE, | |
yMax: Number.MIN_VALUE | |
}, | |
xDataExtent: computed('dataExtents.xMin','dataExtents.xMax', function() { | |
let { xMin, xMax } = this.get('dataExtents'); | |
return [xMin, xMax]; | |
}), | |
yDataExtent: computed('dataExtents.yMin','dataExtents.yMax', function(){ | |
let { yMin, yMax } = this.get('dataExtents'); | |
return [yMin, yMax]; | |
}), | |
updateExtents() { | |
let graphics = this.get('graphics'); | |
let extents = this.get('dataExtents'); | |
let newExtents = graphics.reduce((a, v) => a.concat(v.get('data')), []).reduce((a, v) => { | |
let x = v.x; | |
let y = v.y; | |
Ember.set(a, 'xMin', a.xMin < x ? a.xMin : x); | |
Ember.set(a, 'xMax', a.xMax > x ? a.xMax : x); | |
Ember.set(a, 'yMin', a.yMin < y ? a.yMin : y); | |
Ember.set(a, 'yMax', a.yMax > y ? a.yMax : y); | |
return a; | |
}, extents); | |
this.set('dataExtents', newExtents); | |
}, | |
xPowerExponent: 3, | |
yPowerExponent: 3, | |
xScaleType: 'linear', | |
yScaleType: 'linear', | |
xScaleFactory: scaleFactoryProperty('x'), | |
yScaleFactory: scaleFactoryProperty('y'), | |
xScale: scaleProperty('x'), | |
yScale: scaleProperty('y'), | |
xMin: minProperty('x'), | |
xMax: maxProperty('x'), | |
yMin: minProperty('y'), | |
yMax: maxProperty('y'), | |
xDomain: domainProperty('x'), | |
yDomain: domainProperty('y'), | |
xRange: computed('graphWidth', function() { | |
return [0, this.get('graphWidth')]; | |
}), | |
yRange: computed('graphHeight', function() { | |
return [this.get('graphHeight'), 0]; | |
}) | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
margin: 12px 16px; | |
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
font-size: 12pt; | |
} | |
.rsa-chart-background { | |
fill: #eee; | |
} | |
.chart-content-background { | |
fill: #f3f3f3; | |
} | |
.chart-line { | |
fill: none; | |
stroke: steelblue; | |
stroke-width: 2; | |
} | |
.chart-x-axis-tick-line, | |
.chart-x-axis-line { | |
stroke: black; | |
stroke-width: 1; | |
} | |
.chart-x-axis-tick text { | |
font-size: 10px; | |
font-family: sans-serif; | |
text-anchor: middle; | |
} | |
.orient-bottom {} | |
.chart-tick-label {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"version": "0.10.5", | |
"EmberENV": { | |
"FEATURES": {} | |
}, | |
"options": { | |
"use_pods": true, | |
"enable-testing": false | |
}, | |
"dependencies": { | |
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js", | |
"ember": "2.7.0", | |
"ember-template-compiler": "2.7.0", | |
"d3": "https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.1/d3.min.js" | |
}, | |
"addons": {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment