D3.js Data Visualization Skill
Create powerful, custom data visualizations using D3.js for complete control over SVG elements, transitions, and data binding.
When to Use This Skill
Use D3.js when you need:
-
Complete customization - Every aspect of the visualization controlled
-
Complex interactions - Advanced user interactions and transitions
-
Unique visualizations - Bespoke charts not available in other libraries
-
Data-driven DOM manipulation - Direct binding of data to DOM elements
-
Custom animations - Sophisticated transitions and effects
Avoid when:
-
Simple charts with default styling are sufficient (use Chart.js)
-
Quick implementation is priority (use Plotly or Chart.js)
-
Team lacks JavaScript expertise
Core Capabilities
- Data Binding
// Select and bind data to elements d3.select('#chart') .selectAll('circle') .data(dataset) .enter() .append('circle') .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('r', d => d.radius) .style('fill', d => colorScale(d.category));
- Scales and Axes
// Create scales for positioning const xScale = d3.scaleLinear() .domain([0, d3.max(data, d => d.x)]) .range([0, width]);
const yScale = d3.scaleLinear() .domain([0, d3.max(data, d => d.y)]) .range([height, 0]);
// Create axes const xAxis = d3.axisBottom(xScale); const yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('transform', translate(0, ${height}))
.call(xAxis);
svg.append('g') .call(yAxis);
- Transitions and Animations
// Smooth transitions d3.selectAll('circle') .transition() .duration(1000) .attr('r', d => d.newRadius) .style('fill', 'steelblue');
- Interactive Elements
// Add interactivity const tooltip = d3.select('body') .append('div') .attr('class', 'tooltip') .style('opacity', 0);
circles
.on('mouseover', function(event, d) {
tooltip.transition()
.duration(200)
.style('opacity', .9);
tooltip.html(Value: ${d.value})
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 28) + 'px');
})
.on('mouseout', function(d) {
tooltip.transition()
.duration(500)
.style('opacity', 0);
});
Complete Examples
Example 1: Interactive Bar Chart
<!DOCTYPE html> <html> <head> <script src="https://d3js.org/d3.v7.min.js"></script> <style> .bar { fill: steelblue; cursor: pointer; } .bar:hover { fill: orange; } .tooltip { position: absolute; padding: 10px; background: rgba(0,0,0,0.8); color: white; border-radius: 5px; pointer-events: none; } </style> </head> <body> <div id="chart"></div> <script> // Data const data = [ { category: 'A', value: 30 }, { category: 'B', value: 80 }, { category: 'C', value: 45 }, { category: 'D', value: 60 }, { category: 'E', value: 20 } ];
// Dimensions
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
// Create SVG
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
const xScale = d3.scaleBand()
.domain(data.map(d => d.category))
.range([0, width])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// Axes
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale));
svg.append('g')
.call(d3.axisLeft(yScale));
// Tooltip
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
// Bars
svg.selectAll('.bar')
.data(data)
.enter()
.append('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))
.on('mouseover', function(event, d) {
d3.select(this).style('fill', 'orange');
tooltip.transition().duration(200).style('opacity', .9);
tooltip.html(`${d.category}: ${d.value}`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 28) + 'px');
})
.on('mouseout', function(d) {
d3.select(this).style('fill', 'steelblue');
tooltip.transition().duration(500).style('opacity', 0);
});
</script> </body> </html>
Example 2: Animated Line Chart with CSV Data
// Load and visualize CSV data d3.csv('../data/timeseries.csv').then(data => { // Parse dates and values const parseDate = d3.timeParse('%Y-%m-%d'); data.forEach(d => { d.date = parseDate(d.date); d.value = +d.value; });
// Scales 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);
// Draw line with animation const path = svg.append('path') .datum(data) .attr('class', 'line') .attr('d', line) .style('fill', 'none') .style('stroke', 'steelblue') .style('stroke-width', 2);
// Animate path const totalLength = path.node().getTotalLength(); path .attr('stroke-dasharray', totalLength + ' ' + totalLength) .attr('stroke-dashoffset', totalLength) .transition() .duration(2000) .ease(d3.easeLinear) .attr('stroke-dashoffset', 0);
// Add dots svg.selectAll('.dot') .data(data) .enter() .append('circle') .attr('class', 'dot') .attr('cx', d => xScale(d.date)) .attr('cy', d => yScale(d.value)) .attr('r', 0) .style('fill', 'steelblue') .transition() .delay((d, i) => i * 50) .duration(500) .attr('r', 4); });
Example 3: Force-Directed Network Graph
// Network data const nodes = [ { id: 'A', group: 1 }, { id: 'B', group: 1 }, { id: 'C', group: 2 }, { id: 'D', group: 2 }, { id: 'E', group: 3 } ];
const links = [ { source: 'A', target: 'B', value: 1 }, { source: 'B', target: 'C', value: 2 }, { source: 'C', target: 'D', value: 1 }, { source: 'D', target: 'E', value: 3 }, { source: 'E', target: 'A', value: 2 } ];
// Create force simulation const simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(d => d.id)) .force('charge', d3.forceManyBody().strength(-200)) .force('center', d3.forceCenter(width / 2, height / 2));
// Draw links const link = svg.append('g') .selectAll('line') .data(links) .enter() .append('line') .style('stroke', '#999') .style('stroke-width', d => Math.sqrt(d.value));
// Draw nodes const node = svg.append('g') .selectAll('circle') .data(nodes) .enter() .append('circle') .attr('r', 10) .style('fill', d => d3.schemeCategory10[d.group]) .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended));
// Add labels const label = svg.append('g') .selectAll('text') .data(nodes) .enter() .append('text') .text(d => d.id) .style('font-size', '12px') .attr('dx', 12) .attr('dy', 4);
// Update positions on tick simulation.on('tick', () => { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y);
node .attr('cx', d => d.x) .attr('cy', d => d.y);
label .attr('x', d => d.x) .attr('y', d => d.y); });
// Drag functions function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }
function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }
Best Practices
- Use Proper Margins Convention
const margin = { top: 20, right: 20, bottom: 30, left: 40 }; const width = 960 - margin.left - margin.right; const height = 500 - margin.top - margin.bottom;
const svg = d3.select('body').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}));
- Use Method Chaining
// Good - readable chaining svg.selectAll('circle') .data(data) .enter() .append('circle') .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('r', 5);
- Separate Data from Presentation
// Load data separately d3.json('../data/data.json').then(data => { visualize(data); });
function visualize(data) { // Visualization logic here }
- Use Responsive Design
// Make chart responsive function resize() { const container = d3.select('#chart').node(); const width = container.getBoundingClientRect().width;
xScale.range([0, width]); svg.attr('width', width); // Update chart elements }
window.addEventListener('resize', resize);
Common Patterns
Update Pattern (Enter, Update, Exit)
function update(data) { // Bind data const circles = svg.selectAll('circle') .data(data, d => d.id);
// EXIT: Remove old elements circles.exit() .transition() .duration(500) .attr('r', 0) .remove();
// UPDATE: Update existing elements circles .transition() .duration(500) .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y));
// ENTER: Add new elements circles.enter() .append('circle') .attr('r', 0) .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .transition() .duration(500) .attr('r', 5); }
Brush and Zoom
// Add zoom behavior const zoom = d3.zoom() .scaleExtent([1, 10]) .on('zoom', zoomed);
svg.call(zoom);
function zoomed(event) { const transform = event.transform; svg.attr('transform', transform); }
// Add brush selection const brush = d3.brush() .extent([[0, 0], [width, height]]) .on('end', brushed);
svg.append('g') .attr('class', 'brush') .call(brush);
function brushed(event) { if (!event.selection) return; const [[x0, y0], [x1, y1]] = event.selection; // Handle selected region }
Installation & Setup
CDN (Quick Start)
<script src="https://d3js.org/d3.v7.min.js"></script>
NPM (Production)
npm install d3
import * as d3 from 'd3'; // Or import specific modules import { select, scaleLinear, axisBottom } from 'd3';
Performance Tips
-
Minimize DOM operations - Batch updates when possible
-
Use canvas for large datasets - Switch to canvas for >1000 points
-
Throttle events - Debounce mousemove/scroll events
-
Optimize transitions - Limit concurrent animations
-
Use web workers - Offload heavy computations
Resources
-
Official Docs: https://d3js.org/
-
Observable: https://observablehq.com/@d3 (Interactive examples)
-
GitHub: https://github.com/d3/d3
-
Gallery: https://observablehq.com/@d3/gallery
Integration with Other Tools
With React
import { useEffect, useRef } from 'react'; import * as d3 from 'd3';
function D3Chart({ data }) { const svgRef = useRef();
useEffect(() => { const svg = d3.select(svgRef.current); // D3 code here }, [data]);
return <svg ref={svgRef}></svg>; }
With CSV/JSON Data
// Load from relative path d3.csv('../data/data.csv').then(data => { // Process and visualize });
d3.json('../data/data.json').then(data => { // Visualize JSON });
Use this skill when you need maximum control and customization in your data visualizations!