Color Scales

Ordinal, Sequential and Diverging Color Scales

d3.scaleOrdinal(domain, colors) gives categories fixed colors (BookNest's palette, so a book keeps its color on every chart). d3.scaleSequential(domain, interpolator) maps numbers through a d3-scale-chromatic ramp, and d3.scaleDiverging([min, mid, max], interpolator) runs two ramps away from a meaningful midpoint:

The three color scale families on BookNest's data (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="96" font-family="Arial" font-size="13"></svg>
<script>
  const book = d3.scaleOrdinal(SALES.books.map(b => b.key), SALES.books.map(b => b.color));
  const units = d3.rollups(SALES.sales, v => d3.sum(v, d => d.units), d => d.month)
    .map(d => d[1]);                                                  // 20 monthly totals
  const change = units.slice(1).map((u, i) => u - units[i]);      // month over month
  const seq = d3.scaleSequential(d3.extent(units), d3.interpolateYlGnBu);
  const m = d3.max(change, Math.abs), div = d3.scaleDiverging([-m, 0, m], d3.interpolateRdBu);
  const strip = (row, label, values, fill) => {
    const g = d3.select('svg').append('g').attr('transform', `translate(0, ${row * 32})`);
    g.append('text').attr('y', 18).text(label);
    const w = 520 / values.length;
    g.selectAll('rect').data(values).join('rect').attr('height', 26).attr('fill', fill)
      .attr('x', (d, i) => 90 + i * w).attr('width', w - 1);
  };
  strip(0, 'ordinal', book.domain(), book);  strip(1, 'sequential', units, seq);
  strip(2, 'diverging', change, div);
</script>
Browser output of Listing 6.12
Browser output of 12

The sequential strip runs from pale (January 2025) to deep blue (December 2025); the diverging strip shows each month's change, red for a fall and blue for a rise, around a domain made symmetric by the largest change. interpolateViridis and interpolateCividis stay readable with color-vision deficiencies (Colorblind-Safe Palettes). An ordinal scale given an unexpected value adds it to its domain with the next color, unless you set unknown().

Ordinal, sequential and diverging color scales as three strips, with a choice of interpolatorHTMLLive
<!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>Sequential interpolator: <select id="seq"><option>interpolateYlGnBu</option><option>interpolateViridis</option>
  <option>interpolateCividis</option><option>interpolateOranges</option></select></p>
<svg viewBox="0 0 600 130" font-size="12"></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 };
  })();

  const book = d3.scaleOrdinal(SALES.books.map(b => b.key), SALES.books.map(b => b.color));
  const units = d3.rollups(SALES.sales, v => d3.sum(v, d => d.units), d => d.month).map(d => d[1]);
  const change = units.slice(1).map((u, i) => u - units[i]);           // month over month
  const m = d3.max(change, Math.abs);
  const div = d3.scaleDiverging([-m, 0, m], d3.interpolateRdBu);        // symmetric around 0

  function strip(row, label, values, fill, text) {
    const g = d3.select('svg').selectAll(`g.row${row}`).data([null]).join('g').attr('class', `row${row}`)
      .attr('transform', `translate(0, ${row * 42})`);
    g.selectAll('text.l').data([label]).join('text').attr('class', 'l').attr('y', 18).text(d => d);
    const w = 500 / values.length;
    g.selectAll('rect').data(values).join('rect').attr('height', 26).attr('fill', fill)
      .attr('x', (d, i) => 90 + i * w).attr('width', w - 1)
      .selectAll('title').data(d => [d]).join('title').text(text);
  }
  function draw() {
    const seq = d3.scaleSequential(d3.extent(units), d3[d3.select('#seq').property('value')]);
    strip(0, 'ordinal', book.domain(), book, d => d);
    strip(1, 'sequential', units, seq, d => `${d} units`);
    strip(2, 'diverging', change, div, d => `${d > 0 ? '+' : ''}${d} vs previous month`);
  }
  d3.select('#seq').on('change', draw);
  draw();
</script>