Published
Edited
Mar 1, 2021
1 fork
1 star
Insert cell
md`# D3 Grouped Bar Chart

In this example, we're going to use the same exact data, but we're going to make a side-by-side grouped bar chart with our count broken down by gender.

The code here is almost exactly like the code for the [Basic Bar Chart](https://observablehq.com/@mkane2/d3-basic-bar-chart) example (and in fact, this is a fork of that notebook). I've removed most of the commentary text blocks, so the text blocks and code comments here call out new changes that are different from the previous example.`
Insert cell
md`### Set utilities`
Insert cell
d3 = require('d3@6')
Insert cell
height = 500
Insert cell
margin = ({
top: 10,
right: 10,
bottom: 40,
left: 35
})
Insert cell
md`### Import the data

This is the same file as the data in the previous lesson [D3 Basic Bar Chart](https://observablehq.com/@mkane2/d3-basic-bar-chart).`
Insert cell
data = FileAttachment("1915State.csv").csv()
Insert cell
md `### Get the birth years and aggregate

This code is almost exactly the same as the Basic Bar Chart lesson. We rollup the birth_year data, split it by year and--new--**also by gender**.`
Insert cell
birth_years = d3.rollup(data, v => v.length, d => d.birth_year, d => d.gender)
Insert cell
md`Our aggregate function is much longer here than in our Basic Bar Chart example. This is because we have map objects nested within map objects after splitting by more than one property name (birth_year and gender are property names).`
Insert cell
// This is our aggregate variable from the Basic example.
// aggregate = Array.from(birth_years, ([year, count]) => ({ year, count }))
aggregate = Array.from(birth_years, ([year, count]) => {
const obj = {};
for (const [gender, num] of count) {
obj.year = year;
obj[gender] = num;
}
return obj;
})
Insert cell
import { Table } from "@observablehq/inputs"
Insert cell
Table(aggregate, {
sort: "year"
})
Insert cell
md `We can see in the table above that there's occasionally entries with no gender, like for 1841. This is a data cleaning problem that we would need to fix before bringing our data into D3.`
Insert cell
md `## Define the X and Y Axis

Here we're going to define our x and y axis. Much of this will look similar to the basic bar chart, but we're going to define two variables for the x-axis.

First, we're going to define the xScale variable to draw the x-axis just like we did in the basic bar chart.`
Insert cell
xDomain = aggregate.map(d => d["year"]).sort()
Insert cell
xScale = d3
.scaleBand()
.domain(xDomain)
.range([margin.left, width - margin.right - margin.left])
.padding(0.1) // slightly smaller padding since we're displaying more information
Insert cell
xAxis = d3.axisBottom(xScale).tickSizeOuter(0)
Insert cell
md`### X Axis new steps

Our second variable for the x-axis, *x1*, is necessary because we need additional information to place the grouped gender bars along the x axis. First, we define our groups by naming the groups in the keys variable. (Remember that when we made *aggregate*, we counted the number of male and female individuals per year, so our objects in *aggregate* look like year: 1841, Male: 9, Female: 5). We're naming the keys to access the values we're looking for.

Below, Object.keys() gets the keys of the first object in the *aggregate* array, aggregate[0] (accessed using its index number). .slice(1) slices off the first element of the keys array, which is year, because we don't need that to group our bars by gender.`
Insert cell
keys = Object.keys(aggregate[0]).slice(1)
Insert cell
md `Now in our secondary x-axis variable, the data we're charting is the two gender keys, and we plot them in the range of the band created by *xScale* for each year.`
Insert cell
x1 = d3
.scaleBand()
.domain(keys)
.rangeRound([0, xScale.bandwidth()]) // here we use rangeRound instead of range because we're using values computed by xScale, which may not be whole numbers, and we need whole numbers to avoid errors.
.padding(0.05)
Insert cell
md`### Y Axis new steps

Our Y axis is mostly unchanged, with one exception. Because we have our count information in two different keys (Male and Female) now, we need to look in both to determine the domain for the axis. Here, we use a nested d3.max function to find the maximum of both keys for the .domain().`
Insert cell
yScale = d3
.scaleLinear()
.domain([0, d3.max(aggregate, d => d3.max(keys, key => d[key]))]) // in each key, look for the maximum number
.rangeRound([height - margin.bottom, margin.top])
Insert cell
yAxis = d3.axisLeft(yScale).tickSizeOuter(0)
Insert cell
md`## Define the color scale

The final new element of our grouped chart is to set the colors of our bars to differentiate between Male and Female counts. We're going to use [d3.scaleOrdinal()](https://observablehq.com/@d3/d3-scaleordinal) to define the range of colors for our bar chart. .scaleOrdinal() says we're going to use an ordinal or categorical color system (ordinal data is ordered in some way, categorical is not).

The input to .scaleOrdinal() tells d3 what color palette to use. D3 has a number of [built-in color palettes to choose from](https://github.com/d3/d3-scale-chromatic), or you can define your own colors using rgb or #hex color values in an array. Try commenting out the color variables in the cell below to see what each does, or change the inputs to .scaleOrdinal()

If we were defining our own color values in a large-scale or public facing project, we would want to create a separate "style sheet" document that keeps track of the exact rgb or hex color codes for our color palette, and check that our chosen colors work for people with different kinds of color blindness or other low vision accessibility needs. [Chroma.js](https://gka.github.io/palettes/#/2|s|5e366a,0cca98|ffffe0,ff005e,93003a|1|1) is a nice tool to check if your chosen colors are color-blind-friendly for different types of color blindesss, while [ColorBrewer](https://colorbrewer2.org/#type=sequential&scheme=BuGn&n=3) helps build different kinds of color blind friendly palettes. In general, it's best practice to choose colors that are high contrast from each other and from the background they're displayed on.`
Insert cell
// color = d3.scaleOrdinal(d3.schemePastel1)
color = d3.scaleOrdinal(["rgb(94, 54, 106)", "#0CCA98"])
// color = d3.scaleOrdinal(d3.schemePuOr) // this won't work because it's a color scheme made for continuous data, not categorical data!
Insert cell
md `## Draw the chart`
Insert cell
bars = {
// select the canvas
const svg = d3.select(DOM.svg(width, height));

// draw the bars
svg
.append("g")
.selectAll("g")
.data(aggregate)
.join("g")
.attr("transform", d => `translate(${xScale(d["year"])},0)`) // place each bar along the x-axis at the place defined by the xScale variable
.selectAll("rect")
.data(d => keys.map(key => ({ key, value: d[key] }))) // use the Male/Female keys to access the data separately
.join('rect')
.attr("x", d => x1(d.key)) // use the x1 variable to place the grouped bars
.attr("y", d => yScale(d.value)) // draw the height of the barse using the data from the Male/Female keys as the height value
.attr("width", x1.bandwidth()) // bar is the width defined by the x1 variable
.attr("height", d => yScale(0) - yScale(d.value))
.attr("fill", d => color(d.key)); // color each bar according to its key value as defined by the color variable

// draw the x axis
// nothing new here
svg
.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(xAxis)
.selectAll("text")
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(90)")
.style("text-anchor", "start");

// draw the y axis
// nothing new here
svg
.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${margin.left},0)`)
.call(yAxis);

// render the whole chart
// nothing new here
return svg.node();
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more