Skip to content

Instantly share code, notes, and snippets.

@milroc
Last active September 15, 2016 08:48
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 milroc/5519642 to your computer and use it in GitHub Desktop.
Save milroc/5519642 to your computer and use it in GitHub Desktop.
bar + sum: reusable d3.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 2. This is showcasing the power of the reusable chart API. This is showcasing the difference between a reusable bar chart and a prototyped bar chart.

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 type="text/css" rel="stylesheet" href="style.css"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="src.js"></script>
</head>
<body>
<div class="row">
<div class="span2"><button class="btn btn-success" onclick="update()">update</button></div>
<div class="span2" id="sum">TOTAL: 0</div>
</div>
<div class="row" id="chart"></div>
<script type="text/javascript">
var bar = charts.bar()
.on('brush', makeSum)
.on('brushend', makeSum),
data;
update();
function update() {
data = randomizeData(20, Math.random()*100000);
d3.select("#chart")
.datum(data)
.call(bar);
}
function makeSum() {
var sumDiv = d3.select('#sum'),
extent = d3.event.target.extent()
x = bar.x(),
sum = 0;
// inefficient recommend for performance to leverage crossfilter.js
data.forEach(function(d) {
if (extent[0] <= x(d.x) && x(d.x) + x.rangeBand() <= extent[1])
sum += d.y;
});
sumDiv.text('TOTAL: ' + sum);
}
function randomizeData(n, y) {
if (arguments.length < 2) y = 400;
if (!arguments.length) n = 20;
var i = 0;
return d3.range(~~(Math.random()*n) + 1).map(function(d, i) { return {
x: ++i,
y: ~~(Math.random()*y)
}});
}
</script>
</body>
</html>
charts = {};
charts.bar = function() {
// basic data
var margin = {top: 0, 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 * 14;
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)
.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: #3E4651;
}
.row {
padding: 5px;
}
.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