10. Hello D3 — Selections, Scales, and Data Binding

In Module 09 you learned why SVG and Canvas have different performance envelopes. Now you will put SVG to work with D3.js — a library that sits between raw Canvas drawing (Modules 01–02) and declarative Chart.js (Modules 05–08). D3 does not draw a chart for you. Instead, it provides powerful utilities that bind your data to DOM elements and let you set their attributes. You decide what to draw and how to draw it. In exchange for that extra control, you get the ability to create any visualization you can imagine.

Connection to the Overview: This module implements the imperative, DOM-based rendering approach described in Section 9 of the Data Visualization Overview. D3 outputs SVG elements that live in the DOM, giving you native CSS styling, event handling, and accessibility — unlike Canvas, which produces an opaque bitmap. Section 8 explains why SVG is the right rendering surface for the element counts used in these modules.

Demo Files

Run: Open the HTML files directly in a browser. Each file loads D3 v7 from a CDN — no install required.

1. The D3 Mental Model

D3 stands for Data-Driven Documents. The core idea is a three-step loop:

┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ 1. SELECT │ ──► │ 2. BIND │ ──► │ 3. JOIN │ │ │ │ │ │ │ │ d3.select() │ │ .data(array) │ │ .join('element') │ │ d3.selectAll│ │ │ │ │ │ │ │ Associates │ │ Creates/updates/ │ │ Target a │ │ data items │ │ removes DOM nodes │ │ container │ │ with DOM │ │ to match the data │ │ or elements │ │ elements │ │ │ └─────────────┘ └──────────────┘ └──────────────────────┘ After the join, each DOM element has a datum attached. You can set attributes as functions of that datum: .attr('width', d => xScale(d.value)) .style('fill', d => colorScale(d.category))

This is the enter/update/exit pattern. When the number of data items differs from the number of DOM elements:

The modern .join() method (D3 v5+) handles all three cases in one call. You will see this in every example below.

2. SVG Basics

D3 typically outputs SVG (Scalable Vector Graphics) rather than Canvas. SVG elements are real DOM nodes — you can inspect them in DevTools, style them with CSS, and attach event listeners directly. Here are the SVG elements you need for charting:

ElementPurposeKey Attributes
<svg>Container (the "canvas")width, height, viewBox
<rect>Rectangle (bars)x, y, width, height, fill
<line>Straight linex1, y1, x2, y2, stroke
<text>Text labelx, y, text-anchor, dominant-baseline
<circle>Data point dotcx, cy, r, fill
<g>Group (like a div)transform="translate(x, y)"
<path>Arbitrary shaped (path data string)

The SVG Coordinate System

SVG shares the same coordinate system as Canvas: origin at the top-left, y increases downward. The viewBox attribute lets you define a logical coordinate space independent of the element's physical pixel size:

<!-- Physical size: 600x400 CSS pixels -->
<!-- Logical coordinate space: 0,0 to 600,400 -->
<svg width="600" height="400" viewBox="0 0 600 400">

  <!-- A teal rectangle at x=50, y=50, 200px wide, 100px tall -->
  <rect x="50" y="50" width="200" height="100" fill="#16a085" />

  <!-- A label centered at x=150, y=200 -->
  <text x="150" y="200" text-anchor="middle"
        font-family="sans-serif" font-size="14">Hello SVG</text>

  <!-- A group translated to act like a local coordinate system -->
  <g transform="translate(300, 100)">
    <rect width="100" height="80" fill="#e74c3c" />
    <text y="95" font-size="12">Inside the group</text>
  </g>

</svg>
The <g> element is essential. D3 charts use <g> with a translate() transform to implement the margin convention. Instead of adding margin offsets to every coordinate, you create a group shifted by the margin amounts and draw everything inside it using zero-based coordinates.

3. D3 Selections

A D3 selection is a wrapped reference to one or more DOM elements, similar to jQuery but designed for data binding. Two core methods:

// Select the first matching element (like querySelector)
const header = d3.select('h1');

// Select ALL matching elements (like querySelectorAll)
const paragraphs = d3.selectAll('p');

// Chain modifications
d3.select('#chart')
  .style('border', '1px solid #ccc')
  .attr('class', 'active')
  .text('Updated!');

Selections return the selection itself, so every method chains. This fluent API is how D3 code reads like a pipeline: select something, then transform it step by step.

Try the Selections Demo to see d3.select(), d3.selectAll(), and .data().join() in action with visual feedback.

4. Data Binding: .data().join()

Data binding is the heart of D3. You take a selection, attach an array of data, and D3 figures out which elements need to be created, updated, or removed:

const data = [120, 340, 210, 450, 175];

// Select all <rect> elements inside the SVG (initially none exist)
// Bind the data array
// .join('rect') creates a <rect> for each datum
d3.select('svg')
  .selectAll('rect')
  .data(data)
  .join('rect')
    .attr('x', (d, i) => i * 60)
    .attr('y', d => 300 - d)
    .attr('width', 50)
    .attr('height', d => d)
    .attr('fill', '#16a085');

The callback (d, i) => receives two arguments: d is the datum (the data value bound to this element) and i is the index. This is how every attribute becomes a function of the data.

data = [120, 340, 210, 450, 175] Before .join(): 0 <rect> elements in the DOM After .join(): 5 <rect> elements, each with a bound datum Element 0: datum = 120 → height = 120, y = 180 Element 1: datum = 340 → height = 340, y = -40 Element 2: datum = 210 → height = 210, y = 90 Element 3: datum = 450 → height = 450, y = -150 Element 4: datum = 175 → height = 175, y = 125 If data changes to [120, 340, 210]: Elements 0-2: UPDATE (attributes may change) Elements 3-4: EXIT (removed from DOM)
D3 is not a charting library — it is a set of utilities for transforming data into DOM elements. It does not have a "bar chart" function or a "line chart" function. Once you internalize the pattern of "select → bind data → set attributes," everything else follows. You decide what SVG elements to create and how their attributes map to your data. This is what makes D3 both powerful and initially unfamiliar.

5. D3 Scales

In Module 02 you wrote a linearScale() function by hand. D3 provides the same thing — and much more — as first-class scale objects. A D3 scale is a function: you call it with a data value and it returns a pixel value.

// Linear scale: maps a continuous domain to a continuous range
const yScale = d3.scaleLinear()
    .domain([0, 1250])     // data values: 0 to max pageviews
    .range([300, 0]);       // pixel values: bottom to top (inverted!)

yScale(0);      // → 300  (zero maps to bottom)
yScale(1250);   // → 0    (max maps to top)
yScale(625);    // → 150  (midpoint maps to middle)

// Band scale: maps discrete categories to evenly-spaced bands
const xScale = d3.scaleBand()
    .domain(['/home', '/about', '/products', '/contact', '/blog'])
    .range([0, 500])
    .padding(0.2);          // 20% of each band is gap

xScale('/home');            // → 10  (start of first band)
xScale.bandwidth();         // → 80  (width of each band)

// Ordinal scale: maps categories to specific output values
const colorScale = d3.scaleOrdinal()
    .domain(['/home', '/about', '/products', '/contact', '/blog'])
    .range(['#16a085', '#2980b9', '#8e44ad', '#e67e22', '#e74c3c']);

The critical insight: D3 scales are just functions. You configure them with .domain() and .range(), then call them like any function. This is identical to the hand-rolled linearScale() from Module 02, but D3 adds features like .nice() (rounds domain to friendly values), .ticks() (generates tick values), and .invert() (pixel back to data value).

Explore the Scales Demo to interactively compare the manual scale from Module 02 with D3's scaleLinear() and scaleBand().

Scale TypeInputOutputUse Case
scaleLinear()Continuous numberContinuous numberY-axis (pageviews → pixels)
scaleBand()Discrete categoryBand position + widthX-axis for bar charts
scaleOrdinal()Discrete categoryAny discrete valueMapping categories to colors
scaleTime()Date objectContinuous numberTime-series x-axis
scaleLog()Continuous numberContinuous numberData with huge range

6. D3 Axes

Drawing axis lines, tick marks, and labels by hand (as you did in Module 01) is tedious. D3 provides axis generators that do it all automatically:

// Create axis generators from your scales
const xAxis = d3.axisBottom(xScale);  // ticks below the line
const yAxis = d3.axisLeft(yScale);    // ticks to the left

// Append a <g> element and call the axis generator on it
svg.append('g')
    .attr('transform', `translate(0, ${chartHeight})`)
    .call(xAxis);

svg.append('g')
    .call(yAxis);

The .call() method passes the selection to the axis generator, which populates it with <line>, <text>, and <path> elements forming the complete axis. The axis reads the scale's domain and automatically generates appropriate tick values and labels.

d3.axisBottom(xScale) generates: <g class="tick" transform="translate(50, 0)"> <line y2="6"></line> <text y="9" dy="0.71em">/home</text> </g> <g class="tick" transform="translate(150, 0)"> <line y2="6"></line> <text y="9" dy="0.71em">/about</text> </g> ... one <g class="tick"> per scale domain value You get: axis line + tick marks + labels For free: correct spacing from the scale's range

7. The Margin Convention in D3

Just as in Module 01, D3 charts use a margin convention. But instead of manually adding offsets to every coordinate, D3 uses a <g> element with a translate() transform:

const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const width = 600 - margin.left - margin.right;    // chart area width
const height = 400 - margin.top - margin.bottom;    // chart area height

// Create the SVG at full size
const svg = d3.select('#chart')
  .append('svg')
    .attr('width', 600)
    .attr('height', 400);

// Create a group shifted by the margins — everything draws inside this
const g = svg.append('g')
    .attr('transform', `translate(${margin.left}, ${margin.top})`);

