## D3.js Fundamentals
### The D3 Mental Model
```
D3 is about:
1. Binding DATA to DOM elements
2. Translating DATA to visual attributes
3. Responding to DATA changes with transitions
Key concepts:
- Selections: Select and manipulate elements
- Data binding: Join data to elements
- Scales: Map data → visual values
- Axes: Generate axis elements
- Transitions: Animate between states
```
### Basic Setup
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
.chart-container {
width: 800px;
height: 500px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="chart" class="chart-container"></div>
<script src="chart.js"></script>
</body>
</html>
```
### SVG Container Pattern
```javascript
// Standard chart setup
const margin = { top: 40, right: 40, bottom: 60, left: 60 };
const width = 800 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const svg = d3
.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
```
## Scales
### Linear Scale
```javascript
// Numeric data → pixel position
const xScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)]) // Data range
.range([0, width]); // Pixel range
```
### Band Scale (for categories)
```javascript
// Categories → pixel positions
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.category))
.range([0, width])
.padding(0.2); // Gap between bars
```
### Color Scales
```javascript
// Sequential (numeric → color)
const colorScale = d3
.scaleSequential()
.domain([0, d3.max(data, (d) => d.value)])
.interpolator(d3.interpolateBlues);
// Ordinal (categories → colors)
const colorScale = d3.scaleOrdinal().domain(['A', 'B', 'C']).range(d3.schemeCategory10);
```
### Time Scale
```javascript
const timeScale = d3
.scaleTime()
.domain([new Date('2024-01-01'), new Date('2024-12-31')])
.range([0, width]);
```
## Axes
```javascript
// Create axes
const xAxis = d3.axisBottom(xScale)
.tickFormat(d3.format(",")) // Number formatting
.ticks(10); // Suggested tick count
const yAxis = d3.axisLeft(yScale)
.tickFormat(d => `$${d}`);
// Render axes
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(xAxis);
svg.append("g")
.attr("class", "y-axis")
.call(yAxis);
// Style axes (CSS)
.axis line, .axis path {
stroke: #333;
}
.axis text {
font-size: 12px;
fill: #666;
}
```
## Chart Types
### Bar Chart
```javascript
const data = [
{ category: 'A', value: 30 },
{ category: 'B', value: 80 },
{ category: 'C', value: 45 }
];
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.category))
.range([0, width])
.padding(0.2);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([height, 0]);
// Create bars
svg
.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', (d) => xScale(d.category))
.attr('y', (d) => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', (d) => height - yScale(d.value))
.attr('fill', '#4f46e5');
```
### Line Chart
```javascript
const data = [
{ date: new Date('2024-01-01'), value: 100 },
{ date: new Date('2024-02-01'), value: 120 },
{ date: new Date('2024-03-01'), value: 90 }
];
const xScale = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.date))
.range([0, width]);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([height, 0]);
// Line generator
const line = d3
.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX); // Smooth curve
// Draw line
svg
.append('path')
.datum(data)
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', '#4f46e5')
.attr('stroke-width', 2)
.attr('d', line);
// Add dots
svg
.selectAll('.dot')
.data(data)
.join('circle')
.attr('class', 'dot')
.attr('cx', (d) => xScale(d.date))
.attr('cy', (d) => yScale(d.value))
.attr('r', 5)
.attr('fill', '#4f46e5');
```
### Scatter Plot
```javascript
svg
.selectAll('.point')
.data(data)
.join('circle')
.attr('class', 'point')
.attr('cx', (d) => xScale(d.x))
.attr('cy', (d) => yScale(d.y))
.attr('r', (d) => sizeScale(d.size))
.attr('fill', (d) => colorScale(d.category))
.attr('opacity', 0.7);
```
### Pie Chart
```javascript
const pie = d3
.pie()
.value((d) => d.value)
.sort(null);
const arc = d3
.arc()
.innerRadius(0)
.outerRadius(Math.min(width, height) / 2);
const arcs = svg
.selectAll('.arc')
.data(pie(data))
.join('g')
.attr('class', 'arc')
.attr('transform', `translate(${width / 2},${height / 2})`);
arcs
.append('path')
.attr('d', arc)
.attr('fill', (d, i) => colorScale(i))
.attr('stroke', 'white')
.attr('stroke-width', 2);
```
## Transitions
```javascript
// Basic transition
svg
.selectAll('.bar')
.data(newData)
.join('rect')
.transition()
.duration(750)
.ease(d3.easeCubicOut)
.attr('height', (d) => height - yScale(d.value))
.attr('y', (d) => yScale(d.value));
// Staggered transition
svg
.selectAll('.bar')
.transition()
.duration(500)
.delay((d, i) => i * 100) // Stagger by index
.attr('opacity', 1);
// Chained transitions
selection
.transition('first')
.duration(500)
.attr('r', 20)
.transition('second')
.duration(500)
.attr('fill', 'red');
```
## Interactivity
### Hover Effects
```javascript
svg
.selectAll('.bar')
.on('mouseover', function (event, d) {
d3.select(this).transition().duration(200).attr('fill', '#6366f1');
// Show tooltip
tooltip
.style('opacity', 1)
.html(`<strong>${d.category}</strong>: ${d.value}`)
.style('left', event.pageX + 10 + 'px')
.style('top', event.pageY - 28 + 'px');
})
.on('mouseout', function () {
d3.select(this).transition().duration(200).attr('fill', '#4f46e5');
tooltip.style('opacity', 0);
});
```
### Tooltip Setup
```javascript
const tooltip = d3
.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('background', 'white')
.style('padding', '8px')
.style('border', '1px solid #ddd')
.style('border-radius', '4px')
.style('pointer-events', 'none')
.style('opacity', 0);
```
### Zoom and Pan
```javascript
const zoom = d3
.zoom()
.scaleExtent([1, 10])
.on('zoom', (event) => {
svg.attr('transform', event.transform);
});
d3.select('svg').call(zoom);
```
## Responsive Charts
```javascript
function createChart() {
const container = document.getElementById('chart');
const width = container.clientWidth - margin.left - margin.right;
// Update scales
xScale.range([0, width]);
// Redraw elements
svg.select('.x-axis').call(xAxis);
svg.selectAll('.bar').attr('width', xScale.bandwidth());
}
// Initial render
createChart();
// Resize handler
window.addEventListener('resize', createChart);
```
## Best Practices
1. **Use the General Update Pattern**: `join()` handles enter/update/exit
2. **Separate Concerns**: Data processing separate from rendering
3. **Responsive Design**: Use viewBox and responsive containers
4. **Accessible Charts**: Include ARIA labels and descriptions
5. **Performance**: Use Canvas for large datasets (>10k points)
6. **Consistent Scales**: Keep scales stable during updates
## Related Resources
- [D3.js Documentation](https://d3js.org/)
- [Observable D3 Gallery](https://observablehq.com/@d3/gallery)
- [D3 Graph Gallery](https://d3-graph-gallery.com/)
- [Interactive Data Visualization](https://alignedleft.com/work/d3-book)