d3js-data-visualization
SKILL.md
D3.js Data Visualization
Build sophisticated, interactive data visualizations using d3.js (Data-Driven Documents). D3 binds data to DOM elements and applies data-driven transformations to produce publication-quality, fully customizable visuals.
When to Use This Skill
- Custom charts requiring unique visual encodings or layouts
- Interactive visualizations with pan, zoom, or brush behaviors
- Network/graph visualizations (force-directed, tree, hierarchy, chord diagrams)
- Geographic visualizations with custom projections
- Smooth, choreographed transitions and animations
- Novel chart types not available in standard libraries (Recharts, Chart.js, etc.)
- Fine-grained SVG styling and accessibility control
Consider alternatives for:
- 3D visualizations → use Three.js
- Simple standard charts with minimal customization → use Chart.js or Recharts
Required Tools / Libraries
No backend required. Runs entirely in the browser or Node.js (with jsdom/canvas).
# Install via npm
npm install d3
# Or use CDN in HTML
<script src="https://d3js.org/d3.v7.min.js"></script>
Core Workflow
1. Set Up D3
import * as d3 from 'd3';
2. Standard Chart Structure
Every d3 visualization follows this pattern:
function drawChart(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove(); // clear previous render
const width = 800, height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Define scales
const xScale = d3.scaleLinear().domain([0, d3.max(data, d => d.x)]).range([0, innerWidth]);
const yScale = d3.scaleLinear().domain([0, d3.max(data, d => d.y)]).range([innerHeight, 0]);
// Axes
g.append("g").attr("transform", `translate(0,${innerHeight})`).call(d3.axisBottom(xScale));
g.append("g").call(d3.axisLeft(yScale));
// Data elements
g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 5)
.attr("fill", "steelblue");
}
Common Chart Patterns
Bar Chart
const xScale = d3.scaleBand().domain(data.map(d => d.category)).range([0, innerWidth]).padding(0.1);
const yScale = d3.scaleLinear().domain([0, d3.max(data, d => d.value)]).range([innerHeight, 0]);
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.category))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => innerHeight - yScale(d.value))
.attr("fill", "steelblue");
Line Chart
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
Scatter Plot
g.selectAll("circle")
.data(data)
.join("circle")
.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 / Donut Chart
const pie = d3.pie().value(d => d.value).sort(null);
const arc = d3.arc().innerRadius(0).outerRadius(Math.min(width, height) / 2 - 20);
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
const g = svg.append("g").attr("transform", `translate(${width / 2},${height / 2})`);
g.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", (d, i) => colorScale(i))
.attr("stroke", "white")
.attr("stroke-width", 2);
Force-Directed Network Graph
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));
const link = g.selectAll("line").data(links).join("line").attr("stroke", "#999");
const node = g.selectAll("circle").data(nodes).join("circle")
.attr("r", 8).attr("fill", "steelblue")
.call(d3.drag()
.on("start", (e) => { if (!e.active) simulation.alphaTarget(0.3).restart(); e.subject.fx = e.subject.x; e.subject.fy = e.subject.y; })
.on("drag", (e) => { e.subject.fx = e.x; e.subject.fy = e.y; })
.on("end", (e) => { if (!e.active) simulation.alphaTarget(0); e.subject.fx = null; e.subject.fy = null; }));
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);
});
Heatmap
// data: [{ row, column, value }, ...]
const rows = [...new Set(data.map(d => d.row))];
const cols = [...new Set(data.map(d => d.column))];
const xScale = d3.scaleBand().domain(cols).range([0, innerWidth]).padding(0.01);
const yScale = d3.scaleBand().domain(rows).range([0, innerHeight]).padding(0.01);
const colorScale = d3.scaleSequential(d3.interpolateYlOrRd).domain([0, d3.max(data, d => d.value)]);
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.column))
.attr("y", d => yScale(d.row))
.attr("width", xScale.bandwidth())
.attr("height", yScale.bandwidth())
.attr("fill", d => colorScale(d.value));
Interactivity
Tooltips
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background", "white")
.style("border", "1px solid #ddd")
.style("padding", "10px")
.style("border-radius", "4px")
.style("pointer-events", "none");
elements
.on("mouseover", (event, d) => tooltip.style("visibility", "visible").html(`<strong>${d.label}</strong><br/>Value: ${d.value}`))
.on("mousemove", (event) => tooltip.style("top", (event.pageY - 10) + "px").style("left", (event.pageX + 10) + "px"))
.on("mouseout", () => tooltip.style("visibility", "hidden"));
Zoom and Pan
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => g.attr("transform", event.transform));
svg.call(zoom);
Transitions & Animations
// Basic
circles.transition().duration(750).attr("r", 10);
// Staggered
circles.transition().delay((d, i) => i * 50).duration(500).attr("cy", d => yScale(d.value));
// Custom easing
circles.transition().duration(1000).ease(d3.easeBounceOut).attr("r", 10);
Responsive Sizing
function setupResponsiveChart(containerId, data) {
const container = document.getElementById(containerId);
const svg = d3.select(`#${containerId}`).append('svg');
const updateChart = () => {
const { width, height } = container.getBoundingClientRect();
svg.attr('width', width).attr('height', height);
drawChart(data, svg, width, height);
};
updateChart();
window.addEventListener('resize', updateChart);
return () => window.removeEventListener('resize', updateChart);
}
Scale Reference
| Scale | Use case |
|---|---|
d3.scaleLinear() |
Continuous numeric data |
d3.scaleLog() |
Exponential/logarithmic data |
d3.scaleTime() |
Date/time axes |
d3.scaleBand() |
Bar chart categories |
d3.scaleOrdinal() |
Categorical colors |
d3.scaleSequential() |
Single-hue color gradients |
d3.scaleDiverging() |
Diverging color scales |
Best Practices
- Always validate data: filter nulls and NaN before binding
- Clear previous render with
svg.selectAll("*").remove()before redrawing - Use
.join()(enter/update/exit in one call) instead of separate selections - Add ARIA labels (
role="img",aria-label) for accessibility - For >1000 elements, consider Canvas rendering instead of SVG
- Debounce resize handlers to avoid excessive redraws
- Define color palettes upfront for visual consistency
Troubleshooting
| Problem | Solution |
|---|---|
| Axes not appearing | Check for NaN in scale domain; verify group transform |
| Transitions not working | Call .transition() before attribute changes |
| Responsive sizing broken | Use ResizeObserver or update SVG width/height on resize |
| Performance issues | Switch to Canvas, debounce resize, use .join() |
Related Skills
generate-asset-price-chart— OHLC candlestick chart generationtrading-indicators-from-price-data— Compute indicators to feed into chartsfree-geocoding-and-maps— Geographic data for map visualizations
Weekly Installs
7
Repository
besoeasy/open-skillsGitHub Stars
87
First Seen
13 days ago
Security Audits
Installed on
kimi-cli7
github-copilot6
codex6
gemini-cli6
cursor6
amp6