web-dev4 min read

D3.js Tutorial: Learn Data Visualization from Scratch (2026)

D3.js Tutorial: Learn Data Visualization from Scratch (2026)

Published:  |  Category: Web Dev  |  Reading time: ~15 min
D3.js Tutorial: Learn Data Visualization from Scratch (2026)

D3.js (Data-Driven Documents) is a JavaScript library for producing dynamic, interactive data visualizations in the browser. Created by Mike Bostock in 2011 while at The New York Times, D3.js binds data to the DOM and applies data-driven transformations. Unlike charting libraries with pre-built chart types, D3 gives you low-level control over every visual element.

D3 works directly with SVG, Canvas, and HTML. Its declarative approach describes how data maps to visual properties, making complex visualizations like Sankey diagrams, chord charts, and geographic maps achievable with concise code.

Selections and Data Joins

D3 selections are the entry point. d3.select() and d3.selectAll() return selections operating on DOM nodes. The data join — via .data(), .enter(), .exit(), and .join() — binds data to DOM elements: enter creates new elements, update modifies existing, exit removes old.

The enter-update-exit pattern is the heart of D3's reactivity. The key function to .data() identifies data points across updates for stable element matching.

const data = [10, 25, 5, 40, 30];

const circles = d3.select('svg')
  .selectAll('circle')
  .data(data)
  .join('circle')
    .attr('cx', (d, i) => i * 60 + 30)
    .attr('cy', 100)
    .attr('r', d => d)
    .attr('fill', 'steelblue');

circles.data([25, 40, 30], d => d)
  .join(
    enter => enter.append('circle').attr('r', d => d),
    update => update.attr('fill', 'orange'),
    exit => exit.remove()
  );

Scales and Axes

Scales map data values (domain) to visual values (range). d3.scaleLinear maps continuous numbers, d3.scaleBand maps categorical data to widths, d3.scaleTime handles dates, and d3.scaleOrdinal maps discrete values to colors.

Axes are generated with d3.axisLeft(), d3.axisBottom(), and related functions. Call the axis generator on a g element to produce tick marks, labels, and grid lines. Tick count and format are customizable.

const x = d3.scaleBand()
  .domain(['A', 'B', 'C', 'D'])
  .range([0, width])
  .padding(0.1);

const y = d3.scaleLinear()
  .domain([0, 100])
  .range([height, 0]);

svg.append('g')
  .attr('transform', `translate(0,${height})`)
  .call(d3.axisBottom(x));

svg.append('g')
  .call(d3.axisLeft(y).ticks(5).tickFormat(d => d + '%'));

Building a Bar Chart

Bar charts showcase the data-join pattern. Each bar is a rect element with x from the band scale, width from bandwidth, y from the linear scale (SVG y increases downward), and height as height - y(value).

Labels are appended as text elements. Horizontal bar charts swap the scale roles. Grid lines use axisLeft() with tickSize(-width). Responsive charts re-render on container resize.

const dataset = [
  { name: 'Apples', value: 30 },
  { name: 'Bananas', value: 80 },
  { name: 'Cherries', value: 45 }
];

const x = d3.scaleBand()
  .domain(dataset.map(d => d.name))
  .range([0, width])
  .padding(0.2);

const y = d3.scaleLinear()
  .domain([0, d3.max(dataset, d => d.value)])
  .range([height, 0]);

svg.selectAll('.bar')
  .data(dataset)
  .join('rect')
    .attr('x', d => x(d.name))
    .attr('y', d => y(d.value))
    .attr('width', x.bandwidth())
    .attr('height', d => height - y(d.value))
    .attr('fill', '#4e79a7');

Transitions and Animation

D3 transitions animate changes over time. Call .transition() before attribute setters, and D3 interpolates between old and new values. Transition properties include duration(), delay(), and ease().

Staggered transitions delay elements sequentially with delay((d, i) => i * 20). The transition lifecycle has start, end, and interrupt events. Use d3.timer() for custom frame-by-frame animation.

svg.selectAll('.bar')
  .data(newData)
  .join('rect')
    .attr('class', 'bar')
    .attr('x', d => x(d.name))
    .attr('width', x.bandwidth())
  .transition()
    .duration(800)
    .ease(d3.easeElastic.period(0.4))
    .attr('y', d => y(d.value))
    .attr('height', d => height - y(d.value));

svg.selectAll('.dot')
  .data(points)
  .join('circle')
    .attr('r', 0)
  .transition()
    .delay((d, i) => i * 30)
    .duration(500)
    .attr('r', 5);

Interactive Visualizations

D3 makes interactivity straightforward with event listeners on SVG elements. Common interactions include hover tooltips, click-to-filter, brush selection, and zoom/pan. The on() method controls element-level events like mouseenter and mouseleave.

The d3-brush module enables rectangular selection for filtering. d3-zoom adds pan and zoom with configurable scale extent. Linked views — where brushing one chart updates another — demonstrate D3's reactive data model.

const tooltip = d3.select('body').append('div')
  .attr('class', 'tooltip')
  .style('position', 'absolute')
  .style('opacity', 0);

circles.on('mouseenter', function(event, d) {
  d3.select(this).attr('r', 8);
  tooltip.transition().duration(200).style('opacity', 1);
  tooltip.html(`Value: ${d.value}`)
    .style('left', (event.pageX + 10) + 'px')
    .style('top', (event.pageY - 28) + 'px');
})
.on('mouseleave', function() {
  d3.select(this).attr('r', 5);
  tooltip.transition().duration(200).style('opacity', 0);
});

Geographic Maps with GeoJSON

D3's d3-geo module renders geographic data using GeoJSON format. The d3.geoPath() generator converts GeoJSON geometry to SVG path data. Map projections like geoAlbersUsa, geoMercator, and geoOrthographic transform spherical coordinates to 2D.

Choropleth maps fill regions with color based on data values. Combine quantize scales with GeoJSON features. TopoJSON (more compact format) is converted to GeoJSON via topojson.feature().

const projection = d3.geoAlbersUsa()
  .fitSize([width, height], data);

const path = d3.geoPath().projection(projection);

const color = d3.scaleQuantize()
  .domain([0, 0.3])
  .range(d3.schemeBlues[9]);

svg.append('g')
  .selectAll('path')
  .data(topojson.feature(us, us.objects.counties).features)
  .join('path')
    .attr('d', path)
    .attr('fill', d => color(d.properties.unemployment))
    .attr('stroke', '#fff')
    .attr('stroke-width', 0.5);

Frequently Asked Questions

When should I use D3.js versus a charting library?

Choose D3 for custom, unique visualizations requiring precise control — network graphs, geographic maps, animated infographics. Use Chart.js for standard bar/line/pie charts where speed matters.

Does D3.js work with React or Vue?

Yes, but let D3 handle math (scales, layouts) and React/Vue manage the DOM. Use D3 scales within useMemo hooks. Libraries like @nivo and visx bridge D3 with React declaratively.

What is the D3.js general update pattern?

Select elements, bind data, then handle enter (new), update (existing), and exit (removed) phases. The .join() method in D3 v7+ simplifies this. Provide a key function for stable element identification.

How do I load external data in D3.js?

D3 provides d3.csv(), d3.json(), d3.tsv() returning promises. For large datasets, use fetch() with streaming. The d3-fetch module handles request headers and error handling.

Originally published on Ayodhyyya. Last updated June 1, 2026.