Candy is delicious. When do people buy it most? Visualize the data in a fun way
Dataset: Download dataset š³My solution š
How it works āļø
Did you know Americans buy Eight hundred million dollars
worth of candy on Easter? That's crazy. Absolutely bonkers. Even the normal baseline of $300,000,000
/week throughout the year is just staggering. š
What better way to visualize it than candy falling from the sky into the shape of a bar chart?
The basic idea behind that visualization goes like this:
- Load and parse data
- Scale for horizontal position
- Scale for vertical height
- Render each bar in a loop
- Divide height by
12
- Render that many emojis
- Create a custom tween transition to independently animate horizontal and vertical positionioning in a declarative and visually pleasing way
š
The basics
Let's start with the basics and get them out of the way. Bottom up in the Codesandbox above.
const FallingCandy = ({ data, x = 0, y = 0, width = 600, height = 600 }) => {const xScale = d3.scalePoint().domain(data.map((d) => d.week)).range([0, width])const yScale = d3.scaleLinear().domain([250, d3.max(data, (d) => d.sales)]).range([height, 0])return (<g transform={`translate(${x}, ${y})`}>{data.map((d) => (<CandyJarx={xScale(d.week)}y={height}height={height - yScale(d.sales)}delay={d.week * Math.random() * 100}type={d.special}key={d.week}/>))}<BottomAxis scale={xScale} x={0} y={height} /><LeftAxis scale={yScale} x={0} y={0} /></g>)}
The <FallingCandy>
component takes data, positioning, and sizing props. Creates two scales: A point scale for horizontal positioning of each column, a vertical scale for heights.
Render a grouping element to position everything, walk through the data and render a <CandyJar>
component for each entry. Candy jars need coordinates, a height, some delay for staggered animations, and a type.
Type tells them which emoji to render. Makes it so we can have special harts on Valentine's day, bunnies on Easter, jack-o-lanterns on Halloween, and Christmas trees on Christmas.
I know this works because when my girlfriend saw it this morning she was like "Whaaat why so much candy on Easter?". Didn't even have to tell her what the emojis mean šŖ
We'll talk about the animation staggering later. I'll explain why it has to be random as well.
The axes
Using our standard approach for axes: use d3blackbox to render an anchor element, then take over with D3 and use an axis generator.
const BottomAxis = d3blackbox((anchor, props) => {const scale = props.scalescale.domain(scale.domain().filter((_, i) => i % 5 === 0))const axis = d3.axisBottom().scale(props.scale).tickFormat((d) => `wk ${d}`)d3.select(anchor.current).call(axis)})const LeftAxis = d3blackbox((anchor, props) => {const axis = d3.axisLeft().scale(props.scale).tickFormat((d) => `$${d} million`)d3.select(anchor.current).call(axis)})
We have to filter the scale's domain for <BottomAxis>
because point scales are ordinal. That means there's no generalized way to interpolate values in between other values, so the axis renders everything.
That looks terrible. Instead, we render every 5th tick.
Both axes get a custom tickFormat
so they're easier to read.
The <CandyJar>
Candy jars are just columns of emojis. There's not much logic here.
const CandyJar = ({ x, y, height, delay, type }) =>d3.range(height / 12).map((i) => (<Candyx={x}y={y - i * 12}type={type}delay={delay + i * Math.random() * 100}key={i}/>))
Yes, we could have done this in the main <FallingCandy>
component. Code feels cleaner this way.
Create a counting array from zero to height/12
, the number of emojis we need, walk through the array and render <Candy>
components for each entry. At this point we add some more random delay. I'll tell you why in a bit.
The animated <Candy> component
All that animation happens in the Candy component. Parent components are blissfully unaware and other than passing a delay
prop never have to worry about the details of rendering and animation.
That's the beauty of declarative code. š
Our plan is based on my Declarative D3 transitions with React 16.3+ approach:
- Move coordinates into state
- Render emoji from state
- Run transition on
componentDidMount
- Update state when transition ends
We use component state as a sort of staging area for transitionable props. D3 helps us with what it does best - transitions - and React almost always knows what's going on so it doesn't get confused.
Have had issues in the past with manipulating the DOM and React freaking out at me.
class Candy extends React.Component {state = {x: Math.random() * 600,y: Math.random() * -50,}candyRef = React.createRef()componentDidMount() {const { delay } = this.propsconst node = d3.select(this.candyRef.current)node.transition().duration(1500).delay(delay).ease(d3.easeLinear).attrTween("y", candyYTween(this.state.y, this.props.y)).attr("x", this.props.x).on("end", () => this.setState({ y: this.props.y }))}get emoji() {// return emoji based on this.props.type}render() {const { x, y } = this.statereturn (<text x={x} y={y} style={{ fontSize: "12px" }} ref={this.candyRef}>{this.emoji}</text>)}}
We initate the <Candy>
component in a random location off screen. Too high up to be seen, somewhere on the visualization horizontally. Doesn't matter where.
I'll show you why random soon.
We create a ref as well. D3 will need that to get access to the DOM node.
Then we have componentDidMount
which is where the transition happens.
Separate, yet parallel, transitions for each axis
componentDidMount() {const { delay } = this.propsconst node = d3.select(this.candyRef.current)node.transition().duration(1500).delay(delay).ease(d3.easeLinear).attrTween('y', candyYTween(this.state.y, this.props.y)).attr('x', this.props.x).on('end', () => this.setState({ y: this.props.y }))}
Key logic here is that we d3.select()
the candy node, start a transition on it, define a duration, pass the delay from our props, disable easing functions, and specify what's transitioning.
The tricky bit was figuring out how to run two different transitions in parallel.
D3 doesn't do concurrent transitions, you see. You have to run a transition, then the next one. Or you have to cancel the first transition and start a new one.
Of course you can run concurrent transitions on multiple attributes. But only if they're both the same transition.
In our case we wanted to have candy bounce vertically and fly linearly in the horizontal direction. This was tricky.
I mean I guess it's okay with a bounce in both directions? š§
No that's weird.
You can do it with a tween
First you have to understand some basics of how transitions and easing functions work.
They're based on interpolators. An interpolator is a function that calculates in-between values between a start and end value based on a t
argument. When t=0
, you get the initial value. When t=1
you get the end value.
const interpolate = d3.interpolate(0, 100)interpolate(0) // 0interpolate(0.5) // 50interpolate(1) // 1
Something like that in a nutshell. D3 supports much more complex interpolations than that, but numbers are all we need right now.
Easing functions manipulate how that t
parameter behaves. Does it go from 0
to 1
linearly? Does it bounce around? Does it accelerate and slow down?
When you start a transition with easeLinear
and attr('x', this.props.x)
you are essentially creating an interpolator from the current value of x
to your desired value, and the t
parameter changes by an equal amount on every tick of the transition.
If you have 1500
milliseconds to finish the transition (your duration), that's 90 frames at 60fps. Means your t
adds 0.01 on every tick of the animation.
We can use that to create a custom tween for the vertical coordinate, y
.
function candyYTween(oldY, newY) {const interpolator = d3.interpolate(oldY, newY)return function () {return function (t) {return interpolator(d3.easeBounceOut(t))}}}
candyYTween
takes the initial and new coordinates, creates an interpolator, and returns a function. This function returns a parametrized function that drives our transition. For every t
we return the value of our interpolator
after passing it through the easeBounceOut
easing function.
We're basically taking a linear parameter, turning it into a bouncy paramater, and passing that into our interpolator. This creates a bouncy effect without affecting the x
coordinate in the other transition.
I don't know why we need the double function wrap, but it didn't work otherwise.
So why all the randomness?
Randomness makes our visualization look better. More natural.
Here's what it looks like without any Math.random()
Here's why adding randomness to your animations matters š
ā Swizec Teller (@Swizec) December 13, 2018
This chart of candy buying habits in the US is not random at all. Delay based purely on array index. pic.twitter.com/pTTWxovaSp
Randomness on the CandyJar level.
Here we add randomness to the column delay. pic.twitter.com/ZPfQzInXvi
ā Swizec Teller (@Swizec) December 13, 2018
Randomness on the CandyJar and Candy level.
Adding a random delay to each individual emoji makes it even better š§ pic.twitter.com/Xn49KRbcCy
ā Swizec Teller (@Swizec) December 13, 2018
Randomness in the start position as well.
And when you add a random start point as well, that's when you unlock true beauty š#ReactVizHoliday Day 9 was fun like that.
ā Swizec Teller (@Swizec) December 13, 2018
Check it out here š https://t.co/Yh62OVG3pW pic.twitter.com/5N2gQJtfUX
You decide which looks best āļø
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 š