This is a non-declarative approach to creating reusable D3 graph.
The main method of reusability will be a library of modules used to create specific portions of a D3 graph.
import Ember from 'ember'; | |
export default Ember.Controller.extend({ | |
appName: 'Non 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); | |
} | |
}); |
const min = (d, accessorFn) => d3.min(d.map(_d => d3.min(_d, accessorFn))); | |
const max = (d, accessorFn) => d3.max(d.map(d => d3.max(d, accessorFn))); | |
const extent = (d, accessorFn) => [min(d, accessorFn), max(d, accessorFn)]; | |
const computeExtent = (data, accessorFn) => { | |
return extent(data, accessorFn); | |
}; | |
const createScale = (scaleFn, domain, range) => { | |
return 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 } |
const calcGraphWidth = (width, marginLeft, marginRight) => width - marginLeft - marginRight; | |
const calcGraphHeight = (height, marginTop, marginBottom) => height - marginTop - marginBottom; | |
const baseSvg = (element, width, height, margin) => { | |
const svg = d3.select(element).append('svg') | |
.attr('width', width) | |
.attr('height', height) | |
.append('g') | |
.attr('transform', `translate(${margin.left},${margin.top})`); | |
svg.append('rect') | |
.attr('width', width - margin.left - margin.right) | |
.attr('height', height - margin.bottom - margin.top) | |
.attr('class', 'chart-background'); | |
return svg; | |
}; | |
export default { baseSvg, calcGraphWidth, calcGraphHeight }; |
import Ember from 'ember'; | |
import Chart from '../lib/chart'; | |
import Scales from '../lib/chart-scale'; | |
import Shapes from '../lib/chart-shapes'; | |
const { | |
Component, | |
computed | |
} = Ember; | |
const log = console.log; | |
export default Component.extend({ | |
width: 400, | |
height: 150, | |
showXaxis: true, | |
showYaxis: false, | |
// 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 Chart.calcGraphWidth(width, margin.left, margin.right); | |
}), | |
graphHeight: computed('height', 'margin', function() { | |
const height = this.get('height'); | |
const margin = this.get('margin'); | |
return Chart.calcGraphWidth(height, margin.top, margin.bottom); | |
}), | |
xScale: computed('graphWidth', 'data', function() { | |
const data = this.getAttr('data'); | |
const domain = Scales.computeExtent(data, (d) => d.x); | |
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.y); | |
const graphHeight = this.get('graphHeight'); | |
const range = [graphHeight, 0]; | |
return Scales.createScale(d3.scaleLinear, domain, range); | |
}), | |
xAxisTransform: computed('graphHeight', function() { | |
const graphHeight = this.get('graphHeight'); | |
return `translate(0,${graphHeight})`; | |
}), | |
lineFn: computed('xScale', 'yScale', function() { | |
const xScale = this.get('xScale'); | |
const yScale = this.get('yScale'); | |
const xAccessor = (d) => xScale(d.x); | |
const yAccessor = (d) => yScale(d.y); | |
return Shapes.createLine(xAccessor, yAccessor, d3.curveLinear); | |
}), | |
didUpdateAttrs(attrs) { | |
this._super(...arguments); | |
const data = this.getAttr('data'); | |
const lineFn = this.get('lineFn'); | |
const xScale = this.get('xScale'); | |
const yScale = this.get('yScale'); | |
this.redrawPath(data, lineFn); | |
if (this.get('showXaxis')) { | |
this.redrawXaxis(xScale); | |
} | |
if (this.get('showYaxis')) { | |
this.redrawYaxis(yScale); | |
} | |
}, | |
didInsertElement() { | |
this._super(...arguments); | |
const el = this.element; | |
const width = this.get('width'); | |
const height = this.get('height'); | |
const margin = this.get('margin'); | |
const svg = Chart.baseSvg(el, width, height, margin); | |
const lineFn = this.get('lineFn'); | |
const xScale = this.get('xScale'); | |
const yScale = this.get('yScale'); | |
const data = this.getAttr('data'); | |
const path = svg.append('path') | |
.data(data) | |
.attr('class', 'chart-line') | |
.attr('d', lineFn); | |
if (this.get('showXaxis')) { | |
const xAxis = d3.axisBottom(xScale); | |
const xAxisTransform = this.get('xAxisTransform'); | |
svg.append('g') | |
.attr('class', 'axis xAxis') | |
.attr('transform', xAxisTransform) | |
.call(xAxis) | |
.selectAll('text') | |
.attr('transform', 'rotate(-25)') | |
.style('text-anchor', 'end'); | |
} | |
if (this.get('showYaxis')) { | |
const yAxis = d3.axisLeft(yScale); | |
svg.append('g') | |
.attr('class', 'axis yAxis') | |
.call(yAxis); | |
} | |
}, | |
redrawPath(data, lineFn) { | |
d3.select('.chart-line') | |
.data(data) | |
.transition().duration(750) | |
.attr('d', lineFn); | |
}, | |
redrawXaxis(xScale) { | |
d3.select('.xAxis') | |
.transition().duration(750) | |
.call(d3.axisBottom(xScale)) | |
.selectAll('text') | |
.attr('transform', 'rotate(-25)') | |
.style('text-anchor', 'end'); | |
}, | |
redrawYaxis(yScale) { | |
d3.select('.yAxis') | |
.transition().duration(750) | |
.call(d3.axisLeft(yScale)); | |
} | |
}); |
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; | |
stroke: steelblue; | |
stroke-width: 2; | |
} | |
.tick line, | |
.tick text { | |
shape-rendering: crispEdges; | |
} |
{ | |
"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": {} | |
} |