D3 viz skill

Creating interactive data visualisations using d3.js.

by davila7·MIT license·★ 32,299 Stars on the repo·GitHub ↗

Use now

Files of D3 viz

davila7/main1 file shown
SKILL.md
Show the full text821 lines

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 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:

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 removed

Or 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 animations
Accessibility

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 alternative
Styling

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 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.

1---
2name: d3-viz
3description: 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---
5 
6# D3.js Visualisation
7 
8## Overview
9 
10This 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.
11 
12## When to use d3.js
13 
14**Use d3.js for:**
15- Custom visualisations requiring unique visual encodings or layouts
16- Interactive explorations with complex pan, zoom, or brush behaviours
17- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)
18- Geographic visualisations with custom projections
19- Visualisations requiring smooth, choreographed transitions
20- Publication-quality graphics with fine-grained styling control
21- Novel chart types not available in standard libraries
22 
23**Consider alternatives for:**
24- 3D visualisations - use Three.js instead
25 
26## Core workflow
27 
28### 1. Set up d3.js
29 
30Import d3 at the top of your script:
31 
32```javascript
33import * as d3 from 'd3';
34```
35 
36Or use the CDN version (7.x):
37 
38```html
39<script src="https://d3js.org/d3.v7.min.js"></script>
40```
41 
42All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace.
43 
44### 2. Choose the integration pattern
45 
46**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:
48 
49```javascript
50function drawChart(data) {
51 if (!data || data.length === 0) return;
52 
53 const svg = d3.select('#chart'); // Select by ID, class, or DOM element
54 
55 // Clear previous content
56 svg.selectAll("*").remove();
57 
58 // Set up dimensions
59 const width = 800;
60 const height = 400;
61 const margin = { top: 20, right: 30, bottom: 40, left: 50 };
62 
63 // Create scales, axes, and draw visualisation
64 // ... d3 code here ...
65}
66 
67// Call when data changes
68drawChart(myData);
69```
70 
71**Pattern B: Declarative rendering (for frameworks with templating)**
72Use d3 for data calculations (scales, layouts) but render elements via your framework:
73 
74```javascript
75function getChartElements(data) {
76 const xScale = d3.scaleLinear()
77 .domain([0, d3.max(data, d => d.value)])
78 .range([0, 400]);
79 
80 return data.map((d, i) => ({
81 x: 50,
82 y: i * 30,
83 width: xScale(d.value),
84 height: 25
85 }));
86}
87 
88// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
89// In Vue: v-for directive over the returned array
90// In vanilla JS: Create elements manually from the returned data
91```
92 
93Use 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.
94 
95### 3. Structure the visualisation code
96 
97Follow this standard structure in your drawing function:
98 
99```javascript
100function drawVisualization(data) {
101 if (!data || data.length === 0) return;
102 
103 const svg = d3.select('#chart'); // Or pass a selector/element
104 svg.selectAll("*").remove(); // Clear previous render
105 
106 // 1. Define dimensions
107 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;
112 
113 // 2. Create main group with margins
114 const g = svg.append("g")
115 .attr("transform", `translate(${margin.left},${margin.top})`);
116 
117 // 3. Create scales
118 const xScale = d3.scaleLinear()
119 .domain([0, d3.max(data, d => d.x)])
120 .range([0, innerWidth]);
121 
122 const yScale = d3.scaleLinear()
123 .domain([0, d3.max(data, d => d.y)])
124 .range([innerHeight, 0]); // Note: inverted for SVG coordinates
125 
126 // 4. Create and append axes
127 const xAxis = d3.axisBottom(xScale);
128 const yAxis = d3.axisLeft(yScale);
129 
130 g.append("g")
131 .attr("transform", `translate(0,${innerHeight})`)
132 .call(xAxis);
133 
134 g.append("g")
135 .call(yAxis);
136 
137 // 5. Bind data and create visual elements
138 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}
146 
147// Call when data changes
148drawVisualization(myData);
149```
150 
151### 4. Implement responsive sizing
152 
153Make visualisations responsive to container size:
154 
155```javascript
156function setupResponsiveChart(containerId, data) {
157 const container = document.getElementById(containerId);
158 const svg = d3.select(`#${containerId}`).append('svg');
159 
160 function updateChart() {
161 const { width, height } = container.getBoundingClientRect();
162 svg.attr('width', width).attr('height', height);
163 
164 // Redraw visualisation with new dimensions
165 drawChart(data, svg, width, height);
166 }
167 
168 // Update on initial load
169 updateChart();
170 
171 // Update on window resize
172 window.addEventListener('resize', updateChart);
173 
174 // Return cleanup function
175 return () => window.removeEventListener('resize', updateChart);
176}
177 
178// Usage:
179// const cleanup = setupResponsiveChart('chart-container', myData);
180// cleanup(); // Call when component unmounts or element removed
181```
182 
183Or use ResizeObserver for more direct container monitoring:
184 
185```javascript
186function 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);
192 
193 // Redraw visualisation
194 drawChart(data, d3.select(svgElement), width, height);
195 });
196 
197 observer.observe(svgElement.parentElement);
198 return () => observer.disconnect();
199}
200```
201 
202## Common visualisation patterns
203 
204### Bar chart
205 
206```javascript
207function drawBarChart(data, svgElement) {
208 if (!data || data.length === 0) return;
209 
210 const svg = d3.select(svgElement);
211 svg.selectAll("*").remove();
212 
213 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;
218 
219 const g = svg.append("g")
220 .attr("transform", `translate(${margin.left},${margin.top})`);
221 
222 const xScale = d3.scaleBand()
223 .domain(data.map(d => d.category))
224 .range([0, innerWidth])
225 .padding(0.1);
226 
227 const yScale = d3.scaleLinear()
228 .domain([0, d3.max(data, d => d.value)])
229 .range([innerHeight, 0]);
230 
231 g.append("g")
232 .attr("transform", `translate(0,${innerHeight})`)
233 .call(d3.axisBottom(xScale));
234 
235 g.append("g")
236 .call(d3.axisLeft(yScale));
237 
238 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}
247 
248// Usage:
249// drawBarChart(myData, document.getElementById('chart'));
250```
251 
252### Line chart
253 
254```javascript
255const line = d3.line()
256 .x(d => xScale(d.date))
257 .y(d => yScale(d.value))
258 .curve(d3.curveMonotoneX); // Smooth curve
259 
260g.append("path")
261 .datum(data)
262 .attr("fill", "none")
263 .attr("stroke", "steelblue")
264 .attr("stroke-width", 2)
265 .attr("d", line);
266```
267 
268### Scatter plot
269 
270```javascript
271g.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 encoding
277 .attr("fill", d => colourScale(d.category)) // Optional: colour encoding
278 .attr("opacity", 0.7);
279```
280 
281### Chord diagram
282 
283A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:
284 
285```javascript
286function drawChordDiagram(data) {
287 // data format: array of objects with source, target, and value
288 // Example: [{ source: 'A', target: 'B', value: 10 }, ...]
289 
290 if (!data || data.length === 0) return;
291 
292 const svg = d3.select('#chart');
293 svg.selectAll("*").remove();
294 
295 const width = 600;
296 const height = 600;
297 const innerRadius = Math.min(width, height) * 0.3;
298 const outerRadius = innerRadius + 30;
299 
300 // Create matrix from data
301 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));
303 
304 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 });
310 
311 // Create chord layout
312 const chord = d3.chord()
313 .padAngle(0.05)
314 .sortSubgroups(d3.descending);
315 
316 const arc = d3.arc()
317 .innerRadius(innerRadius)
318 .outerRadius(outerRadius);
319 
320 const ribbon = d3.ribbon()
321 .source(d => d.source)
322 .target(d => d.target);
323 
324 const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
325 .domain(nodes);
326 
327 const g = svg.append("g")
328 .attr("transform", `translate(${width / 2},${height / 2})`);
329 
330 const chords = chord(matrix);
331 
332 // Draw ribbons
333 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());
341 
342 // Draw groups (arcs)
343 const group = g.append("g")
344 .selectAll("g")
345 .data(chords.groups)
346 .join("g");
347 
348 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());
352 
353 // Add labels
354 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```
363 
364### Heatmap
365 
366A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:
367 
368```javascript
369function drawHeatmap(data) {
370 // data format: array of objects with row, column, and value
371 // Example: [{ row: 'A', column: 'X', value: 10 }, ...]
372 
373 if (!data || data.length === 0) return;
374 
375 const svg = d3.select('#chart');
376 svg.selectAll("*").remove();
377 
378 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;
383 
384 // Get unique rows and columns
385 const rows = Array.from(new Set(data.map(d => d.row)));
386 const columns = Array.from(new Set(data.map(d => d.column)));
387 
388 const g = svg.append("g")
389 .attr("transform", `translate(${margin.left},${margin.top})`);
390 
391 // Create scales
392 const xScale = d3.scaleBand()
393 .domain(columns)
394 .range([0, innerWidth])
395 .padding(0.01);
396 
397 const yScale = d3.scaleBand()
398 .domain(rows)
399 .range([0, innerHeight])
400 .padding(0.01);
401 
402 // Colour scale for values
403 const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
404 .domain([0, d3.max(data, d => d.value)]);
405 
406 // Draw rectangles
407 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));
415 
416 // Add x-axis labels
417 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");
427 
428 // Add y-axis labels
429 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");
440 
441 // Add colour legend
442 const legendWidth = 20;
443 const legendHeight = 200;
444 const legend = svg.append("g")
445 .attr("transform", `translate(${width - 60},${margin.top})`);
446 
447 const legendScale = d3.scaleLinear()
448 .domain(colourScale.domain())
449 .range([legendHeight, 0]);
450 
451 const legendAxis = d3.axisRight(legendScale)
452 .ticks(5);
453 
454 // Draw colour gradient in legend
455 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 }
462 
463 legend.append("g")
464 .attr("transform", `translate(${legendWidth},0)`)
465 .call(legendAxis);
466}
467```
468 
469### Pie chart
470 
471```javascript
472const pie = d3.pie()
473 .value(d => d.value)
474 .sort(null);
475 
476const arc = d3.arc()
477 .innerRadius(0)
478 .outerRadius(Math.min(width, height) / 2 - 20);
479 
480const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
481 
482const g = svg.append("g")
483 .attr("transform", `translate(${width / 2},${height / 2})`);
484 
485g.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```
493 
494### Force-directed network
495 
496```javascript
497const 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));
501 
502const link = g.selectAll("line")
503 .data(links)
504 .join("line")
505 .attr("stroke", "#999")
506 .attr("stroke-width", 1);
507 
508const 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));
517 
518simulation.on("tick", () => {
519 link
520 .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);
524 
525 node
526 .attr("cx", d => d.x)
527 .attr("cy", d => d.y);
528});
529 
530function 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}
535 
536function dragged(event) {
537 event.subject.fx = event.x;
538 event.subject.fy = event.y;
539}
540 
541function dragended(event) {
542 if (!event.active) simulation.alphaTarget(0);
543 event.subject.fx = null;
544 event.subject.fy = null;
545}
546```
547 
548## Adding interactivity
549 
550### Tooltips
551 
552```javascript
553// 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");
563 
564// Add to elements
565circles
566 .on("mouseover", function(event, d) {
567 d3.select(this).attr("opacity", 1);
568 tooltip
569 .style("visibility", "visible")
570 .html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
571 })
572 .on("mousemove", function(event) {
573 tooltip
574 .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```
582 
583### Zoom and pan
584 
585```javascript
586const zoom = d3.zoom()
587 .scaleExtent([0.5, 10])
588 .on("zoom", (event) => {
589 g.attr("transform", event.transform);
590 });
591 
592svg.call(zoom);
593```
594 
595### Click interactions
596 
597```javascript
598circles
599 .on("click", function(event, d) {
600 // Handle click (dispatch event, update app state, etc.)
601 console.log("Clicked:", d);
602 
603 // Visual feedback
604 d3.selectAll("circle").attr("fill", "steelblue");
605 d3.select(this).attr("fill", "orange");
606 
607 // Optional: dispatch custom event for your framework/app to listen to
608 // window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
609 });
610```
611 
612## Transitions and animations
613 
614Add smooth transitions to visual changes:
615 
616```javascript
617// Basic transition
618circles
619 .transition()
620 .duration(750)
621 .attr("r", 10);
622 
623// Chained transitions
624circles
625 .transition()
626 .duration(500)
627 .attr("fill", "orange")
628 .transition()
629 .duration(500)
630 .attr("r", 15);
631 
632// Staggered transitions
633circles
634 .transition()
635 .delay((d, i) => i * 50)
636 .duration(500)
637 .attr("cy", d => yScale(d.value));
638 
639// Custom easing
640circles
641 .transition()
642 .duration(1000)
643 .ease(d3.easeBounceOut)
644 .attr("r", 10);
645```
646 
647## Scales reference
648 
649### Quantitative scales
650 
651```javascript
652// Linear scale
653const xScale = d3.scaleLinear()
654 .domain([0, 100])
655 .range([0, 500]);
656 
657// Log scale (for exponential data)
658const logScale = d3.scaleLog()
659 .domain([1, 1000])
660 .range([0, 500]);
661 
662// Power scale
663const powScale = d3.scalePow()
664 .exponent(2)
665 .domain([0, 100])
666 .range([0, 500]);
667 
668// Time scale
669const timeScale = d3.scaleTime()
670 .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
671 .range([0, 500]);
672```
673 
674### Ordinal scales
675 
676```javascript
677// Band scale (for bar charts)
678const bandScale = d3.scaleBand()
679 .domain(['A', 'B', 'C', 'D'])
680 .range([0, 400])
681 .padding(0.1);
682 
683// Point scale (for line/scatter categories)
684const pointScale = d3.scalePoint()
685 .domain(['A', 'B', 'C', 'D'])
686 .range([0, 400]);
687 
688// Ordinal scale (for colours)
689const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
690```
691 
692### Sequential scales
693 
694```javascript
695// Sequential colour scale
696const colourScale = d3.scaleSequential(d3.interpolateBlues)
697 .domain([0, 100]);
698 
699// Diverging colour scale
700const divScale = d3.scaleDiverging(d3.interpolateRdBu)
701 .domain([-10, 0, 10]);
702```
703 
704## Best practices
705 
706### Data preparation
707 
708Always validate and prepare data before visualisation:
709 
710```javascript
711// Filter invalid values
712const cleanData = data.filter(d => d.value != null && !isNaN(d.value));
713 
714// Sort data if order matters
715const sortedData = [...data].sort((a, b) => b.value - a.value);
716 
717// Parse dates
718const parsedData = data.map(d => ({
719 ...d,
720 date: d3.timeParse("%Y-%m-%d")(d.date)
721}));
722```
723 
724### Performance optimisation
725 
726For large datasets (>1000 elements):
727 
728```javascript
729// Use canvas instead of SVG for many elements
730// Use quadtree for collision detection
731// Simplify paths with d3.line().curve(d3.curveStep)
732// Implement virtual scrolling for large lists
733// Use requestAnimationFrame for custom animations
734```
735 
736### Accessibility
737 
738Make visualisations accessible:
739 
740```javascript
741// Add ARIA labels
742svg.attr("role", "img")
743 .attr("aria-label", "Bar chart showing quarterly revenue");
744 
745// Add title and description
746svg.append("title").text("Quarterly Revenue 2024");
747svg.append("desc").text("Bar chart showing revenue growth across four quarters");
748 
749// Ensure sufficient colour contrast
750// Provide keyboard navigation for interactive elements
751// Include data table alternative
752```
753 
754### Styling
755 
756Use consistent, professional styling:
757 
758```javascript
759// Define colour palettes upfront
760const colours = {
761 primary: '#4A90E2',
762 secondary: '#7B68EE',
763 background: '#F5F7FA',
764 text: '#333333',
765 gridLines: '#E0E0E0'
766};
767 
768// Apply consistent typography
769svg.selectAll("text")
770 .style("font-family", "Inter, sans-serif")
771 .style("font-size", "12px");
772 
773// Use subtle grid lines
774g.selectAll(".tick line")
775 .attr("stroke", colours.gridLines)
776 .attr("stroke-dasharray", "2,2");
777```
778 
779## Common issues and solutions
780 
781**Issue**: Axes not appearing
782- Ensure scales have valid domains (check for NaN values)
783- Verify axis is appended to correct group
784- Check transform translations are correct
785 
786**Issue**: Transitions not working
787- Call `.transition()` before attribute changes
788- Ensure elements have unique keys for proper data binding
789- Check that useEffect dependencies include all changing data
790 
791**Issue**: Responsive sizing not working
792- Use ResizeObserver or window resize listener
793- Update dimensions in state to trigger re-render
794- Ensure SVG has width/height attributes or viewBox
795 
796**Issue**: Performance problems
797- Limit number of DOM elements (consider canvas for >1000 items)
798- Debounce resize handlers
799- Use `.join()` instead of separate enter/update/exit selections
800- Avoid unnecessary re-renders by checking dependencies
801 
802## Resources
803 
804### references/
805Contains detailed reference materials:
806- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples
807- `scale-reference.md` - Complete guide to d3 scales with examples
808- `colour-schemes.md` - D3 colour schemes and palette recommendations
809 
810### assets/
811 
812Contains boilerplate templates:
813 
814- `chart-template.js` - Starter template for basic chart
815- `interactive-template.js` - Template with tooltips, zoom, and interactions
816- `sample-data.json` - Example datasets for testing
817 
818These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.
819 
820To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.
821 

Discussion