11 KiB
Getting Started with D3.js
Why Learn D3
Problem D3 Solves: Chart libraries (Chart.js, Highcharts, Plotly) offer pre-made chart types with limited customization. When you need bespoke visualizations—unusual chart types, specific interactions, custom layouts—you need low-level control. D3 provides primitive building blocks that compose into any visualization.
Trade-off: Steeper learning curve and more code than chart libraries, but unlimited customization and flexibility.
When D3 is the Right Choice:
- Custom visualization designs not available in libraries
- Complex interactions (linked views, custom brushing, coordinated highlighting)
- Network graphs, force-directed layouts, geographic maps
- Full control over visual encoding, transitions, animations
- Integration with data pipelines for real-time dashboards
Prerequisites
D3 assumes you know these web technologies:
Required:
- HTML: Document structure, elements, attributes
- SVG: Scalable Vector Graphics (
<svg>,<circle>,<rect>,<path>,viewBox, transforms) - CSS: Styling, selectors, specificity
- JavaScript: ES6 syntax, functions, arrays, objects, arrow functions, promises, async/await
Helpful:
- Modern frameworks (React, Vue) for integration patterns
- Data formats (CSV, JSON, GeoJSON)
- Basic statistics (distributions, correlations) for visualization design
If you lack prerequisites: Learn HTML/SVG/CSS/JavaScript fundamentals first. D3 documentation assumes this knowledge.
Setup
Option 1: CodePen (Recommended for Learning)
Quick start without tooling:
- Create HTML file (
index.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Visualization</title>
<style>
svg { border: 1px solid #ccc; }
</style>
</head>
<body>
<svg width="600" height="400"></svg>
<script type="module" src="index.js"></script>
</body>
</html>
- Create JavaScript file (
index.js):
// Import from Skypack CDN (ESM)
import * as d3 from 'https://cdn.skypack.dev/d3@7';
// Your D3 code here
console.log(d3.version);
- Open HTML in browser or use Live Server extension in VS Code
Option 2: Bundler (Vite, Webpack)
For production apps:
- Install D3:
npm install d3
- Import modules:
// Import specific modules (recommended - smaller bundle)
import { select, selectAll } from 'd3-selection';
import { scaleLinear, scaleBand } from 'd3-scale';
import { axisBottom, axisLeft } from 'd3-axis';
// Or import entire D3 namespace
import * as d3 from 'd3';
- Use in your code:
const svg = d3.select('svg');
Option 3: UMD Script Tag (Legacy)
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// d3 available globally
const svg = d3.select('svg');
</script>
Note: Modern approach uses ES modules (options 1-2).
Core Concepts
Concept 1: Selections
What: D3 selections are wrappers around DOM elements enabling method chaining for manipulation.
Why: Declarative syntax; modify multiple elements concisely.
Basic Pattern:
// Select single element
const svg = d3.select('svg');
// Select all matching elements
const circles = d3.selectAll('circle');
// Modify attributes
circles.attr('r', 10).attr('fill', 'steelblue');
// Add elements
svg.append('circle').attr('cx', 50).attr('cy', 50).attr('r', 20);
Methods:
.select(selector): First matching element.selectAll(selector): All matching elements.attr(name, value): Set attribute.style(name, value): Set CSS style.text(value): Set text content.append(type): Create and append child.remove(): Delete elements
Concept 2: Data Joins
What: Binding data arrays to DOM elements, establishing one-to-one correspondence.
Why: Enables data-driven visualizations; DOM automatically reflects data changes.
Pattern:
const data = [10, 20, 30, 40, 50];
svg.selectAll('circle')
.data(data) // Bind array to selection
.join('circle') // Create/update/remove elements to match data
.attr('cx', (d, i) => i * 50 + 25) // d = datum, i = index
.attr('cy', 50)
.attr('r', d => d); // Radius = data value
What .join() does:
- Enter: Creates new elements for array items without elements
- Update: Updates existing elements
- Exit: Removes elements without corresponding array items
Concept 3: Scales
What: Functions mapping data domain (input range) to visual range (output values).
Why: Data values (e.g., 0-1000) don't directly map to pixels. Scales handle transformation.
Example:
const data = [{x: 0}, {x: 50}, {x: 100}];
// Create scale
const xScale = d3.scaleLinear()
.domain([0, 100]) // Data min/max
.range([0, 500]); // Pixel min/max
// Use scale
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x)); // 0→0px, 50→250px, 100→500px
Common scales:
scaleLinear(): Quantitative, proportionalscaleBand(): Categorical, for barsscaleTime(): Temporal datascaleOrdinal(): Categorical → categories (colors)
Concept 4: Method Chaining
What: D3 methods return the selection, enabling sequential calls.
Why: Concise, readable code that mirrors workflow steps.
Example:
d3.select('svg')
.append('g')
.attr('transform', 'translate(50, 50)')
.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d, i) => i * 40)
.attr('y', d => height - d.value)
.attr('width', 30)
.attr('height', d => d.value)
.attr('fill', 'steelblue');
Reads like: "Select SVG → append group → position group → select rects → bind data → create rects → set attributes"
First Visualization: Simple Bar Chart
Goal
Create a vertical bar chart from array [30, 80, 45, 60, 20, 90, 50].
Setup SVG Container
// Dimensions
const width = 500;
const height = 300;
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Create SVG
const svg = d3.select('svg')
.attr('width', width)
.attr('height', height);
// Create inner group for margins
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
Why margins? Leave space for axes outside the data area.
Create Data and Scales
// Data
const data = [30, 80, 45, 60, 20, 90, 50];
// X scale (categorical - bar positions)
const xScale = d3.scaleBand()
.domain(d3.range(data.length)) // [0, 1, 2, 3, 4, 5, 6]
.range([0, innerWidth])
.padding(0.1); // 10% spacing between bars
// Y scale (quantitative - bar heights)
const yScale = d3.scaleLinear()
.domain([0, d3.max(data)]) // 0 to 90
.range([innerHeight, 0]); // Inverted (SVG y increases downward)
Why inverted y-range? SVG y-axis increases downward; we want high values at top.
Create Bars
g.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d, i) => xScale(i))
.attr('y', d => yScale(d))
.attr('width', xScale.bandwidth())
.attr('height', d => innerHeight - yScale(d))
.attr('fill', 'steelblue');
Breakdown:
x: Bar position from xScaley: Top of bar from yScalewidth: Automatic from scaleBandheight: Distance from bar top to bottom (innerHeight - y)
Add Axes
// X axis
g.append('g')
.attr('transform', `translate(0, ${innerHeight})`)
.call(d3.axisBottom(xScale));
// Y axis
g.append('g')
.call(d3.axisLeft(yScale));
What .call() does: Invokes function with selection as argument. d3.axisBottom(xScale) is a function that generates axis.
Complete Code
import * as d3 from 'https://cdn.skypack.dev/d3@7';
const width = 500, height = 300;
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const data = [30, 80, 45, 60, 20, 90, 50];
const svg = d3.select('svg').attr('width', width).attr('height', height);
const g = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, innerWidth])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([innerHeight, 0]);
g.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d, i) => xScale(i))
.attr('y', d => yScale(d))
.attr('width', xScale.bandwidth())
.attr('height', d => innerHeight - yScale(d))
.attr('fill', 'steelblue');
g.append('g').attr('transform', `translate(0, ${innerHeight})`).call(d3.axisBottom(xScale));
g.append('g').call(d3.axisLeft(yScale));
Loading Data
CSV Files
File: data.csv
name,value
A,30
B,80
C,45
Load and Parse:
d3.csv('data.csv').then(data => {
// data = [{name: 'A', value: '30'}, ...] (values are strings!)
// Convert strings to numbers
data.forEach(d => { d.value = +d.value; });
// Or use conversion function
d3.csv('data.csv', d => ({
name: d.name,
value: +d.value
})).then(data => {
// Now d.value is number
createVisualization(data);
});
});
Key Points:
- Returns Promise
- Values are strings by default (CSV has no types)
- Use
+operator orparseFloat()for numbers - Use
d3.timeParse()for dates
JSON Files
File: data.json
[
{"name": "A", "value": 30},
{"name": "B", "value": 80}
]
Load:
d3.json('data.json').then(data => {
// data already has correct types
createVisualization(data);
});
Advantage: Types preserved (numbers are numbers, not strings).
Async/Await Pattern
async function init() {
const data = await d3.csv('data.csv', d => ({
name: d.name,
value: +d.value
}));
createVisualization(data);
}
init();
Common Pitfalls
Pitfall 1: Forgetting Data Type Conversion
// WRONG - CSV values are strings
d3.csv('data.csv').then(data => {
const max = d3.max(data, d => d.value); // String comparison! '9' > '80'
});
// CORRECT
d3.csv('data.csv').then(data => {
data.forEach(d => { d.value = +d.value; });
const max = d3.max(data, d => d.value); // Numeric comparison
});
Pitfall 2: Inverted Y-Axis
// WRONG - bars upside down
const yScale = scaleLinear().domain([0, 100]).range([0, height]);
// CORRECT - high values at top
const yScale = scaleLinear().domain([0, 100]).range([height, 0]);
Pitfall 3: Missing Margins
// WRONG - axes overlap chart
svg.selectAll('rect').attr('x', d => xScale(d.x))...
// CORRECT - use margin convention
const g = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
g.selectAll('rect').attr('x', d => xScale(d.x))...
Pitfall 4: Not Using .call() for Axes
// WRONG - won't work
g.append('g').axisBottom(xScale);
// CORRECT
g.append('g').call(d3.axisBottom(xScale));
Next Steps
- Understand data joins: Selections & Data Joins
- Master scales: Scales & Axes
- Build more charts: Workflows
- Add interactions: Transitions & Interactions