Skip to content

Instantly share code, notes, and snippets.

@larsvers
Last active September 24, 2016 12:04
Show Gist options
  • Save larsvers/0ac3e1900a3ac33c43a16334e57bb774 to your computer and use it in GitHub Desktop.
Save larsvers/0ac3e1900a3ac33c43a16334e57bb774 to your computer and use it in GitHub Desktop.
mnml reusable
license: mit

The reusable chart pattern in pants

This is Bostock's reusable chart pattern stripped to the bare bones. It includes enter/update/exit, loads of comments and even a tooltip (the pants - easily stripped off, too).

Built with blockbuilder.org

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>mnml reusable</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.9.2/lodash.min.js"></script>
<link rel="stylesheet" href="reusable.css">
</head>
<body>
<style type="text/css">
</style>
<div class="container"></div>
<button id="sort1">sort by rating</button>
<button id="sort2">sort by film</button>
<div class="tooltip"></div>
<script src="reusable.js"></script>
</body>
</html>
film rating budget
Avatar 7.9 425000000
Star Wars Ep. VII: The Force Awakens 8.2 306000000
Pirates of the Caribbean: At World's End 7.1 300000000
Spectre 6.8 300000000
The Lone Ranger 6.5 275000000
John Carter 6.6 275000000
The Dark Knight Rises 8.4 275000000
Tangled 7.8 260000000
Spider-Man 3 6.2 258000000
Harry Potter and the Half-Blood Prince 7.5 250000000
The Hobbit: An Unexpected Journey 7.9 250000000
Pirates of the Caribbean: On Stranger Tides 6.7 250000000
The Hobbit: The Desolation of Smaug 7.9 250000000
The Hobbit: The Battle of the Five Armies 7.5 250000000
Avengers: Age of Ultron 7.5 250000000
Batman v Superman: Dawn of Justice 6.9 250000000
Captain America: Civil War 8.1 250000000
Superman Returns 6.1 232000000
Quantum of Solace 6.7 230000000
The Avengers 8.1 225000000
div.tooltip {
position: absolute;
display: inline-block;
max-width: 20vw;
padding: 10px;
font-family: Arial;
font-size: 0.8em;
color: #000;
background-color: #fff;
border: 1px solid #999;
border-radius: 2px;
pointer-events: none;
opacity: 0;
z-index: 2;
}
// load/format
d3.csv('movie.csv', type, function(err, data) {
var data = data;
// Choose the main quant variable the chart displays (and update the button accordingly)
var xVariable = 'budget';
d3.select('button#sort1').html('sort by ' + xVariable);
// Compose the chart
var newChart = chart()
.xVar(xVariable);
// Initialise the chart
d3.select('div.container')
.datum(data)
.call(newChart);
// Change the data option 1
d3.select('#sort1').on('mousedown', function(d) {
data = _.sortBy(data, function(el) { return el[xVariable]; });
d3.select('div.container')
.datum(data)
.call(newChart);
});
// Change the data option 2
d3.select('#sort2').on('mousedown', function(d) {
data = _.sortBy(data, function(el) { return el.film; });
d3.select('div.container')
.datum(data)
.call(newChart);
});
});
// Chart function
function chart() {
// Exposed variables
var width = 700;
var height = 400;
var xVar = 'rating';
var yVar = 'film';
// Closure to hide mechanics
function my(selection) {
// pass the data to each selection (multiples-friendly)
selection.each(function(data, i) {
var minX = 0;
var maxX = d3.max(data, function(d) { return d[xVar]; });
var scaleX = d3.scale.linear().domain([minX, maxX]).range([0, width]);
var scaleY = d3.scale.ordinal().domain(d3.range(data.length)).rangePoints([height, 0], 1);
// In the following we'll attach an svg element to the container element (the 'selection') when and only when we run this the first time.
// We do this by using the mechanics of the data join and the enter selection.
// As a short reminder: the data join (on its own, not chained with .enter()) checks how many data items there are
// and stages a respective number of DOM elements.
// An join on its own - detached from the .enter() method checks first how many data elements come in new
// (n = new data elements) to the data join selection and then it appends the specified DOM element exactly n times.
// Here we do exactly that with joining the data as one array element with the non-existing svg first:
var svg = d3.select(this) // conatiner (here 'body')
.selectAll('svg') // first time: empty selection of staged svg elements (it's .selectAll not .select)
.data([data]); // first time: one array item, hence one svg will be staged (but not yet entered);
svg // one data item [data] staged with one svg element
.enter() // first time: initialise the DOM element entry; second time+: empty
.append("svg"); // first time: append the svg; second time+: nothing happens
// If we have more elements apart from the svg element that should only be appended once to the chart
// like axes, or svg > g-elements for the margins,
// we would store the enter-selection in a unique variable (like 'svgEnter', or if we inlcude another g 'gEnter'.
// This allows us to reference just the enter()-selection which would be empty with every update,
// not invoking anything that comes after .enter() - apart from the very first time.
svg
.attr('width', width)
.attr('height', height);
// Here comes the general update pattern:
// Data join
var bar = svg // this cost me a night ! see below for explanations.
.selectAll('.bar')
.data(data, function(d) { return d[yVar]; }); // key function to achieve object constancy
// I select the svg via the svg variable. This does not work in V4 anymore - I have to select the svg with a simple selector (d3.select('svg'))
// Why? Because the enter selection in v4 is immutable - it doesn't change and doesn't automatically cover updates.
// In v3.x it was still mutable and covered any updates on the element as well.
// console.log the variable and the selector in v3.x and in v4 and see that in v4 the 2 actually differ.
// The var has enter and exit functions, the d3.select has not.
// in v3.x both have enter and exit functions B U T an enter selection in v3.x was open to changes
// like for example appending elemenst - like we do here with the rects.
// In v4 the enter selection is immutable which means we can't append anything unless it's the first data join of the page loading period.
// (or unless we tell the enter selection to merge with the update selection with .merge())
// In our case, however, the best way is to just take that svg as a simple manifested selection and do with it whatever we want.
// Enter
bar
.enter()
.append('rect')
.classed('bar', true)
.attr('x', scaleX(minX))
.attr('height', 5)
.attr('width', function(d) { return scaleX(minX); });
// Update
bar
.transition().duration(1000).delay(function(d,i) { return i / (data.length-1) * 1000; }) // implement gratuitous object constancy
.attr('width', function(d) { return scaleX(d[xVar]); })
.attr('y', function(d, i) { return scaleY(i); });
// Exit
bar
.exit()
.transition().duration(1000)
.attr('width', function(d) { return scaleX(minX); })
.remove();
}); // selection.each()
triggerTooltip(yVar); // invoke tooltip - not necessary, forget about it, remove it to keep it simple
} // Closure
// Accessor functions for exposed variables
my.xVar = function(value) {
if(!arguments.length) return xVar;
xVar = String(value);
return my;
}
my.yVar = function(value) {
if(!arguments.length) return yVar;
yVar = String(value);
return my;
}
my.width = function(value) {
if(!arguments.length) return width;
width = value;
return my;
}
my.height = function(value) {
if(!arguments.length) return height;
height = value;
return my;
}
return my; // Expose closure
} // chart()
// Format data
function type(d) {
d.film = d.film;
d.rating = +d.rating;
return d;
}
// Tooltip (not key, but kind)
var triggerTooltip = function(yVar) {
d3.selectAll('.bar').on('mouseover', function(d) {
var datapoint = d3.select(this).data()[0];
d3.select('div.tooltip')
.style('left', (d3.event.pageX + 5) + 'px')
.style('top', (d3.event.pageY + 5) + 'px')
.html(datapoint[yVar])
.style('opacity', 0)
.transition()
.style('opacity', .9);
});
d3.selectAll('.bar').on('mousemove', function(d) {
d3.select('div.tooltip')
.style('left', (d3.event.pageX + 5) + 'px')
.style('top', (d3.event.pageY + 5) + 'px');
});
d3.selectAll('.bar').on('mouseout', function(d) {
d3.select('div.tooltip')
.transition()
.style('opacity', 0);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment