Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Last active November 3, 2015 13:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save biovisualize/40ce9f6886ee5011e536 to your computer and use it in GitHub Desktop.
Save biovisualize/40ce9f6886ee5011e536 to your computer and use it in GitHub Desktop.
Backbone + reusable D3 charts
<!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