Skip to content

Instantly share code, notes, and snippets.

@Golodhros
Forked from mbostock/.block
Last active November 29, 2015 19:37
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save Golodhros/2af6e6e47935190936b1 to your computer and use it in GitHub Desktop.
TDD Bar Chart

This chart takes as a base the simple bar chart by Mike Bostock and refactors that example using the reusable API, making it responsive and configurable.

It also uses the Data Manager API from the Developing a D3.js Edge Book.

It includes tests and was conceived on a TDD basis for the talk Better D3 Charts with the Reusable API.

From the original Block:

This simple bar chart is constructed from a TSV file storing the frequency of letters in the English language. The chart employs conventional margins and a number of D3 features:

letter frequency
A .08167
B .01492
C .02782
D .04253
E .12702
F .02288
G .02015
H .06094
I .06966
J .00153
K .00772
L .04025
M .02406
N .06749
O .07507
P .01929
Q .00095
R .05987
S .06327
T .09056
U .02758
V .00978
W .02360
X .00150
Y .01974
Z .00074
var graphs = graphs || {};
graphs.dataManager = function module() {
var exports = {},
dispatch = d3.dispatch('dataReady', 'dataLoading', 'dataError'),
data;
d3.rebind(exports, dispatch, 'on');
exports.loadJsonData = function(_file, _cleaningFn) {
var loadJson = d3.json(_file);
loadJson.on('progress', function(){
dispatch.dataLoading(d3.event.loaded);
});
loadJson.get(function (_err, _response){
if(!_err){
_response.data.forEach(function(d){
_cleaningFn(d);
});
data = _response.data;
dispatch.dataReady(_response.data);
}else{
dispatch.dataError(_err.statusText);
}
});
};
exports.loadTsvData = function(_file, _cleaningFn) {
var loadTsv = d3.tsv(_file);
loadTsv.on('progress', function() {
dispatch.dataLoading(d3.event.loaded);
});
loadTsv.get(function (_err, _response) {
if(!_err){
_response.forEach(function(d){
_cleaningFn(d);
});
data = _response;
dispatch.dataReady(_response);
}else{
dispatch.dataError(_err.statusText);
}
});
};
// If we need more types of data geoJSON, csv, etc. we will need
// to create methods for them
exports.getCleanedData = function(){
return data;
};
return exports;
};
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="style.css"/>
</head>
<body>
<h2 class="block-title">TDD BarChart</h2>
<div class="bar-graph"></div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="dataManager.js"></script>
<script src="src.js"></script>
<script type="text/javascript">
// Code that instantiates the graph and uses the data manager to load the data
var app = {
// D3 Reusable API Chart
barGraph: {
dataManager: null,
config: {
margin : {
top : 20,
bottom: 30,
right : 20,
left : 40
},
aspectWidth: 13,
aspectHeight: 4,
animation: 'linear',
dataURL: 'data.tsv'
},
init: function(ele){
this.$el = ele;
this.requestNewData();
this.addEvents();
},
addEvents: function(){
//Callback triggered by browser
window.onresize = $.proxy(this.drawGraph, this);
},
calculateRatioHeight: function(width) {
var config = this.config;
return Math.ceil((width * config.aspectHeight) / config.aspectWidth);
},
dataCleaningFunction: function(d){
d.frequency = +d.frequency;
d.letter = d.letter;
},
drawGraph: function(){
var config = this.config,
width = this.$el.width(),
height = this.calculateRatioHeight(width);
this.resetGraph();
this.barChart = graphs.barChart()
.width(width).height(height).margin(config.margin);
this.container = d3.select(this.$el[0])
.datum(this.data)
.call(this.barChart);
},
handleReceivedData: function(result){
this.data = result;
this.drawGraph();
},
requestNewData: function(el){
this.dataManager = graphs.dataManager();
this.dataManager.on('dataError', function(errorMsg){
console.log('error:', errorMsg);
});
this.dataManager.on('dataReady', $.proxy(this.handleReceivedData, this));
this.dataManager.loadTsvData(this.config.dataURL, this.dataCleaningFunction);
},
resetGraph: function(){
this.$el.find('svg').remove();
}
}
};
$(function(){
app.barGraph.init($('.bar-graph'));
});
</script>
</body>
var graphs = graphs || {};
graphs.barChart = function module(){
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960,
height = 500,
gap = 2,
data,
chartWidth, chartHeight,
xScale, yScale,
xAxis, yAxis,
svg,
// extractors
getLetter = function(d) { return d.letter; },
getFrequency = function(d) { return d.frequency; };
/**
* This function creates the graph using the selection as container
* @param {D3Selection} _selection A d3 selection that represents
* the container(s) where the chart(s) will be rendered
*/
function exports(_selection){
/* @param {object} _data The data to attach and generate the chart */
_selection.each(function(_data){
chartWidth = width - margin.left - margin.right;
chartHeight = height - margin.top - margin.bottom;
data = _data;
buildScales();
buildAxis();
buildSVG(this);
drawBars();
drawAxis();
});
}
/**
* Creates the d3 x and y axis, setting orientations
* @private
*/
function buildAxis(){
xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.ticks(10, '%');
}
/**
* Builds containers for the chart, the axis and a wrapper for all of them
* Also applies the Margin convention
* @private
*/
function buildContainerGroups(){
var container = svg.append('g')
.classed('container-group', true)
.attr({
transform: 'translate(' + margin.left + ',' + margin.top + ')'
});
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);
}
/**
* Creates the x and y scales of the graph
* @private
*/
function buildScales(){
xScale = d3.scale.ordinal()
.domain(data.map(getLetter))
.rangeRoundBands([0, chartWidth], 0.1);
yScale = d3.scale.linear()
.domain([0, d3.max(data, getFrequency)])
.range([chartHeight, 0]);
}
/**
* @param {HTMLElement} container DOM element that will work as the container of the graph
* @private
*/
function buildSVG(container){
if (!svg) {
svg = d3.select(container)
.append('svg')
.classed('bar-chart', true);
buildContainerGroups();
}
svg.transition().attr({
width: width + margin.left + margin.right,
height: height + margin.top + margin.bottom
});
}
/**
* @description
* Draws the x and y axis on the svg object within their
* respective groups
* @private
*/
function drawAxis(){
svg.select('.x-axis-group.axis')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis);
svg.select('.y-axis-group.axis')
.call(yAxis);
}
/**
* Draws the bar elements within the chart group
* @private
*/
function drawBars(){
var gapSize = xScale.rangeBand() / 100 * gap,
barW = xScale.rangeBand() - gapSize,
bars = svg.select('.chart-group').selectAll('.bar').data(data);
// Enter
bars.enter()
.append('rect')
.classed('bar', true)
.attr({
width: barW,
x: chartWidth, // Initially drawing the bars at the end of Y axis
y: function(d) { return yScale(d.frequency); },
height: function(d) { return chartHeight - yScale(d.frequency); }
});
// Update
bars
.attr({
width: barW,
x: function(d) { return xScale(d.letter) + gapSize/2; },
y: function(d) { return yScale(d.frequency); },
height: function(d) { return chartHeight - yScale(d.frequency); }
});
// Exit
bars.exit()
.style({ opacity: 0 }).remove();
}
exports.margin = function(_x) {
if (!arguments.length) return margin;
margin = _x;
return this;
};
exports.width = function(_x) {
if (!arguments.length) return width;
width = _x;
return this;
};
exports.height = function(_x) {
if (!arguments.length) return height;
height = _x;
return this;
};
return exports;
};
// Simple tests for the bar chart
describe('Reusable barChart Test Suite', function() {
var barChart, dataset, containerFixture, f;
beforeEach(function() {
dataset = [
{
letter: 'A',
frequency: .08167
},
{
letter: 'B',
frequency: .01492
},
{
letter: 'C',
frequency: .02782
},
{
letter: 'D',
frequency: .04253
},
{
letter: 'E',
frequency: .12702
},
{
letter: 'F',
frequency: .02288
},
{
letter: 'G',
frequency: .02015
},
{
letter: 'H',
frequency: .06094
},
{
letter: 'I',
frequency: .06966
},
{
letter: 'J',
frequency: .00153
},
{
letter: 'K',
frequency: .00772
},
{
letter: 'L',
frequency: .04025
},
{
letter: 'M',
frequency: .02406
},
{
letter: 'N',
frequency: .06749
},
{
letter: 'O',
frequency: .07507
},
{
letter: 'P',
frequency: .01929
},
{
letter: 'Q',
frequency: .00095
},
{
letter: 'R',
frequency: .05987
},
{
letter: 'S',
frequency: .06327
},
{
letter: 'T',
frequency: .09056
},
{
letter: 'U',
frequency: .02758
},
{
letter: 'V',
frequency: .00978
},
{
letter: 'W',
frequency: .02360
},
{
letter: 'X',
frequency: .00150
},
{
letter: 'Y',
frequency: .01974
},
{
letter: 'Z',
frequency: .00074
}
];
barChart = graphs.barChart();
$('body').append($('<div class="test-container"></div>'));
containerFixture = d3.select('.test-container');
containerFixture.datum(dataset).call(barChart);
});
afterEach(function() {
containerFixture.remove();
});
it('should render a chart with minimal requirements', function(){
expect(containerFixture.select('.bar-chart').empty()).toBeFalsy();
});
it('should render container, axis and chart groups', function(){
expect(containerFixture.select('g.container-group').empty()).toBeFalsy();
expect(containerFixture.select('g.chart-group').empty()).toBeFalsy();
expect(containerFixture.select('g.x-axis-group').empty()).toBeFalsy();
expect(containerFixture.select('g.y-axis-group').empty()).toBeFalsy();
});
it('should render an X and Y axis', function(){
expect(containerFixture.select('.x-axis-group.axis').empty()).toBeFalsy();
expect(containerFixture.select('.y-axis-group.axis').empty()).toBeFalsy();
});
it('should render a bar for each data entry', function(){
var numBars = dataset.length;
expect(containerFixture.selectAll('.bar').size()).toEqual(numBars);
});
it('should provide margin getter and setter', function(){
var defaultMargin = barChart.margin(),
testMargin = {top: 4, right: 4, bottom: 4, left: 4},
newMargin;
barChart.margin(testMargin);
newMargin = barChart.margin();
expect(defaultMargin).not.toBe(testMargin);
expect(newMargin).toBe(testMargin);
});
it('should provide width getter and setter', function() {
var defaultWidth = barChart.width(),
testWidth = 200,
newWidth;
barChart.width(testWidth);
newWidth = barChart.width();
expect(defaultWidth).not.toBe(testWidth);
expect(newWidth).toBe(testWidth);
});
it('should provide height getter and setter', function() {
var defaultHeight = barChart.height(),
testHeight = 200,
newHeight;
barChart.height(testHeight);
newHeight = barChart.height();
expect(defaultHeight).not.toBe(testHeight);
expect(newHeight).toBe(testHeight);
});
});
@import url("//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");
body {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Helvetica, Arial, sans-serif;
}
.block-title {
color: #222;
font-size: 44px;
font-style: normal;
font-weight: 300;
text-rendering: optimizelegibility;
}
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/jasmine.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/jasmine.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/jasmine-html.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/boot.js"></script>
<!-- Favicon -->
<link rel="shortcut icon" type="image/png" href="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.0.0/jasmine_favicon.png" />
<!-- End Favicon -->
<!-- source files... -->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="dataManager.js"></script>
<script src="src.js"></script>
<!-- spec files... -->
<script src="src.spec.js"></script>
</head>
<body>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment