Last active
November 3, 2015 13:44
-
-
Save biovisualize/40ce9f6886ee5011e536 to your computer and use it in GitHub Desktop.
Backbone + reusable D3 charts
This file contains 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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> | |
<title></title> | |
<script type='text/javascript' src="http://d3js.org/d3.v3.js"></script> | |
<script type='text/javascript' src="http://code.jquery.com/jquery-1.8.3.min.js"></script> | |
<script type='text/javascript' src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script> | |
<script type='text/javascript' src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script> | |
<style> | |
path.domain { | |
fill: none; | |
stroke: black; | |
} | |
.mark { | |
fill: skyblue; | |
} | |
.tick line { | |
stroke: black; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="control"> | |
<button class="update-data">Update Data</button> | |
<button class="update-config">Update Config</button> | |
</div> | |
<div class="chart" id="chart1"></div> | |
<div class="chart" id="chart2"></div> | |
<script> | |
// Chart module | |
///////////////////////////////////// | |
d3.custom = {}; | |
d3.custom.chart = function module(){ | |
var config = { | |
margin: {top: 20, right: 20, bottom: 40, left: 40}, | |
width: 500, | |
height: 500, | |
geometry: 'bar' | |
}; | |
var privateConfig = { | |
chartW: null, | |
chartH: null, | |
data: null, | |
scaleX: null, | |
scaleY: null | |
}; | |
var svg, dispatch = d3.dispatch('customHover'); | |
function exports(_selection){ | |
_selection.each(function(_data){ | |
var chartW = config.width - config.margin.left - config.margin.right, | |
chartH = config.height - config.margin.top - config.margin.bottom; | |
var scaleX = d3.scale.ordinal() | |
.domain(_data.x.map(function(d, i){ return i; })) | |
.rangeRoundBands([0, chartW], .1); | |
// .rangePoints([0, chartW], .1); | |
var scaleY = d3.scale.linear() | |
.domain([0, d3.max(_data.y, function(d, i){ return d; })]) | |
.range([chartH, 0]); | |
privateConfig = { | |
chartW: chartW, | |
chartH: chartH, | |
data: _data, | |
scaleX: scaleX, | |
scaleY: scaleY | |
}; | |
var xAxis = d3.svg.axis() | |
.scale(scaleX) | |
.orient('bottom'); | |
var yAxis = d3.svg.axis() | |
.scale(scaleY) | |
.orient('left'); | |
if(!svg){ | |
svg = d3.select(this) | |
.append('svg') | |
.classed('chart', true); | |
var container = svg.append('g').classed('container-group', true); | |
container.append('g').classed('chart-group', true); | |
container.append('g').classed('x-axis-group axis', true); | |
container.append('g').classed('y-axis-group axis', true); | |
} | |
svg.transition().attr({width: config.width, height: config.height}) | |
svg.select('.container-group') | |
.attr({transform: 'translate(' + config.margin.left + ',' + config.margin.top + ')'}); | |
svg.select('.x-axis-group.axis') | |
.attr({transform: 'translate(0,' + (chartH) + ')'}) | |
.transition() | |
.call(xAxis); | |
svg.select('.y-axis-group.axis') | |
.transition() | |
.call(yAxis); | |
if(config.geometry === 'bar') exports.renderBars(); | |
if(config.geometry === 'line') exports.renderLines(); | |
}); | |
} | |
exports.renderLines = function(){ | |
var barW = privateConfig.scaleX.rangeBand(); | |
var radius = 5; | |
var bars = svg.select('.chart-group') | |
.selectAll('.mark') | |
.data(privateConfig.data.y); | |
bars.enter().append('circle') | |
.classed('mark', true) | |
.attr({ | |
cx: privateConfig.chartW, | |
cy: function(d, i){ return privateConfig.scaleY(d); }, | |
}) | |
.on('mouseover', dispatch.customHover); | |
bars.transition() | |
.attr({ | |
cx: function(d, i){ return privateConfig.scaleX(i) + barW/2; }, | |
// cx: function(d, i){ return privateConfig.scaleX(i); }, | |
cy: function(d, i){ return privateConfig.scaleY(d); }, | |
r: radius | |
}); | |
bars.exit().transition().style({opacity: 0}).remove(); | |
return this; | |
}; | |
exports.renderBars = function(){ | |
var barW = privateConfig.scaleX.rangeBand(); | |
var bars = svg.select('.chart-group') | |
.selectAll('.mark') | |
.data(privateConfig.data.y); | |
bars.enter().append('rect') | |
.classed('mark', true) | |
.attr({ | |
x: privateConfig.chartW, | |
y: function(d, i){ return privateConfig.scaleY(d); } | |
}) | |
.on('mouseover', dispatch.customHover); | |
bars.transition() | |
.attr({ | |
x: function(d, i){ return privateConfig.scaleX(i); }, | |
y: function(d, i){ return privateConfig.scaleY(d); }, | |
width: barW, | |
height: function(d, i){ return privateConfig.chartH - privateConfig.scaleY(d); } | |
}); | |
bars.exit().transition().style({opacity: 0}).remove(); | |
return this; | |
}; | |
exports.config = function(_newConfig){ | |
if(!arguments.length) return config; | |
for(var x in _newConfig) if(x in config) config[x] = _newConfig[x]; | |
return this; | |
}; | |
exports.getPrivateConfig = function(){ //useful for unit tests | |
return privateConfig; | |
}; | |
d3.rebind(exports, dispatch, 'on'); | |
return exports; | |
}; | |
// Chart view | |
///////////////////////////////////// | |
var ChartView = Backbone.View.extend({ | |
chart: null, | |
chartSelection: null, | |
initialize: function(){ | |
this.model.get('data').bind('change', this.render, this); | |
this.model.get('config').bind('change', this.update, this); | |
this.chart = d3.custom.chart(); | |
this.chart.config(this.model.get('config').toJSON()); | |
this.chart.on('customHover', function(d, i){ | |
//console.log('hover', d, i); | |
}); | |
this.render(); | |
}, | |
render: function(){ | |
this.chartSelection = d3.select(this.el) | |
.datum(this.model.get('data').attributes) | |
.call(this.chart); | |
}, | |
update: function(){ | |
this.chartSelection.call(this.chart.config(this.model.get('config').toJSON())); | |
} | |
}); | |
// Buttons view | |
///////////////////////////////////// | |
var WidgetView = Backbone.View.extend({ | |
el: ".control", | |
events: { | |
"click .update-data": "updateData", | |
"click .update-config": "updateConfig" | |
}, | |
updateData: function(){ | |
var that = this | |
var newData = d3.range(this._randomInt(10)).map(function(d, i){ return that._randomInt(100); }); | |
this.model.get('data').set({x: newData.map(function(d, i){ return i; }), y: newData}); | |
}, | |
updateConfig: function(){ | |
var newConfig = {width: this._randomInt(600, 100)}; | |
this.model.get('config').set(newConfig); | |
}, | |
_randomInt: function(_maxSize, _minSize){ | |
var minSize = _minSize || 1; | |
return ~~(Math.random() * (_maxSize - minSize)) + minSize; | |
} | |
}); | |
// Chart data | |
///////////////////////////////////// | |
var ChartData = Backbone.Model.extend({ | |
defaults: { | |
x: [1, 2, 3, 4], | |
y: [10, 20, 30, 40] | |
} | |
}); | |
// Chart config | |
///////////////////////////////////// | |
var ChartConfig = Backbone.Model.extend({ | |
defaults: { | |
height: 300, | |
width: 400 | |
} | |
}); | |
// Usage | |
///////////////////////////////////// | |
var chartData = new ChartData(); | |
var barChartConfig = new ChartConfig(); | |
var barChartModel = new Backbone.Model().set({data: chartData, config: barChartConfig}); | |
var barChartWidgets = new WidgetView({model: barChartModel}); | |
var barChartView = new ChartView({model: barChartModel, el: '#chart1'}) | |
var lineChartConfig = new ChartConfig({height: 200, width: 400, geometry: 'line'}); | |
var lineChartModel = new Backbone.Model().set({data: chartData, config: lineChartConfig}); | |
var lineChartView = new ChartView({model: lineChartModel, el: '#chart2'}) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment