Time Scales for Dates

d3.scaleTime() (local time) and d3.scaleUtc() are linear scales over Dates whose ticks fall on calendar boundaries, with a default format that prints the year at January and month names elsewhere. Parse and scale date-only data such as "2025-01" in UTC, so a reader in another time zone never sees a month slip back:

Monthly units, all books, on a UTC time scale (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>
<svg width="620" height="148" font-family="Arial"></svg>
<script>
  const parse = d3.utcParse('%Y-%m');
  const months = d3.rollups(SALES.sales, v => d3.sum(v, d => d.units), d => d.month)
    .map(([month, units]) => ({ date: parse(month), units }));    // Section 6.5.3
  const x = d3.scaleUtc([parse('2025-01'), parse('2026-09')], [34, 610]);
  const y = d3.scaleLinear([0, d3.max(months, d => d.units)], [124, 6]).nice();
  const svg = d3.select('svg');
  svg.selectAll('rect').data(months).join('rect').attr('fill', '#1f5f8b')
    .attr('x', d => x(d.date) + 1).attr('y', d => y(d.units))
    .attr('width', d => x(d3.utcMonth.offset(d.date, 1)) - x(d.date) - 2)
    .attr('height', d => 124 - y(d.units));
  svg.append('g').attr('transform', 'translate(0,124)').call(d3.axisBottom(x));
  svg.append('g').attr('transform', 'translate(34,0)').call(d3.axisLeft(y).ticks(4));
</script>
Browser output of Listing 6.11
Browser output of 11

Each bar spans its own month (February's is narrower): d3.utcMonth.offset(date, 1) finds the next boundary, and intervals such as d3.utcMonth.every(3) can also be passed to axis.ticks(). The March 2026 jump is Small Steps to Big Summits launching, in the invented data.

Monthly units on a UTC time scale: each bar spans its own calendar monthHTMLLive
<!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>
<svg viewBox="0 0 600 170"></svg>
<p>Hover a bar. Dates are parsed and scaled in UTC, so no reader sees a month slip back.</p>
<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 };
  })();

  const parse = d3.utcParse('%Y-%m');
  const months = d3.rollups(SALES.sales, v => d3.sum(v, d => d.units), d => d.month)
    .map(([month, units]) => ({ date: parse(month), units }));
  const x = d3.scaleUtc([parse('2025-01'), parse('2026-09')], [36, 590]);
  const y = d3.scaleLinear([0, d3.max(months, d => d.units)], [140, 10]).nice();
  const svg = d3.select('svg');
  svg.selectAll('rect').data(months).join('rect').attr('fill', '#1f5f8b')
    .attr('x', d => x(d.date) + 1).attr('y', d => y(d.units))
    .attr('width', d => x(d3.utcMonth.offset(d.date, 1)) - x(d.date) - 2)   // to the next boundary
    .attr('height', d => 140 - y(d.units))
    .append('title').text(d => `${d3.utcFormat('%B %Y')(d.date)}: ${d.units} units`);
  // Ticks fall on calendar boundaries: the year at January, month names elsewhere
  svg.append('g').attr('transform', 'translate(0,140)').call(d3.axisBottom(x));
  svg.append('g').attr('transform', 'translate(36,0)').call(d3.axisLeft(y).ticks(4));
  svg.append('text').attr('x', x(parse('2026-03')) + 4).attr('y', y(months.find(m => +m.date === +parse('2026-03')).units) - 6)
    .attr('fill', '#3f7d3a').text('Summit launches');
</script>