Introduction
D3.js remains in 2026 the reference library for highly customized data visualizations. Unlike high-level frameworks, D3 gives you full control over the DOM and SVG. This expert tutorial guides you through creating a complete interactive visualization with dynamic data handling, performant animations, and physics simulations.
Prerequisites
- Node.js 20+ and TypeScript 5.5+
- Solid knowledge of modern JavaScript and SVG
- D3.js v7 installed
- An editor with TypeScript support
Project Initialization
npm init -y
npm install d3 @types/d3
npm install -D typescript viteWe initialize a Vite project for modern development and install D3.js v7 with its official TypeScript types.
TypeScript Configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}Strict TypeScript configuration optimized for D3.js to benefit from type inference on selections and data.
SVG Container Creation
import * as d3 from 'd3';
const width = 800;
const height = 600;
const svg = d3.select('#app')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`);Create a responsive SVG using viewBox. Always use viewBox to ensure adaptability across all screens.
Data Binding and Scales
interface DataPoint { x: number; y: number; value: number; }
const data: DataPoint[] = [
{ x: 10, y: 20, value: 45 },
{ x: 30, y: 50, value: 78 },
{ x: 60, y: 30, value: 92 }
];
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.x) as [number, number])
.range([50, width - 50]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)!])
.range([height - 50, 50]);Use linear scales with automatic domain calculation via d3.extent. Always type your data to avoid runtime errors.
Adding Circles with Transitions
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 0)
.attr('fill', '#3b82f6')
.transition()
.duration(800)
.ease(d3.easeBounceOut)
.attr('r', d => Math.sqrt(d.value) * 1.5);Modern .join() pattern and transition with custom ease. D3 transitions are performant because they use requestAnimationFrame.
Best Practices
- Always type your data interfaces to benefit from autocompletion
- Use .join() instead of .enter()/.exit() for more concise code
- Prefer short transitions (600-900ms) with appropriate easing
- Clean up event listeners during component unmount
- Measure performance with DevTools before adding too many elements
Common Mistakes to Avoid
- Forgetting to handle null values in scale domains
- Using overly generic CSS selectors that break scoping
- Applying transitions to thousands of elements without throttling
- Neglecting accessibility (aria-labels on SVG elements)
Going Further
Deepen your knowledge of force simulations and hierarchical layouts in our complete training: Learni Courses.