# D3.js Visualisation - Detailed Guide > This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations. ## 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. ## Input and lifecycle contract Treat sample-data.json as fictional demonstration data, including city values; it is not a sourced population dataset. Validate finite numeric values, unique keys and parsed dates. Sort time-series by date before lines/areas; reject empty or degenerate domains and define missing-value treatment. Nonnegative bar/stack snippets need adaptation for signed values. Use text nodes for untrusted labels, never interpolate them into HTML. Cleanup zoom listeners, tooltips, transitions, ResizeObservers and force simulations; clone nodes/links before force mutation. Honor reduced-motion settings, provide keyboard controls and underlying data. Responsive examples need a draw function accepting the supplied SVG/dimensions; the earlier one-argument drawChart sketch does not implement that contract. ## 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 ``` 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) => )} // 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; if (i !== j) matrix[j][i] += d.value; // This example assumes undirected flows }); // 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) .radius(innerRadius); 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: 100, 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") .text(`${d.label}: ${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 circles.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 => Number.isFinite(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 // Downsample with an explicit error budget; curveStep changes interpolation, not point count // 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.jsx` - Starter template for basic chart - `interactive-template.jsx` - Template with tooltips, zoom, and interactions - `sample-data.json` - Example datasets for testing The assets are React JSX components requiring React, D3 and a JSX build pipeline; they do not run unchanged as vanilla JS or Vue/Svelte components. To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.