Skip to content

Instantly share code, notes, and snippets.

@gtb104
Last active August 9, 2016 15:23
Show Gist options
  • Save gtb104/d2b8d443109d797fa9923defd2071bf8 to your computer and use it in GitHub Desktop.
Save gtb104/d2b8d443109d797fa9923defd2071bf8 to your computer and use it in GitHub Desktop.
Declarative D3 v2

Declarative D3 v2

A version which uses a Model(which coordinates multiple scales), and the parent creates the axes.

import Ember from 'ember';
const {
Component,
computed
} = Ember;
const log = console.log;
export default Component.extend({
tagName: 'g',
classNames: ['graph-content'],
attributeBindings: ['clip-path', 'transform'],
'clip-path': computed('model.clipPathId', function() {
var clipPathId = this.get('model.clipPathId');
return `url(#${clipPathId})`;
}),
transform: computed('model.graphTransform', function() {
return this.get('model.graphTransform');
}),
textX: computed('model.graphWidth', function() {
return this.get('model.graphWidth') * 0.5;
}),
textY: computed('model.graphHeight', function() {
return this.get('model.graphHeight') * 0.5;
}),
init() {
this._super(...arguments);
},
});
<rect x=0 y=0 class="chart-content-background" width="{{model.graphWidth}}" height="{{model.graphHeight}}"></rect>
{{#unless model.hasData}}
<text class="no-data" x={{textX}} y={{textY}} dx="-1.75em" dy="0.71em">No data</text>
{{/unless}}
{{yield}}
import Ember from 'ember';
const {
Component,
computed,
run
} = Ember;
const log = console.log;
export default Component.extend({
tagName: 'g',
seriesToRender: 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);
}),
didReceiveAttrs(attrs) {
this._super(...arguments);
if (attrs.newAttrs && attrs.oldAttrs) {
let series = this.get('seriesToRender');
let data = this.get('data');
let line = this.get('lineFn');
this.update(data[series], line);
}
},
didInsertElement() {
this._super(...arguments);
let series = this.get('seriesToRender');
let data = this.get('data');
let line = this.get('lineFn');
if (data && data[series]) {
this.draw(data[series], line);
} else {
this.draw({}, line);
}
},
draw(data, line) {
let path = d3.select(this.element).append('path')
.datum(data)
.attr('class', 'chart-line')
.attr('d', line);
this.set('path', path);
},
update(data, line) {
this.get('path')
.datum(data)
.transition().duration(750)
.attr('d', line);
}
});
import Ember from 'ember';
const {
computed,
warn
} = Ember;
const log = console.log;
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 Ember.Mixin.create({
seriesData: (function() {
let _data = null;
return computed({
get() {
return _data;
},
set(key, value) {
_data = value;
this.updateExtents(value);
return value;
}
});
})(),
clipPathId: computed(function() {
return null;
}),
graphTransform: computed(function() {
return null;
}),
dataExtents: (function() {
let _extents = {
xMin: Number.MAX_VALUE,
xMax: Number.MIN_VALUE,
yMin: Number.MAX_VALUE,
yMax: Number.MIN_VALUE
};
return computed({
get(key) {
return _extents;
},
set(key, value) {
//log(`${key}.set()`, value);
_extents = value;
return value;
}
});
})(),
updateExtents(dataObj) {
let extents = this.get('dataExtents');
let series = Object.keys(dataObj);
let newExtents = series.reduce((a, v) => a.concat(dataObj[v]), []).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);
},
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];
}),
xOrdinalPadding: 0.1,
yOrdinalPadding: 0.1,
xOrdinalOuterPadding: 0.1,
yOrdinalOuterPadding: 0.1,
xPowerExponent: 3,
yPowerExponent: 3,
xScaleType: 'linear',
yScaleType: 'linear',
xScaleFactory: scaleFactoryProperty('x'),
yScaleFactory: scaleFactoryProperty('y'),
xScale: scaleProperty('x'),
yScale: scaleProperty('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];
}),
xMin: minProperty('x'),
xMax: maxProperty('x'),
yMin: minProperty('y'),
yMax: maxProperty('y'),
hasData: computed.bool('seriesData'),
actions: {
updateDomain(data) {
console.log('updateDomain() called', data);
}
}
});
import Ember from 'ember';
import ChartData from '../models/chart-data';
const data = {
A: [
{x:1469385570980, y:3},
{x:1469731166074, y:6},
{x:1469903961473, y:5},
{x:1470076750392, y:2}
],
B: [
{x:1469385570980, y:4},
{x:1469731166074, y:3.5},
{x:1469903961473, y:3},
{x:1470076750392, y:4}
]
};
export default Ember.Controller.extend({
appName: 'Declarative D3 v2',
init() {
this._super(...arguments);
this.set('model', ChartData.create());
//this.set('model', ChartData.create({seriesData: data}));
Ember.run.later(this, () => {
console.log('ADD DATA!!!!!');
this.set('model.seriesData', data);
}, 1000);
Ember.run.later(this, () => {
console.log('ADD DATA!!!!!');
let now = new Date().getTime();
let copy = Object.assign({}, data);
copy.A.push({x: now, y: 4});
copy.B.push({x: now, y: 7});
this.set('model.seriesData', copy);
}, 3000);
}
});
import Ember from 'ember';
import ChartMixin from '../chart-model-mixin';
const {
Object
} = Ember;
export default Object.extend(ChartMixin, {
// Overrides
xScaleType: 'time'
});
import Ember from 'ember';
const {
Component,
computed
} = Ember;
const log = console.log;
export default Component.extend({
tagName: 'svg',
classNames: ['rsa-chart'],
attributeBindings: ['width', 'height'],
width: 400,
height: 200,
marginTop: 10,
marginRight: 20,
marginBottom: 10,
marginLeft: 20,
xTickCount: computed(function() {
let width = this.get('graphWidth');
return width / 80;
}),
showAxisY: false,
yAxis: null,
yAxisWidth: 20,
showAxisX: true,
xAxis: computed('model.xScale', function() {
let scale = this.get('model.xScale');
let t = this.get('xTickCount');
return d3.axisBottom(scale).ticks(t);
}),
xAxisHeight: 20,
clipPathId: computed('elementId', function() {
let id = this.get('elementId') + '-content-mask';
this.set('model.clipPathId', id);
return id;
}),
graphX: computed('marginLeft', 'yAxis.width', function() {
let marginLeft = this.get('marginLeft');
let yAxisWidth = this.get('yAxis.width') || 0;
return marginLeft + yAxisWidth;
}),
graphY: computed('marginTop', function() {
return this.get('marginTop');
}),
graphWidth: computed('width', 'marginRight', 'marginLeft', 'yAxis.width', function() {
let marginRight = this.get('marginRight') || 0;
let marginLeft = this.get('marginLeft') || 0;
let yAxisWidth = this.get('yAxis.width') || 0;
let width = this.get('width') || 0;
let w = Math.max(0, width - marginRight - marginLeft - yAxisWidth);
this.set('model.graphWidth', w);
return w;
}),
graphHeight: computed('height', 'marginTop', 'marginBottom', function(){
let marginTop = this.get('marginTop') || 0;
let marginBottom = this.get('marginBottom') || 0;
let xAxisHeight = this.get('showAxisX') ? this.get('xAxisHeight') : 0;
let height = this.get('height') || 0;
let h = Math.max(0, height - marginTop - marginBottom - xAxisHeight);
this.set('model.graphHeight', h);
return h;
}),
graphTransform: computed('graphX', 'graphY', function() {
let graphX = this.get('graphX');
let graphY = this.get('graphY');
let t = `translate(${graphX} ${graphY})`;
this.set('model.graphTransform', t);
return t;
}),
seriesDataWatcher: Ember.observer('model.seriesData', function() {
this.updateAxes();
}),
init() {
this._super(...arguments);
this.get('graphTransform');
},
didInsertElement() {
this._super(...arguments);
this.set('svg', this.element);
if (this.get('showAxisX')) {
let axis = this.get('xAxis');
let y = this.get('graphHeight')+10;
d3.select('.xAxis')
.attr('transform', 'translate(20,' + y + ')')
.call(axis);
}
},
updateAxes() {
if (this.get('showAxisX')) {
let axis = this.get('xAxis');
d3.select('.xAxis')
.transition().duration(750)
.call(axis);
}
}
});
<defs>
<clipPath id="{{clipPathId}}">
<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>
{{#if showAxisX}}
<g class="axis xAxis"></g>
{{/if}}
{{#if showAxisY}}
<g class="axis yAxis"></g>
{{/if}}
{{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;
}
.no-data {
font-size: 2em;
fill: #ddd;
}
<h1>{{appName}}</h1>
{{#rsa-chart model=model}}
{{#chart-content model=model}}
{{chart-line
data=model.seriesData
seriesToRender='A'
xScale=model.xScale
yScale=model.yScale
}}
{{chart-line
data=model.seriesData
seriesToRender='B'
xScale=model.xScale
yScale=model.yScale
}}
{{/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