I'm a front end engineer at CB Insights. We use d3.js for almost all our visualizations, from simple bar and line charts to complex network graphs.
I wanted to share some of what I've learned but I didn't know where to start. There are a ton of d3 tutorials for newbies out there and I didn't feel like I would be able to add anything new. But after reading Mike Bostock's "Let's make a bar chart", I had an idea: I will pick up where he left off and add something new to the simple bar chart we have at the end of the last part of that tutorial:
Here's the idea: we'll turn this into a chart editor. Let's add the possibility to edit the data right there on the page. It sounds awesome (it is) but difficult (it is NOT). Let's do it.
I forked Mike's gist and made 3 commits, each corresponding to a step of this tutorial. Step 0 is of course to follow Mike's tutorial. Leave this tab open and come back to it when you're done.
At the end of each step is a link to what your code should look like at that point.
Bostock rarely makes his code "generic" in his examples, which allows him to write simpler programs that are easier to understand. Therefore we're going to start by modifying this code to make it reusable, so that it will work with any values we throw at it:
// all of these variables are independent of the data,
// so we exclude them from the draw function
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var gXAxis = chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
var gYAxis = chart.append("g")
.attr("class", "y axis");
// this is the function that will be called each time we draw the chart
function draw () {
d3.tsv("data.tsv", type, function(error, data) {
// the data may have changed so we need to update the domain of our scales
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
// we update the axis to reflect the new scale domains
gXAxis.call(xAxis);
gYAxis.call(yAxis);
var bars = chart.selectAll(".bar").data(data);
// if there are new bars since we last called draw (or if it's the first time),
// we add them now
bars
.enter().append("rect")
.attr("class", "bar");
// we update the bars with new attributes based on the new data
bars
.attr("x", function(d) { return x(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x.rangeBand());
// maybe some bars are not present in the new data, let's remove them!
bars.exit().remove();
});
}
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
draw();
// we can call draw any number of time, axis won't be re drawn, just updated
draw();
// bars won't superpose, they'll just be updated or removed
draw();
// such is the magic of d3 data joins! http://bost.ocks.org/mike/join/
draw();
Right now our chart uses a .tsv file as a data source. The main goal of our little project is to switch from that file to an HTML <textarea>
so you can paste data directly from Excel.
We'll start by modifying the HTML:
<div class="editor">
<p>Paste from Excel:</p>
<textarea cols="13" rows="32">name value
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</textarea>
</div>
<div class="svg"><svg class="chart"></svg></div>
As you can see I copied what was in the .tsv file into that <textarea>
as initial values. It is very important that you mirror the whitespace in the code above. Do not indent the inside of <textarea>
.
Now, we'll have to modify the Javascript code. We'll delete the d3.tsv
line that gave us data
and instead read data
from the <textarea>
like so:
var data = d3.tsv.parse(d3.select('textarea').node().value);
We need to specify .node()
to get the DOM element itself.
This is the easiest and coolest looking part. We can already edit our chart, but as you can see, it's hard to see the transition when you edit the data. It'd be great if we could follow the bars as they become bigger or smaller, and quickly compare the differences between two data sets. If you're new to d3, I have some good news for you (if you're not so new, make that old news): d3 will take care of this pretty much by itself. You just have to tell it what to animate, by adding ONE line:
...
bars
.transition() // ANIMATE ME BABY
.attr("x", function(d) { return x(d.name); })
...
That's it! You're done! I told you it was going to be easy. But of course we can always take it further. I encourage you to fork my gist and use it as your new starting point. Go out and explore. Here are some ideas:
- Modify the HTML to add your company logo
- Add text inputs to allow your users (pretend you have users) to add a title or a legend to their chart
- Add a text input to allow your users to change the color of the bars
- Turn it into a horizontal bar chart, add a button to switch between the two (or even a line chart!)
If at any point you get stuck, confused or frustrated, take a look at my commits, they do a good job of detailing the steps, or simply holler at me on twitter, I'm @atestu, at your service.
If you want to see this idea pushed further, checkout Quartz's Chartbuilder, it's pretty cool.
Considering that this is our first d3.js post, how did we do? What should we write about next time?