Skip to content

Instantly share code, notes, and snippets.

@gtb104
Last active August 15, 2016 18:27
Show Gist options
  • Save gtb104/ea2f07966161e1b52a8d2172939d32b8 to your computer and use it in GitHub Desktop.
Save gtb104/ea2f07966161e1b52a8d2172939d32b8 to your computer and use it in GitHub Desktop.
Mostly Declarative D3

Mostly Declarative D3

This is a mostly declarative approach to creating reusable D3 graph.

import Ember from 'ember';
import Shapes from '../lib/chart-shapes';
const {
Component,
computed
} = Ember;
const log = console.log;
export default Component.extend({
tagName: 'path',
classNames: ['chart-area'],
dataIndex: 0,
pathFn: computed('xScale', 'yScale', function() {
const xScale = this.getAttr('xScale');
const yScale = this.getAttr('yScale');
const height = this.getAttr('height');
const xAccessor = (d) => xScale(d.value);
const yAccessor = (d) => yScale(d.count);
return Shapes.createArea(xAccessor, yAccessor, height, d3.curveLinear);
}),
didInsertElement() {
this._super(...arguments);
const index = this.get('dataIndex');
const data = this.getAttr('data')[index];
const pathFn = this.get('pathFn');
this.draw(data, pathFn);
},
didUpdateAttrs() {
this._super(...arguments);
const index = this.get('dataIndex');
const data = this.getAttr('data')[index];
const pathFn = this.get('pathFn');
this.draw(data, pathFn);
},
draw(data, pathFn) {
d3.select(this.element)
.datum(data)
.transition().duration(750)
.attr('d', pathFn);
}
});
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Mostly Declarative D3',
graphData: [
[
{"name":"day","type":"TimeT","value":1412035200000,"count":40802921},
{"name":"day","type":"TimeT","value":1412121600000,"count":57240797},
{"name":"day","type":"TimeT","value":1412208000000,"count":54824229},
{"name":"day","type":"TimeT","value":1412294400000,"count":62621099},
{"name":"day","type":"TimeT","value":1412380800000,"count":60796090},
{"name":"day","type":"TimeT","value":1412467200000,"count":46186237},
{"name":"day","type":"TimeT","value":1412553600000,"count":53948266},
{"name":"day","type":"TimeT","value":1412640000000,"count":63232914},
{"name":"day","type":"TimeT","value":1412726400000,"count":52186213},
{"name":"day","type":"TimeT","value":1412812800000,"count":3251360}
],
[
{"name":"day","type":"TimeT","value":1412035200000,"count":30702921},
{"name":"day","type":"TimeT","value":1412121600000,"count":37040797},
{"name":"day","type":"TimeT","value":1412208000000,"count":32824229},
{"name":"day","type":"TimeT","value":1412294400000,"count":32421099},
{"name":"day","type":"TimeT","value":1412380800000,"count":30796090},
{"name":"day","type":"TimeT","value":1412467200000,"count":36116237},
{"name":"day","type":"TimeT","value":1412553600000,"count":30948266},
{"name":"day","type":"TimeT","value":1412640000000,"count":30232914},
{"name":"day","type":"TimeT","value":1412726400000,"count":32186213},
{"name":"day","type":"TimeT","value":1412812800000,"count":31513600}
]
],
init() {
this._super(...arguments);
Ember.run.later(this, () => {
console.log('ADD DATA!!!!!');
this.set('graphData', [
[
{"name":"day","type":"TimeT","value":1412035200000,"count":40802921},
{"name":"day","type":"TimeT","value":1412121600000,"count":57240797},
{"name":"day","type":"TimeT","value":1412208000000,"count":54824229},
{"name":"day","type":"TimeT","value":1412294400000,"count":62621099},
{"name":"day","type":"TimeT","value":1412380800000,"count":60796090},
{"name":"day","type":"TimeT","value":1412467200000,"count":46186237},
{"name":"day","type":"TimeT","value":1412553600000,"count":53948266},
{"name":"day","type":"TimeT","value":1412640000000,"count":63232914},
{"name":"day","type":"TimeT","value":1412726400000,"count":52186213},
{"name":"day","type":"TimeT","value":1412812800000,"count":3251360},
{"name":"day","type":"TimeT","value":1413030006000,"count":42513605}
],
[
{"name":"day","type":"TimeT","value":1412035200000,"count":30702921},
{"name":"day","type":"TimeT","value":1412121600000,"count":37040797},
{"name":"day","type":"TimeT","value":1412208000000,"count":32824229},
{"name":"day","type":"TimeT","value":1412294400000,"count":32421099},
{"name":"day","type":"TimeT","value":1412380800000,"count":30796090},
{"name":"day","type":"TimeT","value":1412467200000,"count":36116237},
{"name":"day","type":"TimeT","value":1412553600000,"count":30948266},
{"name":"day","type":"TimeT","value":1412640000000,"count":30232914},
{"name":"day","type":"TimeT","value":1412726400000,"count":32186213},
{"name":"day","type":"TimeT","value":1412812800000,"count":31513600},
{"name":"day","type":"TimeT","value":1413030006000,"count":15136050}
]
]);
}, 3000);
}
});
/**
* Computes minimum value. Assumes data is in format [[{}, {}]].
* @param d Array The Array of Arrays containing the data.
* @param accessorFn Function The function run on each object to return
* the value used in computing the minimum.
*/
const min = (d, accessorFn) => d3.min(d.map(_d => d3.min(_d, accessorFn)));
/**
* Computes maximum value. Assumes data is in format [[{}, {}]].
* @param d Array The Array of Arrays containing the data.
* @param accessorFn Function The function run on each object to return
* the value used in computing the maximum.
*/
const max = (d, accessorFn) => d3.max(d.map(d => d3.max(d, accessorFn)));
/**
* Computes the minimum and maximum value for a set of values.
* Assumes data is in format [[{}, {}]].
* @param d Array The Array of Arrays containing the data.
* @param accessorFn Function The function run on each object to return
* the value used in computing the min/max.
* @return An Array containing the minumum and maximum values.
*/
const computeExtent = (d, accessorFn) => [min(d, accessorFn), max(d, accessorFn)];
/**
* Creates a scale for translating a domain value to a range value. I
* @param scaleFn Function A scaling function.
* @param domain Array The domain extent.
* @param range Array The range extent.
* @return A scaling function.
*/
const createScale = (scaleFn, domain, range) => scaleFn().domain(domain).range(range).clamp(true);
export default { computeExtent, createScale };
/**
* Acceptable curve values
* d3.curveBasis
* d3.curveBasisClosed
* d3.curveBasisOpen
* d3.curveBundle
* d3.curveCardinal
* d3.curveCardinalClosed
* d3.curveCardinalOpen
* d3.curveCatmullRom
* d3.curveCatmullRomClosed
* d3.curveCatmullRomOpen
* d3.curveLinear
* d3.curveLinearClosed
* d3.curveMonotoneX
* d3.curveMonotoneY
* d3.curveNatural
* d3.curveStep
* d3.curveStepAfter
* d3.curveStepBefore
*/
const createLine = (xAccessorFn, yAccessorFn, curve) => {
return d3.line().x(xAccessorFn).y(yAccessorFn).curve(curve);
};
const createArea = (xAccessorFn, yAccessorFn, height, curve) => {
return d3.area().x(xAccessorFn).y0(height).y1(yAccessorFn).curve(curve);
};
export default { createLine, createArea }
import Ember from 'ember';
import Shapes from '../lib/chart-shapes';
const {
Component,
computed
} = Ember;
const log = console.log;
export default Component.extend({
tagName: 'path',
classNames: ['chart-line'],
lineFn: computed('xScale', 'yScale', function() {
const xScale = this.getAttr('xScale');
const yScale = this.getAttr('yScale');
const xAccessor = (d) => xScale(d.value);
const yAccessor = (d) => yScale(d.count);
return Shapes.createLine(xAccessor, yAccessor, d3.curveLinear);
}),
didInsertElement() {
this._super(...arguments);
const index = this.get('dataIndex');
const data = this.getAttr('data')[index];
const lineFn = this.get('lineFn');
this.draw(data, lineFn);
},
didUpdateAttrs() {
this._super(...arguments);
const index = this.get('dataIndex');
const data = this.getAttr('data')[index];
const lineFn = this.get('lineFn');
this.draw(data, lineFn);
},
draw(data, lineFn) {
d3.select(this.element)
.datum(data)
.transition().duration(750)
.attr('d', lineFn);
}
});
import Ember from 'ember';
import Scales from '../lib/chart-scale';
const {
Component,
computed
} = Ember;
const log = console.log;
const calcGraphWidth = (width, marginLeft, marginRight) => width - marginLeft - marginRight;
const calcGraphHeight = (height, marginTop, marginBottom) => height - marginTop - marginBottom;
export default Component.extend({
width: 400,
height: 150,
// Adjust margins so that the axes fit
margin: {top: 5, bottom: 30, left: 30, right: 0},
graphWidth: computed('width', 'margin', function() {
const width = this.get('width');
const margin = this.get('margin');
return calcGraphWidth(width, margin.left, margin.right);
}),
graphHeight: computed('height', 'margin', function() {
const height = this.get('height');
const margin = this.get('margin');
return calcGraphWidth(height, margin.top, margin.bottom);
}),
xScale: computed('graphWidth', 'data', function() {
const data = this.getAttr('data');
const domain = Scales.computeExtent(data, (d) => d.value);
const graphWidth = this.get('graphWidth');
const range = [0, graphWidth];
return Scales.createScale(d3.scaleTime, domain, range);
}),
yScale: computed('graphHeight', 'data', function() {
const data = this.getAttr('data');
const domain = Scales.computeExtent(data, (d) => d.count);
const graphHeight = this.get('graphHeight');
const range = [graphHeight, 0];
return Scales.createScale(d3.scaleLinear, domain, range);
})
});
<svg width="{{width}}" height="{{height}}">
<g transform="translate({{margin.left}},{{margin.top}})">
<rect class="chart-background"
width="{{graphWidth}}"
height="{{graphHeight}}">
</rect>
{{yield data xScale yScale graphHeight}}
</g>
</svg>
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 12pt;
}
.chart-background {
fill: #f9f9f9;
}
.chart-line {
fill: none;
stroke: steelblue;
stroke-width: 2;
}
.chart-area {
fill: lightsteelblue;
fill-opacity: 0.25;
stroke: lightsteelblue;
stroke-width: 2;
}
.tick line,
.tick text {
shape-rendering: crispEdges;
}
<h1>{{appName}}</h1>
{{#rsa-chart data=graphData as |data xScale yScale graphHeight|}}
{{line-series data=data dataIndex=0 xScale=xScale yScale=yScale}}
{{area-series data=data dataIndex=1 height=graphHeight xScale=xScale yScale=yScale}}
{{x-axis height=graphHeight scale=xScale rotation=-25}}
{{y-axis scale=yScale}}
{{/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": {}
}
import Ember from 'ember';
const {
Component,
computed
} = Ember;
/**
* The x axis component.
* {{x-axis height=graphHeight xScale=xScale rotation=-25}}
*/
export default Component.extend({
tagName: 'g',
attributeBindings: ['transform'],
classNames: ['axis'],
transform: computed('height', function() {
const height = this.getAttr('height');
return `translate(0,${height})`;
}),
didInsertElement() {
this._super(...arguments);
const scale = this.getAttr('scale');
const axisFn = d3.axisBottom(scale);
const axis = d3.select(this.element).call(axisFn);
const rotation = this.getAttr('rotation');
if (rotation) {
axis.selectAll('text')
.attr('transform', `rotate(${rotation})`)
.style('text-anchor', 'end');
}
},
didUpdateAttrs() {
this._super(...arguments);
const scale = this.getAttr('scale');
this.update(scale);
},
update(scale) {
const rotation = this.getAttr('rotation');
const axis = d3.select(this.element)
.transition().duration(750)
.call(d3.axisBottom(scale));
if (rotation) {
axis.selectAll('text')
.attr('transform', `rotate(${rotation})`)
.style('text-anchor', 'end');
}
},
});
import Ember from 'ember';
const {
Component,
computed
} = Ember;
/**
* The y axis component.
* {{y-axis yScale=yScale}}
*/
export default Component.extend({
tagName: 'g',
classNames: ['axis'],
didInsertElement() {
this._super(...arguments);
const scale = this.getAttr('scale');
const axisFn = d3.axisLeft(scale).ticks(6, 's');
const axis = d3.select(this.element).call(axisFn);
const rotation = this.getAttr('rotation');
if (rotation) {
axis.selectAll('text')
.attr('transform', `rotate(${rotation})`)
.style('text-anchor', 'end');
}
},
didUpdateAttrs() {
this._super(...arguments);
const scale = this.getAttr('scale');
this.update(scale);
},
update(scale) {
const rotation = this.getAttr('rotation');
const axis = d3.select(this.element)
.transition().duration(750)
.call(d3.axisLeft(scale).ticks(6, 's'));
if (rotation) {
axis.selectAll('text')
.attr('transform', `rotate(${rotation})`)
.style('text-anchor', 'end');
}
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment