A Bar Chart, Part 1

Say you have some data—a simple array of numbers:

1 var data = [4, 8, 15, 16, 23, 42];

One of the ways you might visualize this univariate data is a bar chart. This guide will examine how to create a simple bar chart using D3, first with basic HTML, and then a more advanced example with SVG.

HTML

To get started with HTML, you’ll first need a container for the chart:

1 var chart = d3.select("body")
2   .append("div")
3     .attr("class", "chart");

This code selects the document body, which will be the parent of the new chart. Every visible node needs a parent, with the exception of the document’s root node. The chart container, a div element, is then created and appended to the body. The append operator adds the new node as the last child: the chart will appear at the end of the body.

D3 uses the method chaining design pattern. Above, setting the attribute returns the current selection, and the chart variable thus refers to the chart container element. This approach minimizes the amount of code needed to apply many selections and transformations in sequence.

The attr operator sets the “class” attribute on the chart container, allowing stylesheets to be applied to the chart elements. This is convenient for static styles, such as the background color and font size. CSS code lives in a style element or an external stylesheet referenced by a link element, rather than the script element used for JavaScript:

1 .chart div {
2   font: 10px sans-serif;
3   background-color: steelblue;
4   text-align: right;
5   padding: 3px;
6   margin: 1px;
7   color: white;
8 }

Next, add some div elements to the container, setting the width by scaling the data:

1 chart.selectAll("div")
2     .data(data)
3   .enter().append("div")
4     .style("width", function(d) { return d * 10 + "px"; })
5     .text(function(d) { return d; });

This code selects the child div elements of the chart container. This selection is empty because the container was just added. However, by binding this selection to the array of numbers via the data operator, you can obtain the entering selection—a set of placeholder nodes, one per data element, to which you can append the desired child nodes for each bar.

The text operator sets the text content of the bars. The identity function, function(d) { return d; }, causes each data value (number) to be formatted using JavaScript’s default string conversion, equivalent to the built-in String function. This may be ugly for some numbers (e.g., 0.12000000000000001). The d3.format class, modeled after Python’s string formatting, is available for more control over how the number is formatted, supporting comma-grouping of thousands and fixed precision.

The above code results in a rudimentary bar chart:

One weakness of the code so far is the magic number 10, which scales the data value to the appropriate bar width. This number depends on the domain of the data (the maximum value, 42), and the width of the chart (420). To avoid hard-coding the x-scale of 10, you can use D3’s linear scale class, and compute the maximum value from the data:

1 var x = d3.scale.linear()
2     .domain([0, d3.max(data)])
3     .range(["0px", "420px"]);

Although it looks like an object, the x variable here is actually a function that converts data values (in the domain) to scaled values (in the range). For example, an input value of 4 returns “40px”, and an input value of 16 returns “160px”. The output range of the scale in this example are strings, with the appropriate px units for CSS. D3’s automatic interpolators detect the numbers within the strings, while retaining the constant remainder.

The new scale arguably still has a magic number: 420px, the width of the chart. If you want to make the chart resizable, you can inspect the width of the chart container, chart.style("width"). Or, use percentages rather than pixels. In any case, the reusable scale makes the chart specification easier to modify—for example, you can easily replace the linear scale with a log or root scale.

Using the new scale, you can simplify the width style definition:

1 chart.selectAll("div")
2     .data(data)
3   .enter().append("div")
4     .style("width", x)
5     .text(String);

Now, the HTML representation is very concise, but it’s not very flexible. Displaying reference lines in the background, or generating columns rather than bars, is difficult in pure HTML. Chart types such as pies and streamgraphs are practically impossible. Fortunately, there’s a convenient alternative: Scalable Vector Graphics (SVG)!

SVG

You use SVG much the same way as HTML, but it offers substantially more flexibility. To start with SVG, create an svg:svg container instead of a div:

1 var chart = d3.select("body")
2   .append("svg:svg")
3     .attr("class", "chart")
4     .attr("width", 420)
5     .attr("height", 20 * data.length);

An immediate difference you will notice with SVG is that the units are implicitly pixels, and thus do not need the “px” suffix. Even with pixels, you can rescale the SVG without losing image quality. You can use percentages for relative positioning, too. To use a numeric range for the x-scale:

1 var x = d3.scale.linear()
2     .domain([0, d3.max(data)])
3     .range([0, 420]);

Unlike HTML, SVG does not provide automatic flow layout. Shapes are positioned relative to the top-left corner, called the origin. Thus, by default, the bars would be drawn on top of each other. To fix this, set the y-coordinate and height explicitly:

