
D3 Viz
- 38 installs
- 16 repo stars
- Updated November 20, 2025
- jackspace/claudeskillz
Build custom interactive data visualizations with d3.js: bespoke charts, network diagrams, geographic maps, and choreographed SVG transitions.
About
Provides guidance for creating custom, interactive data visualizations with d3.js. A developer uses it for bespoke charts, force-directed graphs, geographic projections, and complex SVG interactions beyond standard charting libraries.
- Works across vanilla JS, React, Vue, and Svelte
- Covers force layouts, custom projections, and smooth transitions
D3 Viz by the numbers
- 38 all-time installs (skills.sh)
- Ranked #1,390 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jackspace/claudeskillz --skill d3-vizAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 38 |
|---|---|
| repo stars | ★ 16 |
| Last updated | November 20, 2025 |
| Repository | jackspace/claudeskillz ↗ |
What it does
Build custom interactive data visualizations with d3.js: bespoke charts, network diagrams, geographic maps, and choreographed SVG transitions.
Files
D3.js Visualisation
Overview
This 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.
When to use d3.js
Use d3.js for:
- Custom visualisations requiring unique visual encodings or layouts
- Interactive explorations with complex pan, zoom, or brush behaviours
- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)
- Geographic visualisations with custom projections
- Visualisations requiring smooth, choreographed transitions
- Publication-quality graphics with fine-grained styling control
- Novel chart types not available in standard libraries
Consider alternatives for:
- 3D visualisations - use Three.js instead
Core workflow
1. Set up d3.js
Import d3 at the top of your script:
import * as d3 from 'd3';Or use the CDN version (7.x):
<script src="https://d3js.org/d3.v7.min.js"></script>All modules (scales, axes, shapes, transitions, etc.) are accessible through the d3 namespace.
2. Choose the integration pattern
Pattern A: Direct DOM manipulation (recommended for most cases) Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:
function drawChart(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // Select by ID, class, or DOM element
// Clear previous content
svg.selectAll("*").remove();
// Set up dimensions
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
// Create scales, axes, and draw visualisation
// ... d3 code here ...
}
// Call when data changes
drawChart(myData);Pattern B: Declarative rendering (for frameworks with templating) Use d3 for data calculations (scales, layouts) but render elements via your framework:
function getChartElements(data) {
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, 400]);
return data.map((d, i) => ({
x: 50,
y: i * 30,
width: xScale(d.value),
height: 25
}));
}
// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
// In Vue: v-for directive over the returned array
// In vanilla JS: Create elements manually from the returned dataUse 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.
3. Structure the visualisation code
Follow this standard structure in your drawing function:
function drawVisualization(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // Or pass a selector/element
svg.selectAll("*").remove(); // Clear previous render
// 1. Define dimensions
const width = 800;
const 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;
// 2. Create main group with margins
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 3. Create 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]); // Note: inverted for SVG coordinates
// 4. Create and append axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
g.append("g")
.call(yAxis);
// 5. Bind data and create visual 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");
}
// Call when data changes
drawVisualization(myData);4. Implement responsive sizing
Make visualisations responsive to container size:
function setupResponsiveChart(containerId, data) {
const container = document.getElementById(containerId);
const svg = d3.select(`#${containerId}`).append('svg');
function updateChart() {
const { width, height } = container.getBoundingClientRect();
svg.attr('width', width).attr('height', height);
// Redraw visualisation with new dimensions
drawChart(data, svg, width, height);
}
// Update on initial load
updateChart();
// Update on window resize
window.addEventListener('resize', updateChart);
// Return cleanup function
return () => window.removeEventListener('resize', updateChart);
}
// Usage:
// const cleanup = setupResponsiveChart('chart-container', myData);
// cleanup(); // Call when component unmounts or element removedOr use ResizeObserver for more direct container monitoring:
function setupResponsiveChartWithObserver(svgElement, data) {
const observer = new ResizeObserver(() => {
const { width, height } = svgElement.getBoundingClientRect();
d3.select(svgElement)
.attr('width', width)
.attr('height', height);
// Redraw visualisation
drawChart(data, d3.select(svgElement), width, height);
});
observer.observe(svgElement.parentElement);
return () => observer.disconnect();
}Common visualisation patterns
Bar chart
function drawBarChart(data, svgElement) {
if (!data || data.length === 0) return;
const svg = d3.select(svgElement);
svg.selectAll("*").remove();
const width = 800;
const 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})`);
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.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
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");
}
// Usage:
// drawBarChart(myData, document.getElementById('chart'));Line chart
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX); // Smooth curve
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)) // Optional: size encoding
.attr("fill", d => colourScale(d.category)) // Optional: colour encoding
.attr("opacity", 0.7);Chord diagram
A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:
function drawChordDiagram(data) {
// data format: array of objects with source, target, and value
// Example: [{ source: 'A', target: 'B', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 600;
const height = 600;
const innerRadius = Math.min(width, height) * 0.3;
const outerRadius = innerRadius + 30;
// Create matrix from data
const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));
data.forEach(d => {
const i = nodes.indexOf(d.source);
const j = nodes.indexOf(d.target);
matrix[i][j] += d.value;
matrix[j][i] += d.value;
});
// Create chord layout
const chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.source(d => d.source)
.target(d => d.target);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(nodes);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
const chords = chord(matrix);
// Draw ribbons
g.append("g")
.attr("fill-opacity", 0.67)
.selectAll("path")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => colourScale(nodes[d.source.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());
// Draw groups (arcs)
const group = g.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("d", arc)
.attr("fill", d => colourScale(nodes[d.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());
// Add labels
group.append("text")
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", "0.31em")
.attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
.text((d, i) => nodes[i])
.style("font-size", "12px");
}Heatmap
A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:
function drawHeatmap(data) {
// data format: array of objects with row, column, and value
// Example: [{ row: 'A', column: 'X', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const margin = { top: 100, right: 30, bottom: 30, left: 100 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Get unique rows and columns
const rows = Array.from(new Set(data.map(d => d.row)));
const columns = Array.from(new Set(data.map(d => d.column)));
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Create scales
const xScale = d3.scaleBand()
.domain(columns)
.range([0, innerWidth])
.padding(0.01);
const yScale = d3.scaleBand()
.domain(rows)
.range([0, innerHeight])
.padding(0.01);
// Colour scale for values
const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
.domain([0, d3.max(data, d => d.value)]);
// Draw rectangles
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 => colourScale(d.value));
// Add x-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(columns)
.join("text")
.attr("x", d => xScale(d) + xScale.bandwidth() / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.text(d => d)
.style("font-size", "12px");
// Add y-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(rows)
.join("text")
.attr("x", -10)
.attr("y", d => yScale(d) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(d => d)
.style("font-size", "12px");
// Add colour legend
const legendWidth = 20;
const legendHeight = 200;
const legend = svg.append("g")
.attr("transform", `translate(${width - 60},${margin.top})`);
const legendScale = d3.scaleLinear()
.domain(colourScale.domain())
.range([legendHeight, 0]);
const legendAxis = d3.axisRight(legendScale)
.ticks(5);
// Draw colour gradient in legend
for (let i = 0; i < legendHeight; i++) {
legend.append("rect")
.attr("y", i)
.attr("width", legendWidth)
.attr("height", 1)
.attr("fill", colourScale(legendScale.invert(i)));
}
legend.append("g")
.attr("transform", `translate(${legendWidth},0)`)
.call(legendAxis);
}Pie 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 colourScale = 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) => colourScale(i))
.attr("stroke", "white")
.attr("stroke-width", 2);Force-directed network
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")
.attr("stroke-width", 1);
const node = g.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 8)
.attr("fill", "steelblue")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
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);
});
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}Adding interactivity
Tooltips
// Create tooltip div (outside SVG)
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "1px solid #ddd")
.style("padding", "10px")
.style("border-radius", "4px")
.style("pointer-events", "none");
// Add to elements
circles
.on("mouseover", function(event, d) {
d3.select(this).attr("opacity", 1);
tooltip
.style("visibility", "visible")
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
})
.on("mousemove", function(event) {
tooltip
.style("top", (event.pageY - 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", function() {
d3.select(this).attr("opacity", 0.7);
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);Click interactions
circles
.on("click", function(event, d) {
// Handle click (dispatch event, update app state, etc.)
console.log("Clicked:", d);
// Visual feedback
d3.selectAll("circle").attr("fill", "steelblue");
d3.select(this).attr("fill", "orange");
// Optional: dispatch custom event for your framework/app to listen to
// window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
});Transitions and animations
Add smooth transitions to visual changes:
// Basic transition
circles
.transition()
.duration(750)
.attr("r", 10);
// Chained transitions
circles
.transition()
.duration(500)
.attr("fill", "orange")
.transition()
.duration(500)
.attr("r", 15);
// Staggered transitions
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);Scales reference
Quantitative scales
// Linear scale
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
// Log scale (for exponential data)
const logScale = d3.scaleLog()
.domain([1, 1000])
.range([0, 500]);
// Power scale
const powScale = d3.scalePow()
.exponent(2)
.domain([0, 100])
.range([0, 500]);
// Time scale
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
.range([0, 500]);Ordinal scales
// Band scale (for bar charts)
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.1);
// Point scale (for line/scatter categories)
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400]);
// Ordinal scale (for colours)
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);Sequential scales
// Sequential colour scale
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
// Diverging colour scale
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);Best practices
Data preparation
Always validate and prepare data before visualisation:
// Filter invalid values
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));
// Sort data if order matters
const sortedData = [...data].sort((a, b) => b.value - a.value);
// Parse dates
const parsedData = data.map(d => ({
...d,
date: d3.timeParse("%Y-%m-%d")(d.date)
}));Performance optimisation
For large datasets (>1000 elements):
// Use canvas instead of SVG for many elements
// Use quadtree for collision detection
// Simplify paths with d3.line().curve(d3.curveStep)
// Implement virtual scrolling for large lists
// Use requestAnimationFrame for custom animationsAccessibility
Make visualisations accessible:
// Add ARIA labels
svg.attr("role", "img")
.attr("aria-label", "Bar chart showing quarterly revenue");
// Add title and description
svg.append("title").text("Quarterly Revenue 2024");
svg.append("desc").text("Bar chart showing revenue growth across four quarters");
// Ensure sufficient colour contrast
// Provide keyboard navigation for interactive elements
// Include data table alternativeStyling
Use consistent, professional styling:
// Define colour palettes upfront
const colours = {
primary: '#4A90E2',
secondary: '#7B68EE',
background: '#F5F7FA',
text: '#333333',
gridLines: '#E0E0E0'
};
// Apply consistent typography
svg.selectAll("text")
.style("font-family", "Inter, sans-serif")
.style("font-size", "12px");
// Use subtle grid lines
g.selectAll(".tick line")
.attr("stroke", colours.gridLines)
.attr("stroke-dasharray", "2,2");Common issues and solutions
Issue: Axes not appearing
- Ensure scales have valid domains (check for NaN values)
- Verify axis is appended to correct group
- Check transform translations are correct
Issue: Transitions not working
- Call
.transition()before attribute changes - Ensure elements have unique keys for proper data binding
- Check that useEffect dependencies include all changing data
Issue: Responsive sizing not working
- Use ResizeObserver or window resize listener
- Update dimensions in state to trigger re-render
- Ensure SVG has width/height attributes or viewBox
Issue: Performance problems
- Limit number of DOM elements (consider canvas for >1000 items)
- Debounce resize handlers
- Use
.join()instead of separate enter/update/exit selections - Avoid unnecessary re-renders by checking dependencies
Resources
references/
Contains detailed reference materials:
d3-patterns.md- Comprehensive collection of visualisation patterns and code examplesscale-reference.md- Complete guide to d3 scales with examplescolour-schemes.md- D3 colour schemes and palette recommendations
assets/
Contains boilerplate templates:
chart-template.js- Starter template for basic chartinteractive-template.js- Template with tooltips, zoom, and interactionssample-data.json- Example datasets for testing
These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.
To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.
{
"sections": {
"Common issues and solutions": "**Issue**: Axes not appearing\r\n- Ensure scales have valid domains (check for NaN values)\r\n- Verify axis is appended to correct group\r\n- Check transform translations are correct\r\n\r\n**Issue**: Transitions not working\r\n- Call `.transition()` before attribute changes\r\n- Ensure elements have unique keys for proper data binding\r\n- Check that useEffect dependencies include all changing data\r\n\r\n**Issue**: Responsive sizing not working\r\n- Use ResizeObserver or window resize listener\r\n- Update dimensions in state to trigger re-render\r\n- Ensure SVG has width/height attributes or viewBox\r\n\r\n**Issue**: Performance problems\r\n- Limit number of DOM elements (consider canvas for >1000 items)\r\n- Debounce resize handlers\r\n- Use `.join()` instead of separate enter/update/exit selections\r\n- Avoid unnecessary re-renders by checking dependencies",
"Best practices": "### Data preparation\r\n\r\nAlways validate and prepare data before visualisation:\r\n\r\n```javascript\r\n// Filter invalid values\r\nconst cleanData = data.filter(d => d.value != null && !isNaN(d.value));\r\n\r\n// Sort data if order matters\r\nconst sortedData = [...data].sort((a, b) => b.value - a.value);\r\n\r\n// Parse dates\r\nconst parsedData = data.map(d => ({\r\n ...d,\r\n date: d3.timeParse(\"%Y-%m-%d\")(d.date)\r\n}));\r\n```\r\n\r\n### Performance optimisation\r\n\r\nFor large datasets (>1000 elements):\r\n\r\n```javascript\r\n// Use canvas instead of SVG for many elements\r\n// Use quadtree for collision detection\r\n// Simplify paths with d3.line().curve(d3.curveStep)\r\n// Implement virtual scrolling for large lists\r\n// Use requestAnimationFrame for custom animations\r\n```\r\n\r\n### Accessibility\r\n\r\nMake visualisations accessible:\r\n\r\n```javascript\r\n// Add ARIA labels\r\nsvg.attr(\"role\", \"img\")\r\n .attr(\"aria-label\", \"Bar chart showing quarterly revenue\");\r\n\r\n// Add title and description\r\nsvg.append(\"title\").text(\"Quarterly Revenue 2024\");\r\nsvg.append(\"desc\").text(\"Bar chart showing revenue growth across four quarters\");\r\n\r\n// Ensure sufficient colour contrast\r\n// Provide keyboard navigation for interactive elements\r\n// Include data table alternative\r\n```\r\n\r\n### Styling\r\n\r\nUse consistent, professional styling:\r\n\r\n```javascript\r\n// Define colour palettes upfront\r\nconst colours = {\r\n primary: '#4A90E2',\r\n secondary: '#7B68EE',\r\n background: '#F5F7FA',\r\n text: '#333333',\r\n gridLines: '#E0E0E0'\r\n};\r\n\r\n// Apply consistent typography\r\nsvg.selectAll(\"text\")\r\n .style(\"font-family\", \"Inter, sans-serif\")\r\n .style(\"font-size\", \"12px\");\r\n\r\n// Use subtle grid lines\r\ng.selectAll(\".tick line\")\r\n .attr(\"stroke\", colours.gridLines)\r\n .attr(\"stroke-dasharray\", \"2,2\");\r\n```",
"When to use d3.js": "**Use d3.js for:**\r\n- Custom visualisations requiring unique visual encodings or layouts\r\n- Interactive explorations with complex pan, zoom, or brush behaviours\r\n- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)\r\n- Geographic visualisations with custom projections\r\n- Visualisations requiring smooth, choreographed transitions\r\n- Publication-quality graphics with fine-grained styling control\r\n- Novel chart types not available in standard libraries\r\n\r\n**Consider alternatives for:**\r\n- 3D visualisations - use Three.js instead",
"Adding interactivity": "### Tooltips\r\n\r\n```javascript\r\n// Create tooltip div (outside SVG)\r\nconst tooltip = d3.select(\"body\").append(\"div\")\r\n .attr(\"class\", \"tooltip\")\r\n .style(\"position\", \"absolute\")\r\n .style(\"visibility\", \"hidden\")\r\n .style(\"background-color\", \"white\")\r\n .style(\"border\", \"1px solid #ddd\")\r\n .style(\"padding\", \"10px\")\r\n .style(\"border-radius\", \"4px\")\r\n .style(\"pointer-events\", \"none\");\r\n\r\n// Add to elements\r\ncircles\r\n .on(\"mouseover\", function(event, d) {\r\n d3.select(this).attr(\"opacity\", 1);\r\n tooltip\r\n .style(\"visibility\", \"visible\")\r\n .html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);\r\n })\r\n .on(\"mousemove\", function(event) {\r\n tooltip\r\n .style(\"top\", (event.pageY - 10) + \"px\")\r\n .style(\"left\", (event.pageX + 10) + \"px\");\r\n })\r\n .on(\"mouseout\", function() {\r\n d3.select(this).attr(\"opacity\", 0.7);\r\n tooltip.style(\"visibility\", \"hidden\");\r\n });\r\n```\r\n\r\n### Zoom and pan\r\n\r\n```javascript\r\nconst zoom = d3.zoom()\r\n .scaleExtent([0.5, 10])\r\n .on(\"zoom\", (event) => {\r\n g.attr(\"transform\", event.transform);\r\n });\r\n\r\nsvg.call(zoom);\r\n```\r\n\r\n### Click interactions\r\n\r\n```javascript\r\ncircles\r\n .on(\"click\", function(event, d) {\r\n // Handle click (dispatch event, update app state, etc.)\r\n console.log(\"Clicked:\", d);\r\n\r\n // Visual feedback\r\n d3.selectAll(\"circle\").attr(\"fill\", \"steelblue\");\r\n d3.select(this).attr(\"fill\", \"orange\");\r\n\r\n // Optional: dispatch custom event for your framework/app to listen to\r\n // window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));\r\n });\r\n```",
"Overview": "This 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.",
"Transitions and animations": "Add smooth transitions to visual changes:\r\n\r\n```javascript\r\n// Basic transition\r\ncircles\r\n .transition()\r\n .duration(750)\r\n .attr(\"r\", 10);\r\n\r\n// Chained transitions\r\ncircles\r\n .transition()\r\n .duration(500)\r\n .attr(\"fill\", \"orange\")\r\n .transition()\r\n .duration(500)\r\n .attr(\"r\", 15);\r\n\r\n// Staggered transitions\r\ncircles\r\n .transition()\r\n .delay((d, i) => i * 50)\r\n .duration(500)\r\n .attr(\"cy\", d => yScale(d.value));\r\n\r\n// Custom easing\r\ncircles\r\n .transition()\r\n .duration(1000)\r\n .ease(d3.easeBounceOut)\r\n .attr(\"r\", 10);\r\n```",
"Scales reference": "### Quantitative scales\r\n\r\n```javascript\r\n// Linear scale\r\nconst xScale = d3.scaleLinear()\r\n .domain([0, 100])\r\n .range([0, 500]);\r\n\r\n// Log scale (for exponential data)\r\nconst logScale = d3.scaleLog()\r\n .domain([1, 1000])\r\n .range([0, 500]);\r\n\r\n// Power scale\r\nconst powScale = d3.scalePow()\r\n .exponent(2)\r\n .domain([0, 100])\r\n .range([0, 500]);\r\n\r\n// Time scale\r\nconst timeScale = d3.scaleTime()\r\n .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])\r\n .range([0, 500]);\r\n```\r\n\r\n### Ordinal scales\r\n\r\n```javascript\r\n// Band scale (for bar charts)\r\nconst bandScale = d3.scaleBand()\r\n .domain(['A', 'B', 'C', 'D'])\r\n .range([0, 400])\r\n .padding(0.1);\r\n\r\n// Point scale (for line/scatter categories)\r\nconst pointScale = d3.scalePoint()\r\n .domain(['A', 'B', 'C', 'D'])\r\n .range([0, 400]);\r\n\r\n// Ordinal scale (for colours)\r\nconst colourScale = d3.scaleOrdinal(d3.schemeCategory10);\r\n```\r\n\r\n### Sequential scales\r\n\r\n```javascript\r\n// Sequential colour scale\r\nconst colourScale = d3.scaleSequential(d3.interpolateBlues)\r\n .domain([0, 100]);\r\n\r\n// Diverging colour scale\r\nconst divScale = d3.scaleDiverging(d3.interpolateRdBu)\r\n .domain([-10, 0, 10]);\r\n```",
"Core workflow": "### 1. Set up d3.js\r\n\r\nImport d3 at the top of your script:\r\n\r\n```javascript\r\nimport * as d3 from 'd3';\r\n```\r\n\r\nOr use the CDN version (7.x):\r\n\r\n```html\r\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\r\n```\r\n\r\nAll modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace.\r\n\r\n### 2. Choose the integration pattern\r\n\r\n**Pattern A: Direct DOM manipulation (recommended for most cases)**\r\nUse d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:\r\n\r\n```javascript\r\nfunction drawChart(data) {\r\n if (!data || data.length === 0) return;\r\n\r\n const svg = d3.select('#chart'); // Select by ID, class, or DOM element\r\n\r\n // Clear previous content\r\n svg.selectAll(\"*\").remove();\r\n\r\n // Set up dimensions\r\n const width = 800;\r\n const height = 400;\r\n const margin = { top: 20, right: 30, bottom: 40, left: 50 };\r\n\r\n // Create scales, axes, and draw visualisation\r\n // ... d3 code here ...\r\n}\r\n\r\n// Call when data changes\r\ndrawChart(myData);\r\n```\r\n\r\n**Pattern B: Declarative rendering (for frameworks with templating)**\r\nUse d3 for data calculations (scales, layouts) but render elements via your framework:\r\n\r\n```javascript\r\nfunction getChartElements(data) {\r\n const xScale = d3.scaleLinear()\r\n .domain([0, d3.max(data, d => d.value)])\r\n .range([0, 400]);\r\n\r\n return data.map((d, i) => ({\r\n x: 50,\r\n y: i * 30,\r\n width: xScale(d.value),\r\n height: 25\r\n }));\r\n}\r\n\r\n// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill=\"steelblue\" />)}\r\n// In Vue: v-for directive over the returned array\r\n// In vanilla JS: Create elements manually from the returned data\r\n```\r\n\r\nUse 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.\r\n\r\n### 3. Structure the visualisation code\r\n\r\nFollow this standard structure in your drawing function:\r\n\r\n```javascript\r\nfunction drawVisualization(data) {\r\n if (!data || data.length === 0) return;\r\n\r\n const svg = d3.select('#chart'); // Or pass a selector/element\r\n svg.selectAll(\"*\").remove(); // Clear previous render\r\n\r\n // 1. Define dimensions\r\n const width = 800;\r\n const height = 400;\r\n const margin = { top: 20, right: 30, bottom: 40, left: 50 };\r\n const innerWidth = width - margin.left - margin.right;\r\n const innerHeight = height - margin.top - margin.bottom;\r\n\r\n // 2. Create main group with margins\r\n const g = svg.append(\"g\")\r\n .attr(\"transform\", `translate(${margin.left},${margin.top})`);\r\n\r\n // 3. Create scales\r\n const xScale = d3.scaleLinear()\r\n .domain([0, d3.max(data, d => d.x)])\r\n .range([0, innerWidth]);\r\n\r\n const yScale = d3.scaleLinear()\r\n .domain([0, d3.max(data, d => d.y)])\r\n .range([innerHeight, 0]); // Note: inverted for SVG coordinates\r\n\r\n // 4. Create and append axes\r\n const xAxis = d3.axisBottom(xScale);\r\n const yAxis = d3.axisLeft(yScale);\r\n\r\n g.append(\"g\")\r\n .attr(\"transform\", `translate(0,${innerHeight})`)\r\n .call(xAxis);\r\n\r\n g.append(\"g\")\r\n .call(yAxis);\r\n\r\n // 5. Bind data and create visual elements\r\n g.selectAll(\"circle\")\r\n .data(data)\r\n .join(\"circle\")\r\n .attr(\"cx\", d => xScale(d.x))\r\n .attr(\"cy\", d => yScale(d.y))\r\n .attr(\"r\", 5)\r\n .attr(\"fill\", \"steelblue\");\r\n}\r\n\r\n// Call when data changes\r\ndrawVisualization(myData);\r\n```\r\n\r\n### 4. Implement responsive sizing\r\n\r\nMake visualisations responsive to container size:\r\n\r\n```javascript\r\nfunction setupResponsiveChart(containerId, data) {\r\n const container = document.getElementById(containerId);\r\n const svg = d3.select(`#${containerId}`).append('svg');\r\n\r\n function updateChart() {\r\n const { width, height } = container.getBoundingClientRect();\r\n svg.attr('width', width).attr('height', height);\r\n\r\n // Redraw visualisation with new dimensions\r\n drawChart(data, svg, width, height);\r\n }\r\n\r\n // Update on initial load\r\n updateChart();\r\n\r\n // Update on window resize\r\n window.addEventListener('resize', updateChart);\r\n\r\n // Return cleanup function\r\n return () => window.removeEventListener('resize', updateChart);\r\n}\r\n\r\n// Usage:\r\n// const cleanup = setupResponsiveChart('chart-container', myData);\r\n// cleanup(); // Call when component unmounts or element removed\r\n```\r\n\r\nOr use ResizeObserver for more direct container monitoring:\r\n\r\n```javascript\r\nfunction setupResponsiveChartWithObserver(svgElement, data) {\r\n const observer = new ResizeObserver(() => {\r\n const { width, height } = svgElement.getBoundingClientRect();\r\n d3.select(svgElement)\r\n .attr('width', width)\r\n .attr('height', height);\r\n\r\n // Redraw visualisation\r\n drawChart(data, d3.select(svgElement), width, height);\r\n });\r\n\r\n observer.observe(svgElement.parentElement);\r\n return () => observer.disconnect();\r\n}\r\n```",
"Common visualisation patterns": "### Bar chart\r\n\r\n```javascript\r\nfunction drawBarChart(data, svgElement) {\r\n if (!data || data.length === 0) return;\r\n\r\n const svg = d3.select(svgElement);\r\n svg.selectAll(\"*\").remove();\r\n\r\n const width = 800;\r\n const height = 400;\r\n const margin = { top: 20, right: 30, bottom: 40, left: 50 };\r\n const innerWidth = width - margin.left - margin.right;\r\n const innerHeight = height - margin.top - margin.bottom;\r\n\r\n const g = svg.append(\"g\")\r\n .attr(\"transform\", `translate(${margin.left},${margin.top})`);\r\n\r\n const xScale = d3.scaleBand()\r\n .domain(data.map(d => d.category))\r\n .range([0, innerWidth])\r\n .padding(0.1);\r\n\r\n const yScale = d3.scaleLinear()\r\n .domain([0, d3.max(data, d => d.value)])\r\n .range([innerHeight, 0]);\r\n\r\n g.append(\"g\")\r\n .attr(\"transform\", `translate(0,${innerHeight})`)\r\n .call(d3.axisBottom(xScale));\r\n\r\n g.append(\"g\")\r\n .call(d3.axisLeft(yScale));\r\n\r\n g.selectAll(\"rect\")\r\n .data(data)\r\n .join(\"rect\")\r\n .attr(\"x\", d => xScale(d.category))\r\n .attr(\"y\", d => yScale(d.value))\r\n .attr(\"width\", xScale.bandwidth())\r\n .attr(\"height\", d => innerHeight - yScale(d.value))\r\n .attr(\"fill\", \"steelblue\");\r\n}\r\n\r\n// Usage:\r\n// drawBarChart(myData, document.getElementById('chart'));\r\n```\r\n\r\n### Line chart\r\n\r\n```javascript\r\nconst line = d3.line()\r\n .x(d => xScale(d.date))\r\n .y(d => yScale(d.value))\r\n .curve(d3.curveMonotoneX); // Smooth curve\r\n\r\ng.append(\"path\")\r\n .datum(data)\r\n .attr(\"fill\", \"none\")\r\n .attr(\"stroke\", \"steelblue\")\r\n .attr(\"stroke-width\", 2)\r\n .attr(\"d\", line);\r\n```\r\n\r\n### Scatter plot\r\n\r\n```javascript\r\ng.selectAll(\"circle\")\r\n .data(data)\r\n .join(\"circle\")\r\n .attr(\"cx\", d => xScale(d.x))\r\n .attr(\"cy\", d => yScale(d.y))\r\n .attr(\"r\", d => sizeScale(d.size)) // Optional: size encoding\r\n .attr(\"fill\", d => colourScale(d.category)) // Optional: colour encoding\r\n .attr(\"opacity\", 0.7);\r\n```\r\n\r\n### Chord diagram\r\n\r\nA chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:\r\n\r\n```javascript\r\nfunction drawChordDiagram(data) {\r\n // data format: array of objects with source, target, and value\r\n // Example: [{ source: 'A', target: 'B', value: 10 }, ...]\r\n\r\n if (!data || data.length === 0) return;\r\n\r\n const svg = d3.select('#chart');\r\n svg.selectAll(\"*\").remove();\r\n\r\n const width = 600;\r\n const height = 600;\r\n const innerRadius = Math.min(width, height) * 0.3;\r\n const outerRadius = innerRadius + 30;\r\n\r\n // Create matrix from data\r\n const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));\r\n const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));\r\n\r\n data.forEach(d => {\r\n const i = nodes.indexOf(d.source);\r\n const j = nodes.indexOf(d.target);\r\n matrix[i][j] += d.value;\r\n matrix[j][i] += d.value;\r\n });\r\n\r\n // Create chord layout\r\n const chord = d3.chord()\r\n .padAngle(0.05)\r\n .sortSubgroups(d3.descending);\r\n\r\n const arc = d3.arc()\r\n .innerRadius(innerRadius)\r\n .outerRadius(outerRadius);\r\n\r\n const ribbon = d3.ribbon()\r\n .source(d => d.source)\r\n .target(d => d.target);\r\n\r\n const colourScale = d3.scaleOrdinal(d3.schemeCategory10)\r\n .domain(nodes);\r\n\r\n const g = svg.append(\"g\")\r\n .attr(\"transform\", `translate(${width / 2},${height / 2})`);\r\n\r\n const chords = chord(matrix);\r\n\r\n // Draw ribbons\r\n g.append(\"g\")\r\n .attr(\"fill-opacity\", 0.67)\r\n .selectAll(\"path\")\r\n .data(chords)\r\n .join(\"path\")\r\n .attr(\"d\", ribbon)\r\n .attr(\"fill\", d => colourScale(nodes[d.source.index]))\r\n .attr(\"stroke\", d => d3.rgb(colourScale(nodes[d.source.index])).darker());\r\n\r\n // Draw groups (arcs)\r\n const group = g.append(\"g\")\r\n .selectAll(\"g\")\r\n .data(chords.groups)\r\n .join(\"g\");\r\n\r\n group.append(\"path\")\r\n .attr(\"d\", arc)\r\n .attr(\"fill\", d => colourScale(nodes[d.index]))\r\n .attr(\"stroke\", d => d3.rgb(colourScale(nodes[d.index])).darker());\r\n\r\n // Add labels\r\n group.append(\"text\")\r\n .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })\r\n .attr(\"dy\", \"0.31em\")\r\n .attr(\"transform\", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? \"rotate(180)\" : \"\"}`)\r\n .attr(\"text-anchor\", d => d.angle > Math.PI ? \"end\" : null)\r\n .text((d, i) => nodes[i])\r\n .style(\"font-size\", \"12px\");\r\n}\r\n```\r\n\r\n### Heatmap\r\n\r\nA heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:\r\n\r\n```javascript\r\nfunction drawHeatmap(data) {\r\n // data format: array of objects with row, column, and value\r\n // Example: [{ row: 'A', column: 'X', value: 10 }, ...]\r\n\r\n if (!data || data.length === 0) return;\r\n\r\n const svg = d3.select('#chart');\r\n svg.selectAll(\"*\").remove();\r\n\r\n const width = 800;\r\n const height = 600;\r\n const margin = { top: 100, right: 30, bottom: 30, left: 100 };\r\n const innerWidth = width - margin.left - margin.right;\r\n const innerHeight = height - margin.top - margin.bottom;\r\n\r\n // Get unique rows and columns\r\n const rows = Array.from(new Set(data.map(d => d.row)));\r\n const columns = Array.from(new Set(data.map(d => d.column)));\r\n\r\n const g = svg.append(\"g\")\r\n .attr(\"transform\", `translate(${margin.left},${margin.top})`);\r\n\r\n // Create scales\r\n const xScale = d3.scaleBand()\r\n .domain(columns)\r\n .range([0, innerWidth])\r\n .padding(0.01);\r\n\r\n const yScale = d3.scaleBand()\r\n .domain(rows)\r\n .range([0, innerHeight])\r\n .padding(0.01);\r\n\r\n // Colour scale for values\r\n const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)\r\n .domain([0, d3.max(data, d => d.value)]);\r\n\r\n // Draw rectangles\r\n g.selectAll(\"rect\")\r\n .data(data)\r\n .join(\"rect\")\r\n .attr(\"x\", d => xScale(d.column))\r\n .attr(\"y\", d => yScale(d.row))\r\n .attr(\"width\", xScale.bandwidth())\r\n .attr(\"height\", yScale.bandwidth())\r\n .attr(\"fill\", d => colourScale(d.value));\r\n\r\n // Add x-axis labels\r\n svg.append(\"g\")\r\n .attr(\"transform\", `translate(${margin.left},${margin.top})`)\r\n .selectAll(\"text\")\r\n .data(columns)\r\n .join(\"text\")\r\n .attr(\"x\", d => xScale(d) + xScale.bandwidth() / 2)\r\n .attr(\"y\", -10)\r\n .attr(\"text-anchor\", \"middle\")\r\n .text(d => d)\r\n .style(\"font-size\", \"12px\");\r\n\r\n // Add y-axis labels\r\n svg.append(\"g\")\r\n .attr(\"transform\", `translate(${margin.left},${margin.top})`)\r\n .selectAll(\"text\")\r\n .data(rows)\r\n .join(\"text\")\r\n .attr(\"x\", -10)\r\n .attr(\"y\", d => yScale(d) + yScale.bandwidth() / 2)\r\n .attr(\"dy\", \"0.35em\")\r\n .attr(\"text-anchor\", \"end\")\r\n .text(d => d)\r\n .style(\"font-size\", \"12px\");\r\n\r\n // Add colour legend\r\n const legendWidth = 20;\r\n const legendHeight = 200;\r\n const legend = svg.append(\"g\")\r\n .attr(\"transform\", `translate(${width - 60},${margin.top})`);\r\n\r\n const legendScale = d3.scaleLinear()\r\n .domain(colourScale.domain())\r\n .range([legendHeight, 0]);\r\n\r\n const legendAxis = d3.axisRight(legendScale)\r\n .ticks(5);\r\n\r\n // Draw colour gradient in legend\r\n for (let i = 0; i < legendHeight; i++) {\r\n legend.append(\"rect\")\r\n .attr(\"y\", i)\r\n .attr(\"width\", legendWidth)\r\n .attr(\"height\", 1)\r\n .attr(\"fill\", colourScale(legendScale.invert(i)));\r\n }\r\n\r\n legend.append(\"g\")\r\n .attr(\"transform\", `translate(${legendWidth},0)`)\r\n .call(legendAxis);\r\n}\r\n```\r\n\r\n### Pie chart\r\n\r\n```javascript\r\nconst pie = d3.pie()\r\n .value(d => d.value)\r\n .sort(null);\r\n\r\nconst arc = d3.arc()\r\n .innerRadius(0)\r\n .outerRadius(Math.min(width, height) / 2 - 20);\r\n\r\nconst colourScale = d3.scaleOrdinal(d3.schemeCategory10);\r\n\r\nconst g = svg.append(\"g\")\r\n .attr(\"transform\", `translate(${width / 2},${height / 2})`);\r\n\r\ng.selectAll(\"path\")\r\n .data(pie(data))\r\n .join(\"path\")\r\n .attr(\"d\", arc)\r\n .attr(\"fill\", (d, i) => colourScale(i))\r\n .attr(\"stroke\", \"white\")\r\n .attr(\"stroke-width\", 2);\r\n```\r\n\r\n### Force-directed network\r\n\r\n```javascript\r\nconst simulation = d3.forceSimulation(nodes)\r\n .force(\"link\", d3.forceLink(links).id(d => d.id).distance(100))\r\n .force(\"charge\", d3.forceManyBody().strength(-300))\r\n .force(\"center\", d3.forceCenter(width / 2, height / 2));\r\n\r\nconst link = g.selectAll(\"line\")\r\n .data(links)\r\n .join(\"line\")\r\n .attr(\"stroke\", \"#999\")\r\n .attr(\"stroke-width\", 1);\r\n\r\nconst node = g.selectAll(\"circle\")\r\n .data(nodes)\r\n .join(\"circle\")\r\n .attr(\"r\", 8)\r\n .attr(\"fill\", \"steelblue\")\r\n .call(d3.drag()\r\n .on(\"start\", dragstarted)\r\n .on(\"drag\", dragged)\r\n .on(\"end\", dragended));\r\n\r\nsimulation.on(\"tick\", () => {\r\n link\r\n .attr(\"x1\", d => d.source.x)\r\n .attr(\"y1\", d => d.source.y)\r\n .attr(\"x2\", d => d.target.x)\r\n .attr(\"y2\", d => d.target.y);\r\n \r\n node\r\n .attr(\"cx\", d => d.x)\r\n .attr(\"cy\", d => d.y);\r\n});\r\n\r\nfunction dragstarted(event) {\r\n if (!event.active) simulation.alphaTarget(0.3).restart();\r\n event.subject.fx = event.subject.x;\r\n event.subject.fy = event.subject.y;\r\n}\r\n\r\nfunction dragged(event) {\r\n event.subject.fx = event.x;\r\n event.subject.fy = event.y;\r\n}\r\n\r\nfunction dragended(event) {\r\n if (!event.active) simulation.alphaTarget(0);\r\n event.subject.fx = null;\r\n event.subject.fy = null;\r\n}\r\n```",
"Resources": "### references/\r\nContains detailed reference materials:\r\n- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples\r\n- `scale-reference.md` - Complete guide to d3 scales with examples\r\n- `colour-schemes.md` - D3 colour schemes and palette recommendations\r\n\r\n### assets/\r\n\r\nContains boilerplate templates:\r\n\r\n- `chart-template.js` - Starter template for basic chart\r\n- `interactive-template.js` - Template with tooltips, zoom, and interactions\r\n- `sample-data.json` - Example datasets for testing\r\n\r\nThese templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.\r\n\r\nTo use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns."
},
"references": {
"files": [
"references/colour-schemes.md",
"references/d3-patterns.md",
"references/scale-reference.md"
]
},
"id": "claude-d3js-skill_chrisvoncsefalvay",
"name": "d3-viz",
"description": "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."
}---
name: d3-viz
description: 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.
---
# D3.js Visualisation
## Overview
This 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.
## When to use d3.js
**Use d3.js for:**
- Custom visualisations requiring unique visual encodings or layouts
- Interactive explorations with complex pan, zoom, or brush behaviours
- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)
- Geographic visualisations with custom projections
- Visualisations requiring smooth, choreographed transitions
- Publication-quality graphics with fine-grained styling control
- Novel chart types not available in standard libraries
**Consider alternatives for:**
- 3D visualisations - use Three.js instead
## Core workflow
### 1. Set up d3.js
Import d3 at the top of your script:
```javascript
import * as d3 from 'd3';
```
Or use the CDN version (7.x):
```html
<script src="https://d3js.org/d3.v7.min.js"></script>
```
All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace.
### 2. Choose the integration pattern
**Pattern A: Direct DOM manipulation (recommended for most cases)**
Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:
```javascript
function drawChart(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // Select by ID, class, or DOM element
// Clear previous content
svg.selectAll("*").remove();
// Set up dimensions
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
// Create scales, axes, and draw visualisation
// ... d3 code here ...
}
// Call when data changes
drawChart(myData);
```
**Pattern B: Declarative rendering (for frameworks with templating)**
Use d3 for data calculations (scales, layouts) but render elements via your framework:
```javascript
function getChartElements(data) {
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, 400]);
return data.map((d, i) => ({
x: 50,
y: i * 30,
width: xScale(d.value),
height: 25
}));
}
// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
// In Vue: v-for directive over the returned array
// In vanilla JS: Create elements manually from the returned data
```
Use 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.
### 3. Structure the visualisation code
Follow this standard structure in your drawing function:
```javascript
function drawVisualization(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // Or pass a selector/element
svg.selectAll("*").remove(); // Clear previous render
// 1. Define dimensions
const width = 800;
const 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;
// 2. Create main group with margins
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 3. Create 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]); // Note: inverted for SVG coordinates
// 4. Create and append axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
g.append("g")
.call(yAxis);
// 5. Bind data and create visual 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");
}
// Call when data changes
drawVisualization(myData);
```
### 4. Implement responsive sizing
Make visualisations responsive to container size:
```javascript
function setupResponsiveChart(containerId, data) {
const container = document.getElementById(containerId);
const svg = d3.select(`#${containerId}`).append('svg');
function updateChart() {
const { width, height } = container.getBoundingClientRect();
svg.attr('width', width).attr('height', height);
// Redraw visualisation with new dimensions
drawChart(data, svg, width, height);
}
// Update on initial load
updateChart();
// Update on window resize
window.addEventListener('resize', updateChart);
// Return cleanup function
return () => window.removeEventListener('resize', updateChart);
}
// Usage:
// const cleanup = setupResponsiveChart('chart-container', myData);
// cleanup(); // Call when component unmounts or element removed
```
Or use ResizeObserver for more direct container monitoring:
```javascript
function setupResponsiveChartWithObserver(svgElement, data) {
const observer = new ResizeObserver(() => {
const { width, height } = svgElement.getBoundingClientRect();
d3.select(svgElement)
.attr('width', width)
.attr('height', height);
// Redraw visualisation
drawChart(data, d3.select(svgElement), width, height);
});
observer.observe(svgElement.parentElement);
return () => observer.disconnect();
}
```
## Common visualisation patterns
### Bar chart
```javascript
function drawBarChart(data, svgElement) {
if (!data || data.length === 0) return;
const svg = d3.select(svgElement);
svg.selectAll("*").remove();
const width = 800;
const 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})`);
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.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
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");
}
// Usage:
// drawBarChart(myData, document.getElementById('chart'));
```
### Line chart
```javascript
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX); // Smooth curve
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
```
### Scatter plot
```javascript
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)) // Optional: size encoding
.attr("fill", d => colourScale(d.category)) // Optional: colour encoding
.attr("opacity", 0.7);
```
### Chord diagram
A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:
```javascript
function drawChordDiagram(data) {
// data format: array of objects with source, target, and value
// Example: [{ source: 'A', target: 'B', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 600;
const height = 600;
const innerRadius = Math.min(width, height) * 0.3;
const outerRadius = innerRadius + 30;
// Create matrix from data
const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));
data.forEach(d => {
const i = nodes.indexOf(d.source);
const j = nodes.indexOf(d.target);
matrix[i][j] += d.value;
matrix[j][i] += d.value;
});
// Create chord layout
const chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.source(d => d.source)
.target(d => d.target);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(nodes);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
const chords = chord(matrix);
// Draw ribbons
g.append("g")
.attr("fill-opacity", 0.67)
.selectAll("path")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => colourScale(nodes[d.source.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());
// Draw groups (arcs)
const group = g.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("d", arc)
.attr("fill", d => colourScale(nodes[d.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());
// Add labels
group.append("text")
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", "0.31em")
.attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
.text((d, i) => nodes[i])
.style("font-size", "12px");
}
```
### Heatmap
A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:
```javascript
function drawHeatmap(data) {
// data format: array of objects with row, column, and value
// Example: [{ row: 'A', column: 'X', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const margin = { top: 100, right: 30, bottom: 30, left: 100 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Get unique rows and columns
const rows = Array.from(new Set(data.map(d => d.row)));
const columns = Array.from(new Set(data.map(d => d.column)));
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Create scales
const xScale = d3.scaleBand()
.domain(columns)
.range([0, innerWidth])
.padding(0.01);
const yScale = d3.scaleBand()
.domain(rows)
.range([0, innerHeight])
.padding(0.01);
// Colour scale for values
const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
.domain([0, d3.max(data, d => d.value)]);
// Draw rectangles
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 => colourScale(d.value));
// Add x-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(columns)
.join("text")
.attr("x", d => xScale(d) + xScale.bandwidth() / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.text(d => d)
.style("font-size", "12px");
// Add y-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(rows)
.join("text")
.attr("x", -10)
.attr("y", d => yScale(d) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(d => d)
.style("font-size", "12px");
// Add colour legend
const legendWidth = 20;
const legendHeight = 200;
const legend = svg.append("g")
.attr("transform", `translate(${width - 60},${margin.top})`);
const legendScale = d3.scaleLinear()
.domain(colourScale.domain())
.range([legendHeight, 0]);
const legendAxis = d3.axisRight(legendScale)
.ticks(5);
// Draw colour gradient in legend
for (let i = 0; i < legendHeight; i++) {
legend.append("rect")
.attr("y", i)
.attr("width", legendWidth)
.attr("height", 1)
.attr("fill", colourScale(legendScale.invert(i)));
}
legend.append("g")
.attr("transform", `translate(${legendWidth},0)`)
.call(legendAxis);
}
```
### 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 - 20);
const colourScale = 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) => colourScale(i))
.attr("stroke", "white")
.attr("stroke-width", 2);
```
### Force-directed network
```javascript
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")
.attr("stroke-width", 1);
const node = g.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 8)
.attr("fill", "steelblue")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
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);
});
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
```
## Adding interactivity
### Tooltips
```javascript
// Create tooltip div (outside SVG)
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "1px solid #ddd")
.style("padding", "10px")
.style("border-radius", "4px")
.style("pointer-events", "none");
// Add to elements
circles
.on("mouseover", function(event, d) {
d3.select(this).attr("opacity", 1);
tooltip
.style("visibility", "visible")
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
})
.on("mousemove", function(event) {
tooltip
.style("top", (event.pageY - 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", function() {
d3.select(this).attr("opacity", 0.7);
tooltip.style("visibility", "hidden");
});
```
### Zoom and pan
```javascript
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
```
### Click interactions
```javascript
circles
.on("click", function(event, d) {
// Handle click (dispatch event, update app state, etc.)
console.log("Clicked:", d);
// Visual feedback
d3.selectAll("circle").attr("fill", "steelblue");
d3.select(this).attr("fill", "orange");
// Optional: dispatch custom event for your framework/app to listen to
// window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
});
```
## Transitions and animations
Add smooth transitions to visual changes:
```javascript
// Basic transition
circles
.transition()
.duration(750)
.attr("r", 10);
// Chained transitions
circles
.transition()
.duration(500)
.attr("fill", "orange")
.transition()
.duration(500)
.attr("r", 15);
// Staggered transitions
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);
```
## Scales reference
### Quantitative scales
```javascript
// Linear scale
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
// Log scale (for exponential data)
const logScale = d3.scaleLog()
.domain([1, 1000])
.range([0, 500]);
// Power scale
const powScale = d3.scalePow()
.exponent(2)
.domain([0, 100])
.range([0, 500]);
// Time scale
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
.range([0, 500]);
```
### Ordinal scales
```javascript
// Band scale (for bar charts)
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.1);
// Point scale (for line/scatter categories)
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400]);
// Ordinal scale (for colours)
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
```
### Sequential scales
```javascript
// Sequential colour scale
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
// Diverging colour scale
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);
```
## Best practices
### Data preparation
Always validate and prepare data before visualisation:
```javascript
// Filter invalid values
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));
// Sort data if order matters
const sortedData = [...data].sort((a, b) => b.value - a.value);
// Parse dates
const parsedData = data.map(d => ({
...d,
date: d3.timeParse("%Y-%m-%d")(d.date)
}));
```
### Performance optimisation
For large datasets (>1000 elements):
```javascript
// Use canvas instead of SVG for many elements
// Use quadtree for collision detection
// Simplify paths with d3.line().curve(d3.curveStep)
// Implement virtual scrolling for large lists
// Use requestAnimationFrame for custom animations
```
### Accessibility
Make visualisations accessible:
```javascript
// Add ARIA labels
svg.attr("role", "img")
.attr("aria-label", "Bar chart showing quarterly revenue");
// Add title and description
svg.append("title").text("Quarterly Revenue 2024");
svg.append("desc").text("Bar chart showing revenue growth across four quarters");
// Ensure sufficient colour contrast
// Provide keyboard navigation for interactive elements
// Include data table alternative
```
### Styling
Use consistent, professional styling:
```javascript
// Define colour palettes upfront
const colours = {
primary: '#4A90E2',
secondary: '#7B68EE',
background: '#F5F7FA',
text: '#333333',
gridLines: '#E0E0E0'
};
// Apply consistent typography
svg.selectAll("text")
.style("font-family", "Inter, sans-serif")
.style("font-size", "12px");
// Use subtle grid lines
g.selectAll(".tick line")
.attr("stroke", colours.gridLines)
.attr("stroke-dasharray", "2,2");
```
## Common issues and solutions
**Issue**: Axes not appearing
- Ensure scales have valid domains (check for NaN values)
- Verify axis is appended to correct group
- Check transform translations are correct
**Issue**: Transitions not working
- Call `.transition()` before attribute changes
- Ensure elements have unique keys for proper data binding
- Check that useEffect dependencies include all changing data
**Issue**: Responsive sizing not working
- Use ResizeObserver or window resize listener
- Update dimensions in state to trigger re-render
- Ensure SVG has width/height attributes or viewBox
**Issue**: Performance problems
- Limit number of DOM elements (consider canvas for >1000 items)
- Debounce resize handlers
- Use `.join()` instead of separate enter/update/exit selections
- Avoid unnecessary re-renders by checking dependencies
## Resources
### references/
Contains detailed reference materials:
- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples
- `scale-reference.md` - Complete guide to d3 scales with examples
- `colour-schemes.md` - D3 colour schemes and palette recommendations
### assets/
Contains boilerplate templates:
- `chart-template.js` - Starter template for basic chart
- `interactive-template.js` - Template with tooltips, zoom, and interactions
- `sample-data.json` - Example datasets for testing
These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.
To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.