Skip to content

Instantly share code, notes, and snippets.

@gtb104
Last active August 9, 2016 16:55
Show Gist options
  • Save gtb104/959ddc4ce89d5094ba9f73be33538e33 to your computer and use it in GitHub Desktop.
Save gtb104/959ddc4ce89d5094ba9f73be33538e33 to your computer and use it in GitHub Desktop.
Declarative D3
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);
},
});
<rect x=0 y=0 class="chart-content-background" width="{{width}}" height="{{height}}"></rect>
{{#unless graph.hasData}}
<text x=0 y=0>No data</text>
{{/unless}}
{{yield}}
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'));
}
});
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})`;
})
});
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);
});
})
});
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);
}
});
<line class="chart-x-axis-line" x1="0" y1="0" x2="{{width}}" y2="0"></line>
{{#each ticks as |tick|}}
<g class="chart-x-axis-tick">
{{#if hasBlock}}
{{#chart-tick-label x=tick.x y=tick.labely}}
{{yield tick}}
{{/chart-tick-label}}
{{else}}
{{#chart-tick-label x=tick.x y=tick.labely}}
<text>{{tick.value}}</text>
{{/chart-tick-label}}
{{/if}}
<line class="chart-x-axis-tick-line" x1="{{tick.x}}" y1="{{tick.y1}}" x2="{{tick.x}}" y2="{{tick.y2}}"></line>
</g>
{{/each}}
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);
}
});
import Ember from 'ember';
export default Ember.Mixin.create({
graph: null,
init() {
this._super(...arguments);
var graph = this.nearestWithProperty('isParentChart');
this.set('graph', graph);
}
});
import Ember from 'ember';
const format = d3.timeFormat('%m/%d');
export function formatDate([date]) {
return format(date);
}
export default Ember.Helper.helper(formatDate);
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);
}
});
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];
})
});
<defs>
<clipPath id="{{contentClipPathId}}">
<rect x=0 y=0 width="{{graphWidth}}" height="{{graphHeight}}"></rect>
</clipPath>
</defs>
<rect class="rsa-chart-background" x=0 y=0 width="{{width}}" height="{{height}}"></rect>
{{yield}}
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 {}
<h1>{{appName}}</h1>
{{#rsa-chart xScaleType='time' paddingTop=10 paddingLeft=10 paddingRight=10 paddingBottom=10}}
{{!--
{{#chart-x-axis tickCount=4 as |tick|}}
<text>{{format-date tick.value}}</text>
{{/chart-x-axis}}
--}}
{{chart-x-axis-d3 tickCount=4}}
{{#chart-content}}
{{chart-line data=graphData}}
{{/chart-content}}
{{/rsa-chart}}
{
"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