1 chart.selectAll("rect")
2     .data(data)
3   .enter().append("svg:rect")
4     .attr("y", function(d, i) { return i * 20; })
5     .attr("width", x)
6     .attr("height", 20);

Also, the CSS changes slightly when using SVG. Rather than the background, the fill determines the bar color. You can also apply a white border to each bar by setting the stroke style:

1 .chart rect {
2   stroke: white;
3   fill: steelblue;
4 }

The SVG-based chart is now almost identical to our original. The chart is currently missing labels, but that will be fixed shortly:

Astute readers will notice that a magic number crept back into the chart description: the bar height of 20 pixels. Arguably, this number isn’t magic—twenty is a reasonable height for the bars, just as fourteen is a reasonable point size for text. However, if you prefer to set a height for the entire chart, use a second scale for the y-axis:

1 var y = d3.scale.ordinal()
2     .domain(data)
3     .rangeBands([0, 120]);

As with x previously, y is now a function. It takes as input values from the data array, and for each value returns the corresponding y-coordinate. For example, an input value of 4 returns 0, and an input value of 16 returns 60. This approach requires that the values in our dataset are unique; ordinal scales are often used to encode non-quantitative data, such as country names. Alternatively, you can use array indices as the ordinal domain: [0, 1, 2…].

The new scale plugs into the bar specification, replacing the “y” attribute:

1 chart.selectAll("rect")
2     .data(data)
3   .enter().append("svg:rect")
4     .attr("y", y)
5     .attr("width", x)
6     .attr("height", y.rangeBand());

The new scales can also be applied to render labels, showing the associated value. This code centers labels vertically within each bar, and right-aligns text:

1 chart.selectAll("text")
2     .data(data)
3   .enter().append("svg:text")
4     .attr("x", x)
5     .attr("y", function(d) { return y(d) + y.rangeBand() / 2; })
6     .attr("dx", -3) // padding-right
7     .attr("dy", ".35em") // vertical-align: middle
8     .attr("text-anchor", "end") // text-align: right
9     .text(String);

The formal SVG Text specification describes in detail the meaning of the “dx”, “dy” and “text-anchor” attributes. The full spec is dense, as SVG offers a level of control required by only the most ambitious typographers; fortunately, it’s not too hard to remember standard settings for alignment and padding!

The SVG chart now looks identical to the earlier HTML version:

With the basic chart is in place, you can place additional marks to improve readability. As a first step, pad the SVG container to make space for labels:

1 var chart = d3.select(".content")
2   .append("svg:svg")
3     .attr("class", "chart")
4     .attr("width", 440)
5     .attr("height", 140)
6   .append("svg:g")
7     .attr("transform", "translate(10,15)");

The svg:g element is a container element, much like the div element in HTML. Setting a transform on a container affects how its children are positioned. For padding, you need only to translate; however, for advanced graphical effects, you can use any affine transformation, such as scale, rotate and shear!

The linear scale, x, provides a ticks routine that generates values in the domain at sensible intervals. For a chart this size, about ten ticks is appropriate; for smaller or larger charts, you can vary the number of ticks to generate. These tick values serve as data for reference lines:

1 chart.selectAll("line")
2     .data(x.ticks(10))
3   .enter().append("svg:line")
4     .attr("x1", x)
5     .attr("x2", x)
6     .attr("y1", 0)
7     .attr("y2", 120)
8     .attr("stroke", "#ccc");

Positioning text above the reference lines reveals their value:

1 chart.selectAll("text.rule")
2     .data(x.ticks(10))
3   .enter().append("svg:text")
4     .attr("class", "rule")
5     .attr("x", x)
6     .attr("y", 0)
7     .attr("dy", -3)
8     .attr("text-anchor", "middle")
9     .text(String);

Note that the rule labels are assigned the class “rule”; this avoids a selector collision with the value labels on each bar. (Another way to disambiguate is to put reference labels in a separate g container.) Lastly, add a single black line for the y-axis:

1 chart.append("svg:line")
2     .attr("y1", 0)
3     .attr("y2", 120)
4     .attr("stroke", "#000");

Et voilà!

This tutorial covered many of the core concepts in D3, including selections, dynamic properties, and scales. However, this only scratches the surface! Continue reading part 2 to learn about transitions in dynamic visualizations. Or, explore the examples gallery to see more advanced techniques with D3.

Copyright © 2011 Mike Bostock
Fork me on GitHub