Skip to content
Student Login

What do Americans want for Christmas?

Last updated: December 2018Ā šŸ‘‰ livestreamed every last Sunday of the month. Join live or subscribe by email šŸ’Œ

Different ages want different things. Create a horizontal stack chart showing what everyone wants for Christmas.

Dataset: Download dataset šŸ—³

My solution šŸ‘‡

How it works āš™ļø

Today's challenge is a perfect example of how chroma-js automatically makes your dataviz beautiful. Best magic trick I ever learned from Shirley Wu.

We used Susie Lu's d3-legend for the color legend, D3's stack layout to calculate coordinates for stacking those bar charts, D3 axis for the axis, and the rest was React. Similar code to the bar chart in Christmas movies at the box office.

Load the data

We begin once more by loading the data. If you've been following along so far, this code will look familiar.

componentDidMount() {
d3.tsv("/data.tsv", d => ({
category: d.category,
young: Number(d.young),
mid: Number(d.mid),
old: Number(d.old)
})).then(data => this.setState({ data }));
}

d3.tsv loads our tab separated values file with data, a parsing function turns each line into nice objects we can use, and then we save it into component local state.

An axis and a legend

Building axes and legends from scratch is not hard, but it is fiddly and time consuming and fraught with tiny little traps for you to fall into. No time for that on a daily challenge!

d3blackbox to the rescue!

const VerticalAxis = d3blackbox((anchor, props) => {
const axis = d3.axisLeft().scale(props.scale)
d3.select(anchor.current).call(axis)
})
const Legend = d3blackbox((anchor, props) => {
d3.select(anchor.current).call(
legend.legendColor().scale(props.scale).title("Age group")
)
})

Here you can see just how flexible the blackbox rendering approach I teach in React for Data Visualization can be. You can take just about any D3 code and turn it into a React component.

Means you don't have to write your own fiddly stuff šŸ‘Œ

d3blackbox ensures our render functions are called on every component render and creates a positionable grouping, <g>, SVG element for us to move around.

Each category's barchart

You can think a stacked bar chart as a series of barcharts. Each category gets its own.

const BarChart = ({ entries, y, width, marginLeft, color }) => (
<React.Fragment>
{entries.map(([min, max], i) => (
<rect
x={marginLeft + width(min)}
width={width(max) - width(min)}
y={y(y.domain()[i])}
height={y.bandwidth()}
key={y.domain()[i]}
fill={color}
>
<title>
{min}, {max}
</title>
</rect>
))}
</React.Fragment>
)

These barchart subcomponents are fully controled components. They help us clean up the rendering and don't need any logic of their own.

Takes a list of entries to render, a y scale for vertical positioning, a width scale to calculate widths, some margin on the left for the big axis, and a color to use.

Renders a React Fragment with a bunch of rectangles. Loop over the entries, return a positioned rectangle for each.

Our entries are pairs of min and max values as calculated by the stack layout. We use them to decide the horizontal, x position of our rectangle, and its width. Using the width scale both times. That takes care of proper sizing for us.

That key prop is a little funny though.

The y scale is an ordinal scale. Its domain is a list of categories, which means we can get the name of each bar's category by picking the right index out of that array. Perfect for identifying our elements :)

A stack chart built with React and D3

Here's how all of that ties together šŸ‘‡

class StackChart extends React.Component {
y = d3
.scaleBand()
.domain(this.props.data.map((d) => d.category))
.range([0, this.props.height])
.paddingInner(0.1)
stack = d3.stack().keys(["young", "mid", "old"])
color = chroma.brewer.pastel1
colorScale = d3
.scaleOrdinal()
.domain(["šŸ§’ 18 to 29 years", "šŸ™ā€ā™‚ļø 30 to 59 years", "šŸ§“ 60 years or older"])
.range(this.color)
render() {
const { data } = this.props
const stack = this.stack(data)
const width = d3
.scaleLinear()
.domain([0, d3.max(stack[2], (d) => d[1])])
.range([0, 400])
return (
<g>
<VerticalAxis scale={this.y} x={220} y={0} />
{this.stack(data).map((entries, i) => (
<BarChart
entries={entries}
y={this.y}
key={i}
marginLeft={223}
color={this.color[i]}
width={width}
/>
))}
<Legend scale={this.colorScale} x={500} y={this.props.height - 100} />
</g>
)
}
}

Okay that's a longer code snippet šŸ˜…

D3 setup

In the beginning, we have some D3 objects.

  1. A y band scale. Handles vertical positioning, sizing, spacing, and all
  2. A stack generator with hardcoded keys. We know what we want and there's no need to be fancy
  3. A color list. Chroma's brewer.pastel1 looked Best
  4. A colorScale with a more verbose domain and our list of colors as the range

Having a separate list of colors and color scale is important. Our individual bars want a specific color, our legend wants a color scale. They use different domains and unifying them would be fiddly. Easier to keep apart.

render

We do a little cheating in our render method. That stack should be generated in a componentDidUpdate of some sort and so should the width linear scale.

But our data is small so it's okay to recalculate all this every time.

The stack generator creates a list of lists of entries. 3 lists, one for each category (age group). Each list contains pairs of numbers representing how they should stack.

Like this

[
[[0, 5], [0, 10]],
[[5, 7], [10, 16]],
[[13, 20], [26, 31]]
]

Entries in the first list all begin at 0. Second list begins where the previous list ends. Third list begins where the second list ended. Stacking up as far as you need.

Your job is then to take these numbers, feed them into some sort of scale to help with sizing, and render.

That was our <BarChart> sub component up above. It takes each list, feeds its values into a width scale, and renders.

Making sure we render 3 of them, one for each age group, is this part:

return (
<g>
<VerticalAxis scale={this.y} x={220} y={0} />
{stack.map((entries, i) => (
<BarChart
entries={entries}
y={this.y}
key={i}
marginLeft={223}
color={this.color[i]}
width={width}
/>
))}
<Legend scale={this.colorScale} x={500} y={this.props.height - 100} />
</g>
)

Starts by rendering an axis, followed by a loop through our stack, rendering a <BarChart> for each, and then the <Legend> component neatly positioned to look good.

A beautiful chart pops out.

Beautiful chart

Today you learned šŸ§

  • chroma-js exist and is amazing
  • d3-legend for easy legends
  • d3blackbox still saving the day
  • D3 stack generator

šŸ¤“

About the Author

Hi, Iā€™m Swizec Teller. I help coders become software engineers.

Story time šŸ‘‡

React+D3 started as a bet in April 2015. A friend wanted to learn React and challenged me to publish a book. A month later React+D3 launched with 79 pages of hard earned knowledge.

In April 2016 it became React+D3 ES6. 117 pages and growing beyond a single big project it was a huge success. I kept going, started live streaming, and publishing videos on YouTube.

In 2017, after 10 months of work, React + D3v4 became the best book I'd ever written. At 249 pages, many examples, and code to play with it was designed like a step-by-step course. But I felt something was missing.

So in late 2018 I rebuilt the entire thing as React for Data Visualization ā€” a proper video course. Designed for busy people with real lives like you. Over 8 hours of video material, split into chunks no longer than 5 minutes, a bunch of new chapters, and techniques I discovered along the way.

React for Data Visualization is the best way to learn how to build scalable dataviz components your whole team can understand.

Some of my work has been featured in šŸ‘‡

Created bySwizecwith ā¤ļø