## Curation Note
D3.js remains the gold standard for custom data visualization on the web, but its learning curve is steep. This skill bridges the gap between understanding data and producing publication-quality visualizations. The approach emphasizes the "data join" paradigm that makes D3 powerful and the importance of proper scales, rather than hard-coded pixel values. Community feedback indicates this skill particularly helps when standard chart libraries (Chart.js, Plotly) lack the customization needed.
## Core Concepts
### The D3 Selection Pattern
```javascript
// Select elements, bind data, modify
d3.select('svg')
.selectAll('circle')
.data(dataset)
.join('circle')
.attr('cx', (d) => xScale(d.x))
.attr('cy', (d) => yScale(d.y))
.attr('r', (d) => rScale(d.value));
```
### The Data Join
```javascript
// Enter, Update, Exit pattern
const circles = svg.selectAll('circle').data(data);
// Enter: new data points
circles
.enter()
.append('circle')
.attr('r', 0)
.transition()
.attr('r', (d) => rScale(d.value));
// Update: existing data points
circles
.transition()
.attr('cx', (d) => xScale(d.x))
.attr('cy', (d) => yScale(d.y));
// Exit: removed data points
circles.exit().transition().attr('r', 0).remove();
```
## Scales and Axes
### Linear Scale
```javascript
const xScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([margin.left, width - margin.right]);
const yScale = d3
.scaleLinear()
.domain([0, 100])
.range([height - margin.bottom, margin.top]);
```
### Time Scale
```javascript
const timeScale = d3
.scaleTime()
.domain([new Date('2024-01-01'), new Date('2024-12-31')])
.range([margin.left, width - margin.right]);
```
### Ordinal Scale
```javascript
const colorScale = d3
.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#e41a1c', '#377eb8', '#4daf4a']);
const bandScale = d3
.scaleBand()
.domain(data.map((d) => d.category))
.range([margin.left, width - margin.right])
.padding(0.1);
```
### Creating Axes
```javascript
const xAxis = d3.axisBottom(xScale).ticks(5).tickFormat(d3.format('.0f'));
const yAxis = d3.axisLeft(yScale).tickFormat((d) => `${d}%`);
svg
.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(xAxis);
svg.append('g').attr('transform', `translate(${margin.left},0)`).call(yAxis);
```
## Common Chart Patterns
### Bar Chart
```javascript
function createBarChart(data, container) {
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3
.select(container)
.append('svg')
.attr(
'viewBox',
`0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`
)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const x = d3
.scaleBand()
.domain(data.map((d) => d.category))
.range([0, width])
.padding(0.1);
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.nice()
.range([height, 0]);
svg
.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', (d) => x(d.category))
.attr('y', (d) => y(d.value))
.attr('width', x.bandwidth())
.attr('height', (d) => height - y(d.value))
.attr('fill', '#4e79a7');
svg.append('g').attr('transform', `translate(0,${height})`).call(d3.axisBottom(x));
svg.append('g').call(d3.axisLeft(y));
}
```
### Line Chart with Area
```javascript
function createLineChart(data, container) {
const margin = { top: 20, right: 20, bottom: 30, left: 50 };
const width = 600;
const height = 400;
const x = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.date))
.range([margin.left, width - margin.right]);
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.nice()
.range([height - margin.bottom, margin.top]);
const line = d3
.line()
.x((d) => x(d.date))
.y((d) => y(d.value))
.curve(d3.curveMonotoneX);
const area = d3
.area()
.x((d) => x(d.date))
.y0(height - margin.bottom)
.y1((d) => y(d.value))
.curve(d3.curveMonotoneX);
const svg = d3.select(container).append('svg').attr('viewBox', `0 0 ${width} ${height}`);
svg.append('path').datum(data).attr('fill', '#4e79a7').attr('fill-opacity', 0.2).attr('d', area);
svg
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', '#4e79a7')
.attr('stroke-width', 2)
.attr('d', line);
}
```
### Scatter Plot with Tooltip
```javascript
function createScatterPlot(data, container) {
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const width = 600;
const height = 400;
const x = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.x))
.nice()
.range([margin.left, width - margin.right]);
const y = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.y))
.nice()
.range([height - margin.bottom, margin.top]);
const svg = d3.select(container).append('svg').attr('viewBox', `0 0 ${width} ${height}`);
// Tooltip
const tooltip = d3
.select(container)
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0)
.style('position', 'absolute');
svg
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', (d) => x(d.x))
.attr('cy', (d) => y(d.y))
.attr('r', 5)
.attr('fill', '#4e79a7')
.on('mouseover', (event, d) => {
tooltip.transition().style('opacity', 0.9);
tooltip
.html(`X: ${d.x}<br>Y: ${d.y}`)
.style('left', event.pageX + 10 + 'px')
.style('top', event.pageY - 10 + 'px');
})
.on('mouseout', () => {
tooltip.transition().style('opacity', 0);
});
}
```
## Animation and Transitions
```javascript
// Smooth transitions
svg
.selectAll('rect')
.data(newData)
.transition()
.duration(750)
.ease(d3.easeCubicOut)
.attr('height', (d) => height - y(d.value))
.attr('y', (d) => y(d.value));
// Staggered animation
svg
.selectAll('circle')
.data(data)
.join('circle')
.attr('r', 0)
.transition()
.delay((d, i) => i * 50)
.duration(500)
.attr('r', 5);
```
## Responsive Design
```javascript
function makeResponsive(svg, width, height) {
svg
.attr('viewBox', `0 0 ${width} ${height}`)
.attr('preserveAspectRatio', 'xMidYMid meet')
.style('max-width', '100%')
.style('height', 'auto');
}
```
## Best Practices
1. **Always use scales** - Never hard-code pixel values
2. **Use viewBox for responsiveness** - Scales naturally
3. **Prefer data joins over manual DOM manipulation**
4. **Add transitions for data updates** - Helps users track changes
5. **Include axes with proper labels** - Context is essential
6. **Use semantic color scales** - Accessible to colorblind users
7. **Test with edge cases** - Empty data, extreme values
## Related Resources
- [D3.js Documentation](https://d3js.org/)
- [Observable D3 Gallery](https://observablehq.com/@d3/gallery)
- [D3 Graph Gallery](https://www.d3-graph-gallery.com/)