Skip to content

Instantly share code, notes, and snippets.

@milroc milroc/README.md

Last active May 3, 2016
Embed
What would you like to do?
bar + sum: d3.js & backbone.js

This is a six part series, taking you through stages of designing and creating reusable visualizations with d3.js

All visualizations have the same functionality, showcase the individual points with a bar chart and sum up the selected bars.

Part 5. This is showcasing the power of combining Backbone.js with d3.js and how to bind them together creating an abstraction between the visualization (the bar chart) and the rest of the application.

These are examples created for a talk (slides and video).

Cheers,

Miles @milr0c

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="http://littlesparkvt.com/flatstrap/assets/css/bootstrap.css"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script src="http://code.jquery.com/jquery-2.0.0.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="src.js"></script>
</head>
<body>
<div class="row">
<div class="span2" id="helper"><button class="btn btn-success" id="update">update</button></div>
<div class="span2" id="sum"></div>
</div>
<div class="row">
<div id="chart"></div>
</div>
<script type="text/javascript">
// Models
var Point = Backbone.Model.extend({
defaults: {
x: 0,
y: 0
},
type: "point",
});
var Sum = Backbone.Model.extend({
defaults: {
sum: 0
}
});
// Collections
var Data = Backbone.Collection.extend({
model: Point,
extent: [0,0],
x: d3.scale.ordinal(),
sum: new Sum(),
initialize: function() {
this.fetch();
},
total: function() {
var total = 0;
this.forEach(function(d) {
var xVal = d.get('x'),
yVal = d.get('y');
if (this.extent[0] <= this.x(xVal) && this.x(xVal) + this.x.rangeBand() <= this.extent[1]) {
total += yVal;
}
}, this);
this.sum.set('sum', total);
},
fetch: function() {
this.randomize();
this.extent = [0, 0];
this.total();
},
randomize: function(n, y) {
if (arguments.length < 2) y = 400;
if (!arguments.length) n = 20;
var i = 0;
this.set(d3.range(~~(Math.random()*n) + 1).map(function(d, i) { return new Point({x: ++i, y: ~~(Math.random()*y)});
}));
},
});
// Views
var BarChart = Backbone.View.extend({
el: "#chart",
initialize: function() {
this.collection.on("change reset add remove", this.render, this);
var bar = charts.bar();
var collection = this.collection;
bar.on('brush', function(data) {
collection.extent = d3.event.target.extent(),
collection.x = bar.x();
collection.total();
});
bar.on('brushend', function(data) {
collection.extent = d3.event.target.extent(),
collection.x = bar.x();
collection.total();
});
bar.xValue(function(d) { return d.get('x'); });
bar.yValue(function(d) { return d.get('y'); });
this.bar = bar;
this.render();
},
render: function() {
d3.select(this.el)
.datum(this.collection.models)
.call(this.bar);
},
});
var HelperView = Backbone.View.extend({
el: "#helper",
events: {
"click #update": "update",
},
update: function() {
this.collection.fetch();
}
});
var SumView = Backbone.View.extend({
el: "#sum",
initialize: function() {
_.bindAll(this);
this.model.bind('change', this.render);
this.render();
},
render: function() {
d3.select(this.$el.selector)
.text('TOTAL: ' + this.model.get('sum'));
}
});
// Main
var dataArr = new Data();
var barView = new BarChart({ collection: dataArr });
var helper = new HelperView({ collection: dataArr });
var sum = new SumView({ model: dataArr.sum });
</script>
</body>
</html>
charts = {};
charts.bar = function() {
// basic data
var margin = {top: 10, bottom: 20, left: 0, right: 0},
width = 400,
height = 400,
// accessors
xValue = function(d) { return d.x; },
yValue = function(d) { return d.y; },
// chart underpinnings
brush = d3.svg.brush(),
xAxis = d3.svg.axis().orient('bottom'),
yAxis = d3.svg.axis().orient('left'),
x = d3.scale.ordinal(),
y = d3.scale.linear(),
// chart enhancements
elastic = {
margin: true,
x: true,
y: true
},
convertData = true,
duration = 500,
formatNumber = d3.format(',d');
function render(selection) {
selection.each(function(data) {
// setup the basics
if (elastic.margin) margin.left = formatNumber(d3.max(data, function(d) { return d.y; })).length * 13;
var w = width - margin.left - margin.right,
h = height - margin.top - margin.bottom;
// if needed convert the data
if (convertData) {
data = data.map(function(d, i) {
return {
x: xValue.call(data, d, i),
y: yValue.call(data, d, i)
};
});
}
// set scales
if (elastic.x) x.domain(data.map(function(d) { return d.x; }));
if (elastic.y) y.domain([0, d3.max(data, function(d) { return d.y; })]);
x.rangeRoundBands([0, w], .1);
y.range([h, 0]);
// reset axes and brush
xAxis.scale(x);
yAxis.scale(y);
brush.x(x)
.on('brushstart.chart', brushstart)
.on('brush.chart', brushmove)
.on('brushend.chart', brushend);
brush.clear();
var svg = selection.selectAll('svg').data([data]),
chartEnter = svg.enter().append('svg')
.append('g')
.attr('width', w)
.attr('height', h)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.classed('chart', true),
chart = svg.select('.chart');
chartEnter.append('g')
.classed('x axis', true)
.attr('transform', 'translate(' + 0 + ',' + h + ')');
chartEnter.append('g')
.classed('y axis', true)
chartEnter.append('g').classed('barGroup', true);
chart.selectAll('.brush').remove();
chart.selectAll('.selected').classed('selected', false);
chart.append('g')
.classed('brush', true)
.call(brush)
.selectAll('rect')
.attr('height', h);
bars = chart.select('.barGroup').selectAll('.bar').data(data);
bars.enter()
.append('rect')
.classed('bar', true)
.attr('x', w) // start here for object constancy
.attr('width', x.rangeBand())
.attr('y', function(d, i) { return y(d.y); })
.attr('height', function(d, i) { return h - y(d.y); });
bars.transition()
.duration(duration)
.style('opacity', 1) // quick fix for exit problem
.attr('width', x.rangeBand())
.attr('x', function(d, i) { return x(d.x); })
.attr('y', function(d, i) { return y(d.y); })
.attr('height', function(d, i) { return h - y(d.y); });
bars.exit()
.transition()
.duration(duration)
.style('opacity', 0)
.remove();
chart.select('.x.axis')
.transition()
.duration(duration)
.call(xAxis);
chart.select('.y.axis')
.transition()
.duration(duration)
.call(yAxis);
function brushstart() {
chart.classed("selecting", true);
}
function brushmove() {
var extent = d3.event.target.extent();
bars.classed("selected", function(d) { return extent[0] <= x(d.x) && x(d.x) + x.rangeBand() <= extent[1]; });
}
function brushend() {
chart.classed("selecting", !d3.event.target.empty());
}
});
}
// basic data
render.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return render;
};
render.width = function(_) {
if (!arguments.length) return width;
width = _;
return render;
};
render.height = function(_) {
if (!arguments.length) return height;
height = _;
return render;
};
// accessors
render.xValue = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return render;
};
render.yValue = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return render;
};
// chart underpinnings
render.brush = function(_) {
if (!arguments.length) return brush;
brush = _;
return render;
};
render.xAxis = function(_) {
if (!arguments.length) return xAxis;
xAxis = _;
return render;
};
render.yAxis = function(_) {
if (!arguments.length) return yAxis;
yAxis = _;
return render;
};
render.x = function(_) {
if (!arguments.length) return x;
x = _;
return render;
};
render.y = function(_) {
if (!arguments.length) return y;
y = _;
return render;
};
// chart enhancements
render.elastic = function(_) {
if (!arguments.length) return elastic;
elastic = _;
return render;
};
render.convertData = function(_) {
if (!arguments.length) return convertData;
convertData = _;
return render;
};
render.duration = function(_) {
if (!arguments.length) return duration;
duration = _;
return render;
};
render.formatNumber = function(_) {
if (!arguments.length) return formatNumber;
formatNumber = _;
return render;
};
return d3.rebind(render, brush, 'on');
};
body {
font: 14px helvetica;
color: #f0f0f0;
background-color: #333;
}
.row {
padding: 5px;
margin: 0px;
}
.axis path,
.axis line {
fill: none;
stroke: #f0f0f0;
shape-rendering: crispEdges;
}
.axis text {
fill: #f0f0f0;
}
.brush .extent {
stroke: #f0f0f0;
fill-opacity: .125;
shape-rendering: crispEdges;
}
.bar {
fill: #5EB4E3;
}
.selected {
fill: #78C656;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.