D3.js Complete Guide | Data Visualization for the Web
이 글의 핵심
D3.js gives you the building blocks ??SVG, scales, axes, layouts ??to build any chart imaginable. This guide covers the core concepts with complete working examples: bar charts, line charts, scatter plots, and interactive transitions.
D3.js Core Concepts
D3 (Data-Driven Documents) works by:
- Selecting DOM elements
- Binding data to them
- Transforming elements based on data (enter/update/exit)
Data ??Scale ??SVG Element
[10, 20, 50] ??scaleLinear ??rect height="10" height="20" height="50"
Setup
npm install d3
npm install --save-dev @types/d3 # TypeScript types
<!-- Or CDN for quick prototyping -->
<script src="https://d3js.org/d3.v7.min.js"></script>
1. SVG Basics
D3 draws into SVG. Understanding SVG coordinates:
(0,0)———————————————??x
?? ?? SVG coordinate system
?? origin is top-left
?? y increases downward
??y
// Create SVG container
const width = 800
const height = 400
const margin = { top: 20, right: 30, bottom: 40, left: 50 }
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
// Inner chart area (accounting for margins)
const chart = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
const innerWidth = width - margin.left - margin.right // 720
const innerHeight = height - margin.top - margin.bottom // 340
2. Scales ??The Core of D3
Scales map data values to visual values (pixels, colors):
// Linear scale: numbers ??pixels
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)]) // data range
.range([innerHeight, 0]) // pixel range (inverted: 0 is bottom)
.nice() // round to nice values
yScale(0) // ??innerHeight (bottom)
yScale(100) // ??0 (top)
yScale(50) // ??innerHeight / 2
// Band scale: categories ??bar positions
const xScale = d3.scaleBand()
.domain(data.map(d => d.name)) // ['Jan', 'Feb', 'Mar', ...]
.range([0, innerWidth])
.padding(0.2) // 20% padding between bars
xScale('Jan') // ??x position of Jan bar
xScale.bandwidth() // ??width of each bar
// Time scale
const timeScale = d3.scaleTime()
.domain([new Date('2024-01-01'), new Date('2024-12-31')])
.range([0, innerWidth])
// Color scale
const colorScale = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#ff6b6b', '#4ecdc4', '#45b7d1'])
3. Bar Chart
const data = [
{ month: 'Jan', sales: 4200 },
{ month: 'Feb', sales: 3800 },
{ month: 'Mar', sales: 5100 },
{ month: 'Apr', sales: 4700 },
{ month: 'May', sales: 5300 },
{ month: 'Jun', sales: 6200 },
]
const width = 600, height = 400
const margin = { top: 20, right: 20, bottom: 40, left: 60 }
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const svg = d3.select('#bar-chart')
.append('svg')
.attr('width', width)
.attr('height', height)
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
// Scales
const x = d3.scaleBand()
.domain(data.map(d => d.month))
.range([0, innerWidth])
.padding(0.3)
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.sales) * 1.1])
.range([innerHeight, 0])
// Axes
g.append('g')
.attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(x))
g.append('g')
.call(d3.axisLeft(y).tickFormat(d => `$${d3.format(',')(d)}`))
// Bars
g.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.month))
.attr('y', d => y(d.sales))
.attr('width', x.bandwidth())
.attr('height', d => innerHeight - y(d.sales))
.attr('fill', '#4ecdc4')
.attr('rx', 4) // rounded corners
4. Line Chart
const timeData = [
{ date: new Date('2024-01-01'), value: 100 },
{ date: new Date('2024-02-01'), value: 120 },
{ date: new Date('2024-03-01'), value: 115 },
{ date: new Date('2024-04-01'), value: 140 },
{ date: new Date('2024-05-01'), value: 135 },
{ date: new Date('2024-06-01'), value: 160 },
]
// Scales
const x = d3.scaleTime()
.domain(d3.extent(timeData, d => d.date))
.range([0, innerWidth])
const y = d3.scaleLinear()
.domain([0, d3.max(timeData, d => d.value) * 1.1])
.range([innerHeight, 0])
// Line generator
const line = d3.line()
.x(d => x(d.date))
.y(d => y(d.value))
.curve(d3.curveMonotoneX) // smooth curve
// Area (gradient fill under line)
const area = d3.area()
.x(d => x(d.date))
.y0(innerHeight)
.y1(d => y(d.value))
.curve(d3.curveMonotoneX)
// Draw area (fill under curve)
g.append('path')
.datum(timeData)
.attr('fill', 'rgba(78, 205, 196, 0.2)')
.attr('d', area)
// Draw line
g.append('path')
.datum(timeData)
.attr('fill', 'none')
.attr('stroke', '#4ecdc4')
.attr('stroke-width', 2.5)
.attr('d', line)
// Data points
g.selectAll('circle')
.data(timeData)
.join('circle')
.attr('cx', d => x(d.date))
.attr('cy', d => y(d.value))
.attr('r', 5)
.attr('fill', '#4ecdc4')
.attr('stroke', 'white')
.attr('stroke-width', 2)
5. Tooltips
// Create tooltip element
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('background', 'rgba(0,0,0,0.8)')
.style('color', 'white')
.style('padding', '8px 12px')
.style('border-radius', '4px')
.style('font-size', '13px')
.style('pointer-events', 'none')
.style('opacity', 0)
// Add tooltip to bars
g.selectAll('rect')
.data(data)
.join('rect')
// ... attrs ...
.on('mouseover', (event, d) => {
tooltip
.style('opacity', 1)
.html(`
<strong>${d.month}</strong><br/>
Sales: $${d3.format(',')(d.sales)}
`)
})
.on('mousemove', (event) => {
tooltip
.style('left', `${event.pageX + 12}px`)
.style('top', `${event.pageY - 28}px`)
})
.on('mouseout', () => {
tooltip.style('opacity', 0)
})
6. Transitions (Animation)
// Animate bars on load
g.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.month))
.attr('width', x.bandwidth())
.attr('y', innerHeight) // start from bottom
.attr('height', 0) // start with height 0
.attr('fill', '#4ecdc4')
.transition() // start transition
.duration(800)
.delay((d, i) => i * 100) // stagger bars
.ease(d3.easeCubicOut)
.attr('y', d => y(d.sales))
.attr('height', d => innerHeight - y(d.sales))
// Update chart with new data
function updateChart(newData) {
const y = d3.scaleLinear()
.domain([0, d3.max(newData, d => d.value)])
.range([innerHeight, 0])
g.selectAll('rect')
.data(newData)
.join(
enter => enter.append('rect') // new elements
.attr('fill', '#4ecdc4')
.attr('y', innerHeight)
.attr('height', 0),
update => update, // existing elements
exit => exit // removed elements
.transition().duration(300)
.attr('height', 0)
.attr('y', innerHeight)
.remove()
)
.transition().duration(500)
.attr('x', d => x(d.name))
.attr('width', x.bandwidth())
.attr('y', d => y(d.value))
.attr('height', d => innerHeight - y(d.value))
}
7. Scatter Plot with Brushing
const scatterData = d3.range(100).map(() => ({
x: Math.random() * 100,
y: Math.random() * 100,
category: ['A', 'B', 'C'][Math.floor(Math.random() * 3)],
}))
const color = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#ff6b6b', '#4ecdc4', '#45b7d1'])
const xScale = d3.scaleLinear().domain([0, 100]).range([0, innerWidth])
const yScale = d3.scaleLinear().domain([0, 100]).range([innerHeight, 0])
// Draw points
const dots = g.selectAll('circle')
.data(scatterData)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 6)
.attr('fill', d => color(d.category))
.attr('opacity', 0.7)
// Add brush for selection
const brush = d3.brush()
.extent([[0, 0], [innerWidth, innerHeight]])
.on('brush end', ({ selection }) => {
if (!selection) {
dots.attr('opacity', 0.7)
return
}
const [[x0, y0], [x1, y1]] = selection
dots.attr('opacity', d => {
const cx = xScale(d.x), cy = yScale(d.y)
return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1 ? 1 : 0.2
})
})
g.append('g').call(brush)
8. React + D3 Integration
import { useEffect, useRef } from 'react'
import * as d3 from 'd3'
interface BarChartProps {
data: { name: string; value: number }[]
width?: number
height?: number
}
function BarChart({ data, width = 500, height = 300 }: BarChartProps) {
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!svgRef.current || !data.length) return
const margin = { top: 20, right: 20, bottom: 40, left: 50 }
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
// Clear previous render
d3.select(svgRef.current).selectAll('*').remove()
const svg = d3.select(svgRef.current)
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
const x = d3.scaleBand()
.domain(data.map(d => d.name))
.range([0, innerWidth])
.padding(0.3)
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)!])
.range([innerHeight, 0])
.nice()
g.append('g')
.attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(x))
g.append('g')
.call(d3.axisLeft(y))
g.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.name)!)
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => innerHeight - y(d.value))
.attr('fill', '#4ecdc4')
.attr('rx', 3)
}, [data, width, height])
return <svg ref={svgRef} width={width} height={height} />
}
// Usage
const data = [
{ name: 'Jan', value: 4200 },
{ name: 'Feb', value: 3800 },
{ name: 'Mar', value: 5100 },
]
<BarChart data={data} width={600} height={350} />
D3 vs Chart Libraries
| D3.js | Chart.js | Recharts | Observable Plot | |
|---|---|---|---|---|
| Flexibility | Unlimited | Low | Medium | Medium |
| Learning curve | Steep | Gentle | Medium | Gentle |
| Bundle size | Modular (~50KB) | 200KB | 300KB+ | ~60KB |
| Custom charts | Yes | Limited | Limited | Limited |
| React integration | Manual | Plugin | Native | Plugin |
| Best for | Custom, complex | Common charts | React apps | Quick exploration |
Key Takeaways
- Scales are the foundation ??they map data values to pixels/colors
selection.join()handles enter/update/exit for data-driven DOM updates- Margins pattern: draw inside a
<g>translated by margin to leave room for axes - Transitions animate with
.transition().duration().attr() - React integration: let D3 manage SVG in
useEffect, or use D3 only for math and let React render - Import only what you need:
import { scaleLinear, axisBottom } from 'd3'for smaller bundles
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. D3.js gives you the building blocks ??SVG, scales, axes, layouts ??to build any chart imaginable. This guide covers the core concepts with complete working examples: bar charts, line charts, scatter plots, and interactive transitions.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
Keywords Covered in This Article (Related Search Terms)
This article covers D3.js, Data Visualization, SVG, JavaScript, Frontend, Charts.