claude-d3js-skill

This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js.

INSTALLATION
npx skills add https://github.com/sickn33/antigravity-awesome-skills --skill claude-d3js-skill
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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 &#x26;&#x26; !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.

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card