Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment.
Add this skill
npx mdskills install sickn33/claude-d3js-skillComprehensive D3.js guidance with clear patterns, multiple examples, and framework integration
1---2name: d3-viz3description: Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment.4---56# D3.js Visualisation78## Overview910This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to create custom, publication-quality visualisations with precise control over every visual element. The techniques work across any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks.1112## When to use d3.js1314**Use d3.js for:**15- Custom visualisations requiring unique visual encodings or layouts16- Interactive explorations with complex pan, zoom, or brush behaviours17- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)18- Geographic visualisations with custom projections19- Visualisations requiring smooth, choreographed transitions20- Publication-quality graphics with fine-grained styling control21- Novel chart types not available in standard libraries2223**Consider alternatives for:**24- 3D visualisations - use Three.js instead2526## Core workflow2728### 1. Set up d3.js2930Import d3 at the top of your script:3132```javascript33import * as d3 from 'd3';34```3536Or use the CDN version (7.x):3738```html39<script src="https://d3js.org/d3.v7.min.js"></script>40```4142All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace.4344### 2. Choose the integration pattern4546**Pattern A: Direct DOM manipulation (recommended for most cases)**47Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:4849```javascript50function drawChart(data) {51 if (!data || data.length === 0) return;5253 const svg = d3.select('#chart'); // Select by ID, class, or DOM element5455 // Clear previous content56 svg.selectAll("*").remove();5758 // Set up dimensions59 const width = 800;60 const height = 400;61 const margin = { top: 20, right: 30, bottom: 40, left: 50 };6263 // Create scales, axes, and draw visualisation64 // ... d3 code here ...65}6667// Call when data changes68drawChart(myData);69```7071**Pattern B: Declarative rendering (for frameworks with templating)**72Use d3 for data calculations (scales, layouts) but render elements via your framework:7374```javascript75function getChartElements(data) {76 const xScale = d3.scaleLinear()77 .domain([0, d3.max(data, d => d.value)])78 .range([0, 400]);7980 return data.map((d, i) => ({81 x: 50,82 y: i * 30,83 width: xScale(d.value),84 height: 2585 }));86}8788// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}89// In Vue: v-for directive over the returned array90// In vanilla JS: Create elements manually from the returned data91```9293Use Pattern A for complex visualisations with transitions, interactions, or when leveraging d3's full capabilities. Use Pattern B for simpler visualisations or when your framework prefers declarative rendering.9495### 3. Structure the visualisation code9697Follow this standard structure in your drawing function:9899```javascript100function drawVisualization(data) {101 if (!data || data.length === 0) return;102103 const svg = d3.select('#chart'); // Or pass a selector/element104 svg.selectAll("*").remove(); // Clear previous render105106 // 1. Define dimensions107 const width = 800;108 const height = 400;109 const margin = { top: 20, right: 30, bottom: 40, left: 50 };110 const innerWidth = width - margin.left - margin.right;111 const innerHeight = height - margin.top - margin.bottom;112113 // 2. Create main group with margins114 const g = svg.append("g")115 .attr("transform", `translate(${margin.left},${margin.top})`);116117 // 3. Create scales118 const xScale = d3.scaleLinear()119 .domain([0, d3.max(data, d => d.x)])120 .range([0, innerWidth]);121122 const yScale = d3.scaleLinear()123 .domain([0, d3.max(data, d => d.y)])124 .range([innerHeight, 0]); // Note: inverted for SVG coordinates125126 // 4. Create and append axes127 const xAxis = d3.axisBottom(xScale);128 const yAxis = d3.axisLeft(yScale);129130 g.append("g")131 .attr("transform", `translate(0,${innerHeight})`)132 .call(xAxis);133134 g.append("g")135 .call(yAxis);136137 // 5. Bind data and create visual elements138 g.selectAll("circle")139 .data(data)140 .join("circle")141 .attr("cx", d => xScale(d.x))142 .attr("cy", d => yScale(d.y))143 .attr("r", 5)144 .attr("fill", "steelblue");145}146147// Call when data changes148drawVisualization(myData);149```150151### 4. Implement responsive sizing152153Make visualisations responsive to container size:154155```javascript156function setupResponsiveChart(containerId, data) {157 const container = document.getElementById(containerId);158 const svg = d3.select(`#${containerId}`).append('svg');159160 function updateChart() {161 const { width, height } = container.getBoundingClientRect();162 svg.attr('width', width).attr('height', height);163164 // Redraw visualisation with new dimensions165 drawChart(data, svg, width, height);166 }167168 // Update on initial load169 updateChart();170171 // Update on window resize172 window.addEventListener('resize', updateChart);173174 // Return cleanup function175 return () => window.removeEventListener('resize', updateChart);176}177178// Usage:179// const cleanup = setupResponsiveChart('chart-container', myData);180// cleanup(); // Call when component unmounts or element removed181```182183Or use ResizeObserver for more direct container monitoring:184185```javascript186function setupResponsiveChartWithObserver(svgElement, data) {187 const observer = new ResizeObserver(() => {188 const { width, height } = svgElement.getBoundingClientRect();189 d3.select(svgElement)190 .attr('width', width)191 .attr('height', height);192193 // Redraw visualisation194 drawChart(data, d3.select(svgElement), width, height);195 });196197 observer.observe(svgElement.parentElement);198 return () => observer.disconnect();199}200```201202## Common visualisation patterns203204### Bar chart205206```javascript207function drawBarChart(data, svgElement) {208 if (!data || data.length === 0) return;209210 const svg = d3.select(svgElement);211 svg.selectAll("*").remove();212213 const width = 800;214 const height = 400;215 const margin = { top: 20, right: 30, bottom: 40, left: 50 };216 const innerWidth = width - margin.left - margin.right;217 const innerHeight = height - margin.top - margin.bottom;218219 const g = svg.append("g")220 .attr("transform", `translate(${margin.left},${margin.top})`);221222 const xScale = d3.scaleBand()223 .domain(data.map(d => d.category))224 .range([0, innerWidth])225 .padding(0.1);226227 const yScale = d3.scaleLinear()228 .domain([0, d3.max(data, d => d.value)])229 .range([innerHeight, 0]);230231 g.append("g")232 .attr("transform", `translate(0,${innerHeight})`)233 .call(d3.axisBottom(xScale));234235 g.append("g")236 .call(d3.axisLeft(yScale));237238 g.selectAll("rect")239 .data(data)240 .join("rect")241 .attr("x", d => xScale(d.category))242 .attr("y", d => yScale(d.value))243 .attr("width", xScale.bandwidth())244 .attr("height", d => innerHeight - yScale(d.value))245 .attr("fill", "steelblue");246}247248// Usage:249// drawBarChart(myData, document.getElementById('chart'));250```251252### Line chart253254```javascript255const line = d3.line()256 .x(d => xScale(d.date))257 .y(d => yScale(d.value))258 .curve(d3.curveMonotoneX); // Smooth curve259260g.append("path")261 .datum(data)262 .attr("fill", "none")263 .attr("stroke", "steelblue")264 .attr("stroke-width", 2)265 .attr("d", line);266```267268### Scatter plot269270```javascript271g.selectAll("circle")272 .data(data)273 .join("circle")274 .attr("cx", d => xScale(d.x))275 .attr("cy", d => yScale(d.y))276 .attr("r", d => sizeScale(d.size)) // Optional: size encoding277 .attr("fill", d => colourScale(d.category)) // Optional: colour encoding278 .attr("opacity", 0.7);279```280281### Chord diagram282283A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:284285```javascript286function drawChordDiagram(data) {287 // data format: array of objects with source, target, and value288 // Example: [{ source: 'A', target: 'B', value: 10 }, ...]289290 if (!data || data.length === 0) return;291292 const svg = d3.select('#chart');293 svg.selectAll("*").remove();294295 const width = 600;296 const height = 600;297 const innerRadius = Math.min(width, height) * 0.3;298 const outerRadius = innerRadius + 30;299300 // Create matrix from data301 const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));302 const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));303304 data.forEach(d => {305 const i = nodes.indexOf(d.source);306 const j = nodes.indexOf(d.target);307 matrix[i][j] += d.value;308 matrix[j][i] += d.value;309 });310311 // Create chord layout312 const chord = d3.chord()313 .padAngle(0.05)314 .sortSubgroups(d3.descending);315316 const arc = d3.arc()317 .innerRadius(innerRadius)318 .outerRadius(outerRadius);319320 const ribbon = d3.ribbon()321 .source(d => d.source)322 .target(d => d.target);323324 const colourScale = d3.scaleOrdinal(d3.schemeCategory10)325 .domain(nodes);326327 const g = svg.append("g")328 .attr("transform", `translate(${width / 2},${height / 2})`);329330 const chords = chord(matrix);331332 // Draw ribbons333 g.append("g")334 .attr("fill-opacity", 0.67)335 .selectAll("path")336 .data(chords)337 .join("path")338 .attr("d", ribbon)339 .attr("fill", d => colourScale(nodes[d.source.index]))340 .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());341342 // Draw groups (arcs)343 const group = g.append("g")344 .selectAll("g")345 .data(chords.groups)346 .join("g");347348 group.append("path")349 .attr("d", arc)350 .attr("fill", d => colourScale(nodes[d.index]))351 .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());352353 // Add labels354 group.append("text")355 .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })356 .attr("dy", "0.31em")357 .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)358 .attr("text-anchor", d => d.angle > Math.PI ? "end" : null)359 .text((d, i) => nodes[i])360 .style("font-size", "12px");361}362```363364### Heatmap365366A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:367368```javascript369function drawHeatmap(data) {370 // data format: array of objects with row, column, and value371 // Example: [{ row: 'A', column: 'X', value: 10 }, ...]372373 if (!data || data.length === 0) return;374375 const svg = d3.select('#chart');376 svg.selectAll("*").remove();377378 const width = 800;379 const height = 600;380 const margin = { top: 100, right: 30, bottom: 30, left: 100 };381 const innerWidth = width - margin.left - margin.right;382 const innerHeight = height - margin.top - margin.bottom;383384 // Get unique rows and columns385 const rows = Array.from(new Set(data.map(d => d.row)));386 const columns = Array.from(new Set(data.map(d => d.column)));387388 const g = svg.append("g")389 .attr("transform", `translate(${margin.left},${margin.top})`);390391 // Create scales392 const xScale = d3.scaleBand()393 .domain(columns)394 .range([0, innerWidth])395 .padding(0.01);396397 const yScale = d3.scaleBand()398 .domain(rows)399 .range([0, innerHeight])400 .padding(0.01);401402 // Colour scale for values403 const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)404 .domain([0, d3.max(data, d => d.value)]);405406 // Draw rectangles407 g.selectAll("rect")408 .data(data)409 .join("rect")410 .attr("x", d => xScale(d.column))411 .attr("y", d => yScale(d.row))412 .attr("width", xScale.bandwidth())413 .attr("height", yScale.bandwidth())414 .attr("fill", d => colourScale(d.value));415416 // Add x-axis labels417 svg.append("g")418 .attr("transform", `translate(${margin.left},${margin.top})`)419 .selectAll("text")420 .data(columns)421 .join("text")422 .attr("x", d => xScale(d) + xScale.bandwidth() / 2)423 .attr("y", -10)424 .attr("text-anchor", "middle")425 .text(d => d)426 .style("font-size", "12px");427428 // Add y-axis labels429 svg.append("g")430 .attr("transform", `translate(${margin.left},${margin.top})`)431 .selectAll("text")432 .data(rows)433 .join("text")434 .attr("x", -10)435 .attr("y", d => yScale(d) + yScale.bandwidth() / 2)436 .attr("dy", "0.35em")437 .attr("text-anchor", "end")438 .text(d => d)439 .style("font-size", "12px");440441 // Add colour legend442 const legendWidth = 20;443 const legendHeight = 200;444 const legend = svg.append("g")445 .attr("transform", `translate(${width - 60},${margin.top})`);446447 const legendScale = d3.scaleLinear()448 .domain(colourScale.domain())449 .range([legendHeight, 0]);450451 const legendAxis = d3.axisRight(legendScale)452 .ticks(5);453454 // Draw colour gradient in legend455 for (let i = 0; i < legendHeight; i++) {456 legend.append("rect")457 .attr("y", i)458 .attr("width", legendWidth)459 .attr("height", 1)460 .attr("fill", colourScale(legendScale.invert(i)));461 }462463 legend.append("g")464 .attr("transform", `translate(${legendWidth},0)`)465 .call(legendAxis);466}467```468469### Pie chart470471```javascript472const pie = d3.pie()473 .value(d => d.value)474 .sort(null);475476const arc = d3.arc()477 .innerRadius(0)478 .outerRadius(Math.min(width, height) / 2 - 20);479480const colourScale = d3.scaleOrdinal(d3.schemeCategory10);481482const g = svg.append("g")483 .attr("transform", `translate(${width / 2},${height / 2})`);484485g.selectAll("path")486 .data(pie(data))487 .join("path")488 .attr("d", arc)489 .attr("fill", (d, i) => colourScale(i))490 .attr("stroke", "white")491 .attr("stroke-width", 2);492```493494### Force-directed network495496```javascript497const simulation = d3.forceSimulation(nodes)498 .force("link", d3.forceLink(links).id(d => d.id).distance(100))499 .force("charge", d3.forceManyBody().strength(-300))500 .force("center", d3.forceCenter(width / 2, height / 2));501502const link = g.selectAll("line")503 .data(links)504 .join("line")505 .attr("stroke", "#999")506 .attr("stroke-width", 1);507508const node = g.selectAll("circle")509 .data(nodes)510 .join("circle")511 .attr("r", 8)512 .attr("fill", "steelblue")513 .call(d3.drag()514 .on("start", dragstarted)515 .on("drag", dragged)516 .on("end", dragended));517518simulation.on("tick", () => {519 link520 .attr("x1", d => d.source.x)521 .attr("y1", d => d.source.y)522 .attr("x2", d => d.target.x)523 .attr("y2", d => d.target.y);524525 node526 .attr("cx", d => d.x)527 .attr("cy", d => d.y);528});529530function dragstarted(event) {531 if (!event.active) simulation.alphaTarget(0.3).restart();532 event.subject.fx = event.subject.x;533 event.subject.fy = event.subject.y;534}535536function dragged(event) {537 event.subject.fx = event.x;538 event.subject.fy = event.y;539}540541function dragended(event) {542 if (!event.active) simulation.alphaTarget(0);543 event.subject.fx = null;544 event.subject.fy = null;545}546```547548## Adding interactivity549550### Tooltips551552```javascript553// Create tooltip div (outside SVG)554const tooltip = d3.select("body").append("div")555 .attr("class", "tooltip")556 .style("position", "absolute")557 .style("visibility", "hidden")558 .style("background-color", "white")559 .style("border", "1px solid #ddd")560 .style("padding", "10px")561 .style("border-radius", "4px")562 .style("pointer-events", "none");563564// Add to elements565circles566 .on("mouseover", function(event, d) {567 d3.select(this).attr("opacity", 1);568 tooltip569 .style("visibility", "visible")570 .html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);571 })572 .on("mousemove", function(event) {573 tooltip574 .style("top", (event.pageY - 10) + "px")575 .style("left", (event.pageX + 10) + "px");576 })577 .on("mouseout", function() {578 d3.select(this).attr("opacity", 0.7);579 tooltip.style("visibility", "hidden");580 });581```582583### Zoom and pan584585```javascript586const zoom = d3.zoom()587 .scaleExtent([0.5, 10])588 .on("zoom", (event) => {589 g.attr("transform", event.transform);590 });591592svg.call(zoom);593```594595### Click interactions596597```javascript598circles599 .on("click", function(event, d) {600 // Handle click (dispatch event, update app state, etc.)601 console.log("Clicked:", d);602603 // Visual feedback604 d3.selectAll("circle").attr("fill", "steelblue");605 d3.select(this).attr("fill", "orange");606607 // Optional: dispatch custom event for your framework/app to listen to608 // window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));609 });610```611612## Transitions and animations613614Add smooth transitions to visual changes:615616```javascript617// Basic transition618circles619 .transition()620 .duration(750)621 .attr("r", 10);622623// Chained transitions624circles625 .transition()626 .duration(500)627 .attr("fill", "orange")628 .transition()629 .duration(500)630 .attr("r", 15);631632// Staggered transitions633circles634 .transition()635 .delay((d, i) => i * 50)636 .duration(500)637 .attr("cy", d => yScale(d.value));638639// Custom easing640circles641 .transition()642 .duration(1000)643 .ease(d3.easeBounceOut)644 .attr("r", 10);645```646647## Scales reference648649### Quantitative scales650651```javascript652// Linear scale653const xScale = d3.scaleLinear()654 .domain([0, 100])655 .range([0, 500]);656657// Log scale (for exponential data)658const logScale = d3.scaleLog()659 .domain([1, 1000])660 .range([0, 500]);661662// Power scale663const powScale = d3.scalePow()664 .exponent(2)665 .domain([0, 100])666 .range([0, 500]);667668// Time scale669const timeScale = d3.scaleTime()670 .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])671 .range([0, 500]);672```673674### Ordinal scales675676```javascript677// Band scale (for bar charts)678const bandScale = d3.scaleBand()679 .domain(['A', 'B', 'C', 'D'])680 .range([0, 400])681 .padding(0.1);682683// Point scale (for line/scatter categories)684const pointScale = d3.scalePoint()685 .domain(['A', 'B', 'C', 'D'])686 .range([0, 400]);687688// Ordinal scale (for colours)689const colourScale = d3.scaleOrdinal(d3.schemeCategory10);690```691692### Sequential scales693694```javascript695// Sequential colour scale696const colourScale = d3.scaleSequential(d3.interpolateBlues)697 .domain([0, 100]);698699// Diverging colour scale700const divScale = d3.scaleDiverging(d3.interpolateRdBu)701 .domain([-10, 0, 10]);702```703704## Best practices705706### Data preparation707708Always validate and prepare data before visualisation:709710```javascript711// Filter invalid values712const cleanData = data.filter(d => d.value != null && !isNaN(d.value));713714// Sort data if order matters715const sortedData = [...data].sort((a, b) => b.value - a.value);716717// Parse dates718const parsedData = data.map(d => ({719 ...d,720 date: d3.timeParse("%Y-%m-%d")(d.date)721}));722```723724### Performance optimisation725726For large datasets (>1000 elements):727728```javascript729// Use canvas instead of SVG for many elements730// Use quadtree for collision detection731// Simplify paths with d3.line().curve(d3.curveStep)732// Implement virtual scrolling for large lists733// Use requestAnimationFrame for custom animations734```735736### Accessibility737738Make visualisations accessible:739740```javascript741// Add ARIA labels742svg.attr("role", "img")743 .attr("aria-label", "Bar chart showing quarterly revenue");744745// Add title and description746svg.append("title").text("Quarterly Revenue 2024");747svg.append("desc").text("Bar chart showing revenue growth across four quarters");748749// Ensure sufficient colour contrast750// Provide keyboard navigation for interactive elements751// Include data table alternative752```753754### Styling755756Use consistent, professional styling:757758```javascript759// Define colour palettes upfront760const colours = {761 primary: '#4A90E2',762 secondary: '#7B68EE',763 background: '#F5F7FA',764 text: '#333333',765 gridLines: '#E0E0E0'766};767768// Apply consistent typography769svg.selectAll("text")770 .style("font-family", "Inter, sans-serif")771 .style("font-size", "12px");772773// Use subtle grid lines774g.selectAll(".tick line")775 .attr("stroke", colours.gridLines)776 .attr("stroke-dasharray", "2,2");777```778779## Common issues and solutions780781**Issue**: Axes not appearing782- Ensure scales have valid domains (check for NaN values)783- Verify axis is appended to correct group784- Check transform translations are correct785786**Issue**: Transitions not working787- Call `.transition()` before attribute changes788- Ensure elements have unique keys for proper data binding789- Check that useEffect dependencies include all changing data790791**Issue**: Responsive sizing not working792- Use ResizeObserver or window resize listener793- Update dimensions in state to trigger re-render794- Ensure SVG has width/height attributes or viewBox795796**Issue**: Performance problems797- Limit number of DOM elements (consider canvas for >1000 items)798- Debounce resize handlers799- Use `.join()` instead of separate enter/update/exit selections800- Avoid unnecessary re-renders by checking dependencies801802## Resources803804### references/805Contains detailed reference materials:806- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples807- `scale-reference.md` - Complete guide to d3 scales with examples808- `colour-schemes.md` - D3 colour schemes and palette recommendations809810### assets/811812Contains boilerplate templates:813814- `chart-template.js` - Starter template for basic chart815- `interactive-template.js` - Template with tooltips, zoom, and interactions816- `sample-data.json` - Example datasets for testing817818These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.819820To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.821
Full transparency — inspect the skill content before installing.