Chaining Transitions

Chaining, Delaying and Interrupting Transitions

delay(fn) staggers elements, and transition() called on a transition chains one that starts when the first ends. Transitions fire start, end, interrupt and cancel events (transition.end() wraps them in a promise), and selection.interrupt() stops the running transition and cancels queued ones:

Staggered, chained transitions interrupted at 700 ms (sample data)HTML
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<script src="booknest-sales.js"></script><script src="booknest-prep.js"></script>
<svg width="620" height="110"></svg>
<script>
  const { books } = prepareSales(SALES), y = d3.scaleLinear([0, 1300], [110, 0]);
  d3.select('svg').selectAll('rect').data(books).join('rect').attr('fill', '#cfd8dc')
      .attr('x', (d, i) => 10 + i * 100).attr('width', 80).attr('y', 110).attr('height', 0)
    .transition().duration(300).delay((d, i) => i * 100)                     // staggered
      .attr('y', d => y(d.units)).attr('height', d => 110 - y(d.units))
    .transition().duration(300).attr('fill', d => d.color);                  // then, chained
  d3.timeout(() => { d3.selectAll('rect').interrupt(); window.__done = true; }, 700);
</script>
Browser output of Listing 6.25
Browser output of 25

In this single frame, stopped at 700 ms, the first bars have finished both stages, the middle ones are changing color and the last still grows. A new transition interrupts the running one of the same name, so a hover effect named transition('hover') cannot cut short a data update.

Staggered bar growth chained to a color change, with replay and interruptHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
  svg { width: 100%; max-width: 600px; display: block; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<p><button id="play">Replay</button> <button id="stop">Interrupt</button> <span id="log"></span></p>
<svg viewBox="0 0 600 150" font-size="11"></svg>
<script>
  // BookNest sample data, invented for these examples (not real sales)
  const SALES = (() => {
    const books = [
      { id: 1, key: 'harbor', title: 'The Quiet Harbor', author: 'Elena Marsh', genre: 'Fiction', color: '#1f5f8b', price: 18.99, rating: 4.6 },
      { id: 2, key: 'deepweb', title: 'Patterns of the Deep Web', author: 'Ravi Menon', genre: 'Technology', color: '#5b3f99', price: 39.5, rating: 4.3 },
      { id: 3, key: 'saffron', title: 'Salt and Saffron', author: 'Leila Haddad', genre: 'Cooking', color: '#e09a10', price: 24, rating: 4.8 },
      { id: 4, key: 'summit', title: 'Small Steps to Big Summits', author: 'Tom Okafor', genre: 'Self-Help', color: '#3f7d3a', price: 14.99, rating: 4.1 },
      { id: 5, key: 'clock', title: "The Clockmaker's Paradox", author: 'Iris Vale', genre: 'Science Fiction', color: '#b5452f', price: 16.2, rating: 4.5 },
      { id: 6, key: 'glass', title: 'Gardens in Glass', author: 'June Park', genre: 'Gardening', color: '#2a9d8f', price: 22.5, rating: 4.4 },
    ];
    const units = {        // units sold per month, January 2025 to August 2026
      1: [40, 47, 46, 47, 57, 51, 46, 54, 60, 60, 71, 111, 52, 42, 49, 56, 59, 48, 53, 51],
      2: [40, 37, 35, 41, 48, 43, 40, 42, 50, 53, 53, 84, 42, 35, 38, 41, 45, 41, 46, 46],
      3: [46, 45, 41, 46, 53, 49, 46, 45, 60, 51, 63, 100, 48, 48, 49, 48, 53, 49, 48, 52],
      4: [100, 80, 72, 70, 66, 66],                    // released March 2026
      5: [48, 47, 54, 52, 65, 55, 57, 58, 64, 66, 75, 124, 58, 49, 59, 56, 63, 57, 54, 57],
      6: [31, 30, 28, 35, 31, 32, 29, 30, 41, 36, 45, 64, 31, 30, 30, 38, 40, 38, 36, 38],
    };
    const months = d3.utcMonths(new Date('2025-01-01'), new Date('2026-09-01')).map(d3.utcFormat('%Y-%m'));
    const sales = books.flatMap(b => units[b.id].map((n, i) =>
      ({ month: months[i + months.length - units[b.id].length], book: b.id, units: n })));
    const ratings = [[4, 8, 20, 87, 293], [6, 10, 22, 95, 147], [2, 3, 8, 45, 342], [2, 3, 8, 24, 28],
      [4, 8, 20, 90, 228], [4, 7, 20, 82, 147]].map((counts, i) => ({ book: i + 1, counts }));  // 1 to 5 stars
    const regions = [{ id: '826', name: 'United Kingdom', units: 1480 }, { id: '276', name: 'Germany', units: 1120 },
      { id: '250', name: 'France', units: 760 }, { id: '372', name: 'Ireland', units: 540 },
      { id: '528', name: 'Netherlands', units: 410 }, { id: '724', name: 'Spain', units: 320 }];
    const links = [[5, 1, 38], [1, 3, 24], [2, 5, 18], [3, 6, 21], [4, 1, 12], [2, 6, 9], [4, 3, 15], [5, 6, 14]]
      .map(([source, target, value]) => ({ source, target, value }));   // customers who bought both
    return { books, sales, ratings, regions, links };
  })();
  // prepareSales(raw): parsed dates, sales linked to book objects, revenue and totals
  function prepareSales(raw) {
    const parse = d3.utcParse('%Y-%m'), books = raw.books.map(b => ({ ...b }));
    const byId = d3.index(books, b => b.id);
    const sales = raw.sales.map(s => ({ date: parse(s.month), book: byId.get(s.book),
      units: s.units, revenue: s.units * byId.get(s.book).price }));
    for (const [book, rows] of d3.group(sales, s => s.book)) {
      book.units = d3.sum(rows, s => s.units);
      book.revenue = d3.sum(rows, s => s.revenue);
    }
    const months = d3.rollups(sales, v => ({ units: d3.sum(v, s => s.units),
      revenue: d3.sum(v, s => s.revenue) }), s => +s.date).map(([t, v]) => ({ date: new Date(t), ...v }))
      .sort((a, b) => a.date - b.date);
    const ratings = raw.ratings.map(r => ({ ...r, book: byId.get(r.book) }));
    return { ...raw, books, sales, months, ratings };
  }

  const { books } = prepareSales(SALES), y = d3.scaleLinear([0, 1300], [130, 0]);
  const svg = d3.select('svg'), log = d3.select('#log');
  svg.selectAll('text').data(books).join('text').attr('x', (d, i) => 50 + i * 100).attr('y', 146)
    .attr('text-anchor', 'middle').text(d => d.key);

  function play() {
    let ended = 0;
    svg.selectAll('rect').data(books).join('rect')
        .attr('fill', '#cfd8dc').attr('x', (d, i) => 10 + i * 100).attr('width', 80)
        .attr('y', 130).attr('height', 0)
      .transition().duration(600).delay((d, i) => i * 250)             // staggered
        .attr('y', d => y(d.units)).attr('height', d => 130 - y(d.units))
      .transition().duration(500).attr('fill', d => d.color)          // chained: starts when the first ends
        .on('end', () => { if (++ended === books.length) log.text('all transitions ended'); })
        .on('interrupt', () => log.text('interrupted: bars stop where they are'));
    log.text('growing, then coloring…');
  }
  d3.select('#play').on('click', play);
  d3.select('#stop').on('click', () => svg.selectAll('rect').interrupt());   // stops the running one, cancels queued ones
  play();
</script>