// Now coordinates are relative to the chart area:
// (0, 0) = top-left of the chart area
// (width, height) = bottom-right of the chart area

8. Putting It Together: A Bar Chart in D3

Here is the complete pattern for a D3 bar chart, using the same pageview data from Module 01. See the full working version in d3-bar-chart.html.

const data = [
    { page: '/home',     views: 1250 },
    { page: '/about',    views: 430 },
    { page: '/products', views: 890 },
    { page: '/contact',  views: 320 },
    { page: '/blog',     views: 675 }
];

// 1. Scales
const xScale = d3.scaleBand()
    .domain(data.map(d => d.page))
    .range([0, width])
    .padding(0.2);

const yScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.views)])
    .nice()
    .range([height, 0]);

const colorScale = d3.scaleOrdinal()
    .domain(data.map(d => d.page))
    .range(d3.schemeTableau10);

// 2. Axes
g.append('g')
    .attr('transform', `translate(0, ${height})`)
    .call(d3.axisBottom(xScale));

g.append('g')
    .call(d3.axisLeft(yScale));

// 3. Bars — the data binding pattern
g.selectAll('rect')
  .data(data)
  .join('rect')
    .attr('x', d => xScale(d.page))
    .attr('y', d => yScale(d.views))
    .attr('width', xScale.bandwidth())
    .attr('height', d => height - yScale(d.views))
    .attr('fill', d => colorScale(d.page));

Count the lines: scales, axes, and data-bound bars in about 30 lines of code. The Canvas version from Module 01 needed roughly 80 lines to achieve the same result, and the Chart.js version from Module 05 was about 20 lines but with far less control over the output.

9. Three Approaches Compared

By now you have seen the same bar chart built three ways. Here is how they compare:

Aspect Canvas (Module 01) Chart.js (Module 05) D3 (This Module)
Paradigm Imperative pixel drawing Declarative config object Imperative data binding
Output Bitmap (pixels) Bitmap (Canvas internally) SVG (DOM elements)
Scale functions Hand-rolled Automatic (hidden) Explicit (d3.scaleLinear)
Axes Hand-drawn with loops Automatic from config d3.axisBottom() / axisLeft()
CSS styling Not possible Not possible Full CSS support
Interactivity Manual hit-testing Built-in tooltips Native DOM events per element
Lines of code (bar chart) ~80 ~20 ~30
Custom visualization Anything (low-level) Limited to chart types Anything (high-level primitives)
Performance limit ~100K data points ~100K (same Canvas engine) ~5K–10K SVG elements
Best for Performance (many points) Standard dashboard charts Custom, interactive, unique visualizations
When to use which: Use Chart.js when you need standard chart types quickly (bar, line, doughnut) and the built-in options cover your design needs. Use D3 when you need a visualization that no charting library provides out of the box — force-directed graphs, geographic maps, custom layouts, or anything involving non-standard data-to-visual mappings. Use raw Canvas when you need maximum rendering performance with tens of thousands of data points.

10. D3 v7 Module Structure

D3 v7 is modular. The full bundle (d3) includes everything, but in production you can import only the modules you need:

<!-- Full bundle from CDN (simplest — what we use in this tutorial) -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>

<!-- Or import specific modules (tree-shakeable in bundled projects) -->
<script type="module">
import { select, selectAll } from 'd3-selection';
import { scaleLinear, scaleBand } from 'd3-scale';
import { axisBottom, axisLeft } from 'd3-axis';
import { max } from 'd3-array';
</script>

The modules we used in this tutorial:

11. Common Mistakes

  1. Selecting elements that don't exist yet: selectAll('rect').data(data).join('rect') works even when there are zero <rect> elements — the enter selection creates them. This is counterintuitive but correct.
  2. Forgetting the <g> transform: If your chart draws at the very top-left of the SVG, you forgot to create the margin group with translate().
  3. Bar height calculation: The height of a bar is chartHeight - yScale(value), not yScale(value). Since y=0 is the top, yScale(value) gives the top of the bar and the height extends downward to the baseline.
  4. Mixing up scaleBand and scaleLinear: Use scaleBand() for categorical axes (page names, months) and scaleLinear() for numeric axes (pageview counts).
  5. Calling axis generators incorrectly: Use .call(axisBottom(xScale)) on a <g> element, not on the SVG directly.

12. Summary

13. Exercises

  1. Horizontal bar chart: Modify d3-bar-chart.html to draw horizontal bars. Swap the scales: scaleBand() on the y-axis, scaleLinear() on the x-axis.
  2. Value labels: Add a <text> element above each bar showing the exact pageview count.
  3. Color by threshold: Change the fill color based on the data: green for views > 800, orange for 400–800, red for < 400.
  4. Data update: Add a button that generates random data and calls .data(newData).join('rect') again. Watch D3 update the bars.