Skip to content

Instantly share code, notes, and snippets.

@atestu
Forked from mbostock/.block
Last active May 15, 2016 01:17
Show Gist options
  • Save atestu/9647538 to your computer and use it in GitHub Desktop.
Save atestu/9647538 to your computer and use it in GitHub Desktop.
Let's make a bar chart... editor!

Let's make a bar chart... editor!

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.

Step 1: Reusable chart

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();

Code on gist

Step 2: Editable chart

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.

Code on gist

Step 3: Final touch: animations

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); })
...

Code on gist

Next steps

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?

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.editor, .svg {
display: inline-block;
vertical-align: top;
}
.editor {
padding-left: 12px;
}
.svg {
border-left: 1px solid #dedede;
padding-left: 12px;
margin-left: 12px;
}
</style>
<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>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// 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 = 800 - 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 () {
// now we source the data from the textarea instead of the external file
var data = d3.tsv.parse(d3.select('textarea').node().value);
// 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
.transition() // ANIMATE ME BABY
.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;
}
// we call the draw function when the focus is out of the textarea
d3.select('textarea').on('blur', draw);
// on page load we call the function which will use what is currently in the textarea
draw();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment