A Chart in Plot

A BookNest Chart in a Few Lines of Plot

Areas stacked BookNest's monthly units in a dozen lines of d3.index, d3.stack, scales, an area generator and axes, with no legend. In Plot 40,840 the stack is implicit in rectY, interval: 'month' makes each date a month-wide bar on a time axis, and tip: true adds a pointer tooltip:

Monthly units stacked by book, with a legend and tooltips (sample data)HTML
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/dist/plot.umd.min.js">
</script>
<script src="booknest-sales.js"></script><script src="booknest-prep.js"></script>
<div id="chart"></div>
<script>
  const { books, sales } = prepareSales(SALES), title = d => d.book.title;
  document.getElementById('chart').append(Plot.plot({
    width: 620, height: 150, y: { label: 'Units per month', grid: true },
    color: { domain: books.map(b => b.title), range: books.map(b => b.color), legend: true },
    marks: [Plot.rectY(sales, { x: 'date', interval: 'month', y: 'units', fill: title,
      insetLeft: 1, tip: true }), Plot.ruleY([0])]
  }));
</script>
Browser output of Listing 6.59
Browser output of 59

Small Steps to Big Summits (green) joins in March 2026 with no special case. Keyed Joins's sorted bar chart is one mark, Plot.barX(sales, Plot.groupY({ x: 'sum' }, { y: title, x: 'units', sort: { y: '-x' } })): it sums the raw rows per book and orders the bars from 1,218 units down to 454.

Monthly units stacked by book in one Plot.rectY mark, with a color legend and pointer tipsHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/dist/plot.umd.min.js"></script>
<p>View: <label><input type="radio" name="v" value="stack" checked> stacked months</label>
  <label><input type="radio" name="v" value="sum"> sorted totals (Plot.groupY)</label></p>
<div id="chart"></div>
<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, sales } = prepareSales(SALES), title = d => d.book.title;
  const color = { domain: books.map(b => b.title), range: books.map(b => b.color) };
  function draw(view) {
    const chart = view === 'stack'
      // The stack is implicit in rectY; interval: 'month' makes each date a month-wide bar
      ? Plot.plot({ width: 580, height: 200, y: { label: 'Units per month', grid: true },
          color: { ...color, legend: true },
          marks: [Plot.rectY(sales, { x: 'date', interval: 'month', y: 'units', fill: title, insetLeft: 1, tip: true }),
            Plot.ruleY([0])] })
      // The sorted bar chart is one mark: sum the raw rows per book, order bars by the sum
      : Plot.plot({ width: 580, height: 200, marginLeft: 170, x: { label: 'Units sold', grid: true }, color,
          marks: [Plot.barX(sales, Plot.groupY({ x: 'sum' }, { y: title, x: 'units', fill: title, sort: { y: '-x' }, tip: true })),
            Plot.ruleX([0])] });
    document.getElementById('chart').replaceChildren(chart);
  }
  document.querySelectorAll('input[name=v]').forEach(r => r.addEventListener('change', () => draw(r.value)));
  draw('stack');
</script>