This is a mostly declarative approach to creating reusable D3 graph.
Last active
August 15, 2016 18:27
-
-
Save gtb104/ea2f07966161e1b52a8d2172939d32b8 to your computer and use it in GitHub Desktop.
Mostly 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 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); | |
} | |
}); |
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: '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); | |
} | |
}); |
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
/** | |
* 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 }; |
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
/** | |
* 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 } |
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 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); | |
} | |
}); |
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 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); | |
}) | |
}); |
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; | |
} | |
.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; | |
} |
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": {} | |
} |
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; | |
/** | |
* 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'); | |
} | |
}, | |
}); |
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; | |
/** | |
* 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