Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~22 min· topic 2 of 6

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 t from 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. 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 with d3.easeCubicOut and the bars never pass the end.

  2. 2

    Choose the steepness yourself

    In the first example replace ["easeExpOut", d3.easeExpOut] by ["easePolyOut(5)", d3.easePolyOut.exponent(5)]. The new row reads 0.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. 3

    Remove the cap

    In the second example add console.log(stagger(3), stagger(3, 240, Infinity)); at the end. The output is 40 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 the total is what keeps a long list short.

Code & diagrams

Eight easings, as numbers and as curves New tab

Time runs across, progress up. Back and elastic rise above the dotted line (the final value).

Preview

Press Run to see the page.

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.37
A staggered start that scales with the count New tab

Only the start times are logged, never the frame times: the order and the planned delays are the same on every machine.

Preview

Press Run to see the page.

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 ms
Wrong easing, duration and delay values New tab

Two of these are quiet failures: a negative duration acts like 0 and a NaN delay never starts, with no error.

Preview

Press Run to see the page.

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: true

Break 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.

terminal
$ Console
── what you'll see ──
ease with a name: Error: ""

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.

terminal
$ In the preview
── what you'll see ──
That bar never moves and no error appears: NaN-delay bar after everything else finished, width: 10

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 t once 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 different ease, or use transition.easeVarying (a different curve per element, chosen by a function).

  • ▸

    A custom easing is any function with f(0) === 0 and f(1) === 1. On the last frame D3 does not call the easing at all: it uses t = 1 itself, 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 at t = 0 makes the first written value differ from the start.)

Remember this

  1. 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. 2

    An easing function is a function from time to progress. It takes t from 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.easeLinear returns t. d3.easeCubicOut returns 1 - (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 writes interpolator(ease(t)).

  3. 3

    The names say where the speed is. In is slow at the start, Out is slow at the end, InOut is slow at both. The default is easeCubicInOut, 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. 4

    Every easing starts at 0 and ends at 1, but not all stay inside. easeBackOut goes up to 1.10 and comes back. easeElasticOut reaches 1.37 and wobbles. easeBounceOut bounces 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. 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. 6

    Delay and stagger. .delay(ms) waits before each element starts. A function (d, i) => i * 40 staggers: 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. 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

01

What does an easing function take and return?

02

Which easing would you pick for a bar growing to its value, and which for a bar that must not pass its value?

03

Why should a stagger step depend on the number of marks?

Practice

01

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.

02

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 NaN delay and a string ease do.