Topic 9.2
Easing and Timing
In one line
Easing decides how fast a movement is at each moment: slow at the start, fast in the middle, or even past the target. Duration and delay decide how long it lasts and when each mark starts. Good motion is short, and it ends where the data says.
Think of it like this
A car pulling up to a red light. It does not drive at one speed and then stop dead. It slows down gradually and settles. A bike courier hurrying to a door does the opposite: bursts off, then brakes late. Same road, same length, different speed curve, and you read different moods from them. Easing is the speed curve of a movement.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- easing
- The rule for how quickly a movement goes at each moment of its duration: the speed curve.
- ease function
- A function that takes
tfrom 0 to 1 and returns the progress, also from 0 to 1 (or a little beyond it for some curves). - overshoot
- Going past the final value before coming back. Back and elastic easings do it.
- stagger
- Giving each element in a group a slightly later start than the one before it.
- delay
- Milliseconds to wait before an element's transition starts.
Step by step
01Read the curve before you pick it
The first example prints eight easings at six moments, and draws them. Time runs across, progress goes up. A straight line is easeLinear. An S is the default. A hump above the dotted line is an overshoot, and it is what a bar would do if you used it.
02Pick by what the mark is doing
A bar growing to a value: an out curve (easeCubicOut). A bar sliding to a new rank: in-out (the default). A row leaving: an in curve, or a fade only. Nothing that carries a number: back, elastic or bounce.
03Stagger by the count, not by a fixed step
The second example staggers seven bars with a 40 ms step and prints the start order, then prints the step for 7, 25, 100 and 1,000 bars. The step shrinks so that the whole group has started within 240 ms, whatever the count.
04Know what breaks quietly
The third example feeds the API wrong values. ease("cubic") throws an error with an empty message. A negative duration ends at once. A NaN delay never starts, and nothing says so.
Try it yourself
- 1
Overshoot a bar
Use the second example and add
.ease(d3.easeBackOut)after.duration(duration). The end width is the same. During the movement each bar is briefly longer than its final width, up to about 10% longer. Replace it withd3.easeCubicOutand the bars never pass the end. - 2
Choose the steepness yourself
In the first example replace
["easeExpOut", d3.easeExpOut]by["easePolyOut(5)", d3.easePolyOut.exponent(5)]. The new row reads0.00 0.41 0.76 0.97 1.00 1.00: with exponent 5 the movement is nearly finished at half the time. The exponent is a dial between a gentle curve (exponent 1 is linear) and an abrupt one. - 3
Remove the cap
In the second example add
console.log(stagger(3), stagger(3, 240, Infinity));at the end. The output is40 120. With only three bars an uncapped step is 120 ms: each bar would start as a separate event, and the group would feel slow. The cap (40 ms) is what keeps a short list lively, and thetotalis what keeps a long list short.
Code & diagrams
Time runs across, progress up. Back and elastic rise above the dotted line (the final value).
Preview
Console
Nothing logged yet.Expected console output
t 0 0.1 0.25 0.5 0.75 1
easeLinear 0.00 0.10 0.25 0.50 0.75 1.00
easeCubicInOut 0.00 0.00 0.06 0.50 0.94 1.00
easeCubicOut 0.00 0.27 0.58 0.88 0.98 1.00
easeCubicIn 0.00 0.00 0.02 0.13 0.42 1.00
easeBackOut 0.00 0.41 0.82 1.09 1.06 1.00
easeElasticOut 0.00 1.25 0.91 1.02 1.00 1.00
easeBounceOut 0.00 0.08 0.47 0.77 0.97 1.00
easeExpOut 0.00 0.50 0.82 0.97 1.00 1.00
every easing starts at 0 and ends at 1: true
goes past 1 on the way: easeBackOut reaches 1.10, easeElasticOut reaches 1.37Only the start times are logged, never the frame times: the order and the planned delays are the same on every machine.
Preview
Console
Nothing logged yet.Expected console output
step for 1, 7, 25, 100, 1000 bars: 0, 40, 10, 2.42, 0.24 ms
started in this order: Mon Tue Wed Thu Fri Sat Sun
last bar starts at 240 ms; the whole thing takes 540 msTwo of these are quiet failures: a negative duration acts like 0 and a NaN delay never starts, with no error.
Preview
Console
Nothing logged yet.Expected console output
ease with a name: Error: ""
ease with a number: Error
duration -100: ended, width 50
height transition ended
NaN-delay bar after everything else finished, width: 10 | its schedule is still stored: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Give ease a name
In the third example, the first line already does it: bar.transition().ease("cubic"). D3 version 7 wants a function.
Break #2
Delay from a missing field
In the third example the NaN delay stands for d.rank * 40 where rank is missing from one row.
Myth vs fact
Myth
A bouncy easing makes a chart feel friendly.
Fact
It makes a bar longer than its value for a moment, and the reader may read that length. Keep overshoot out of anything that carries a number.
Myth
Longer animations look more polished.
Fact
Past about 600 ms the reader waits for the numbers. The best motion is the one nobody notices because they understood the change.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The easing is applied to
tonce per frame for the whole transition, and the interpolator is applied after. A colour fades with the same curve as a width. To give two attributes different curves, make two transitions with differentease, or usetransition.easeVarying(a different curve per element, chosen by a function). - ▸
A custom easing is any function with
f(0) === 0andf(1) === 1. On the last frame D3 does not call the easing at all: it usest = 1itself, so the end value is exact even when your formula has a rounding error near 1. (The first frame is different: an easing that returns something other than 0 att = 0makes the first written value differ from the start.)
Remember this
- 1
The problem this solves: a movement at constant speed looks mechanical, and a movement that is too slow wastes the reader's time. You need to say two things: how long, and how the speed changes. Without them every bar slides the same way, and every animation in the dashboard feels the same whether it shows good news or an error.
- 2
An easing function is a function from time to progress. It takes
tfrom 0 to 1 (the share of the duration that has passed) and returns a number: how far along the change is, also usually from 0 to 1.d3.easeLinearreturnst.d3.easeCubicOutreturns1 - (1 - t)³, which is 0.88 at the half-way moment: most of the movement happens early, then it settles. The first example prints these numbers. A transition then writesinterpolator(ease(t)). - 3
The names say where the speed is.
Inis slow at the start,Outis slow at the end,InOutis slow at both. The default iseaseCubicInOut, which is 0.06 at a quarter of the time and 0.94 at three quarters. Use out curves for things that arrive (a bar growing to its value, a tooltip appearing), in curves for things that leave, and in-out for things that move from one place to another (a bar sliding to a new rank). The families (Quad,Cubic,Poly,Sin,Exp,Circle) differ in how steep the middle is.easePolyOut.exponent(5)lets you choose. - 4
Every easing starts at 0 and ends at 1, but not all stay inside.
easeBackOutgoes up to 1.10 and comes back.easeElasticOutreaches 1.37 and wobbles.easeBounceOutbounces on the way. For a bar that means it is for a moment longer than its value: a bar showing 2,979 rides would briefly show about 3,277. In a chart, that is a small lie told on purpose. Overshooting easings are for toys and badges, not for the lengths that carry numbers. - 5
Duration: as short as the eye can follow. The default is 250 ms. For one mark moving a short way, 200 to 300 ms. For the whole chart changing, 400 to 600 ms. Over a second makes the reader wait for numbers. Remember that a hundred-bar chart does not need a longer duration than a seven-bar chart: the eye follows the group.
- 6
Delay and stagger.
.delay(ms)waits before each element starts. A function(d, i) => i * 40staggers: bar 0 starts at once, bar 1 after 40 ms, and so on. It makes the group's order visible, and it makes a long group take long: 100 bars at 40 ms would take 4 seconds to start. So compute the step from the count (total ÷ (count − 1), capped), as the second example does. Each bar's delay is read once, when the transition is created. - 7
Too much motion is a failure mode. A chart that animates on every hover, loops, or wiggles after it has arrived draws the eye away from the data. Animate changes the reader caused or needs to track, once, then stay still. Topic 9.5 adds the setting some people use to turn motion off.
Explain it without notes
What does an easing function take and return?
Which easing would you pick for a bar growing to its value, and which for a bar that must not pass its value?
Why should a stagger step depend on the number of marks?
Practice
Write fixedStep(count) as above, then use it for the bars of the second example with d3.easeCubicOut. Log the time the last bar starts and the time the transition ends.
Write safeDelay(d, i, step) that uses d.rank when it is a finite number and i otherwise. Test it with { rank: 3 }, {} and { rank: NaN } and step 40.
Trade-offs
- ↔
Staggering shows order and makes a group read as a sequence, and it makes the whole movement longer. A short cap keeps it a hint rather than a wait.
Done when you can
I can say what an easing function takes and returns.
I can choose in, out or in-out for a mark that arrives, leaves or moves.
I know which D3 easings overshoot, and why a bar should not use them.
I can compute a stagger step from the number of marks.
I know what a
NaNdelay and a stringeasedo.