skills/besoeasy/open-skills/d3js-data-visualization

d3js-data-visualization

SKILL.md

D3.js Data Visualization

Build sophisticated, interactive data visualizations using d3.js (Data-Driven Documents). D3 binds data to DOM elements and applies data-driven transformations to produce publication-quality, fully customizable visuals.

When to Use This Skill

  • Custom charts requiring unique visual encodings or layouts
  • Interactive visualizations with pan, zoom, or brush behaviors
  • Network/graph visualizations (force-directed, tree, hierarchy, chord diagrams)
  • Geographic visualizations with custom projections
  • Smooth, choreographed transitions and animations
  • Novel chart types not available in standard libraries (Recharts, Chart.js, etc.)
  • Fine-grained SVG styling and accessibility control

Consider alternatives for:

  • 3D visualizations → use Three.js
  • Simple standard charts with minimal customization → use Chart.js or Recharts

Required Tools / Libraries

No backend required. Runs entirely in the browser or Node.js (with jsdom/canvas).

# Install via npm
npm install d3

# Or use CDN in HTML
<script src="https://d3js.org/d3.v7.min.js"></script>

Core Workflow

1. Set Up D3

import * as d3 from 'd3';

2. Standard Chart Structure

Every d3 visualization follows this pattern:

function drawChart(data) {
  if (!data || data.length === 0) return;

  const svg = d3.select('#chart');
  svg.selectAll("*").remove(); // clear previous render

  const width = 800, 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})`);

  // Define 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]);

  // Axes
  g.append("g").attr("transform", `translate(0,${innerHeight})`).call(d3.axisBottom(xScale));
  g.append("g").call(d3.axisLeft(yScale));

  // Data 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");
}

Common Chart Patterns

Bar Chart

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.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");

Line Chart

const line = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.value))
  .curve(d3.curveMonotoneX);

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))
  .attr("fill", d => colorScale(d.category))
  .attr("opacity", 0.7);

Pie / Donut 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 colorScale = 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) => colorScale(i))
  .attr("stroke", "white")
  .attr("stroke-width", 2);

Force-Directed Network Graph

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");
const node = g.selectAll("circle").data(nodes).join("circle")
  .attr("r", 8).attr("fill", "steelblue")
  .call(d3.drag()
    .on("start", (e) => { if (!e.active) simulation.alphaTarget(0.3).restart(); e.subject.fx = e.subject.x; e.subject.fy = e.subject.y; })
    .on("drag",  (e) => { e.subject.fx = e.x; e.subject.fy = e.y; })
    .on("end",   (e) => { if (!e.active) simulation.alphaTarget(0); e.subject.fx = null; e.subject.fy = null; }));

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);
});

Heatmap

// data: [{ row, column, value }, ...]
const rows = [...new Set(data.map(d => d.row))];
const cols = [...new Set(data.map(d => d.column))];

const xScale = d3.scaleBand().domain(cols).range([0, innerWidth]).padding(0.01);
const yScale = d3.scaleBand().domain(rows).range([0, innerHeight]).padding(0.01);
const colorScale = d3.scaleSequential(d3.interpolateYlOrRd).domain([0, d3.max(data, d => d.value)]);

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 => colorScale(d.value));

Interactivity

Tooltips

const tooltip = d3.select("body").append("div")
  .style("position", "absolute")
  .style("visibility", "hidden")
  .style("background", "white")
  .style("border", "1px solid #ddd")
  .style("padding", "10px")
  .style("border-radius", "4px")
  .style("pointer-events", "none");

elements
  .on("mouseover", (event, d) => tooltip.style("visibility", "visible").html(`<strong>${d.label}</strong><br/>Value: ${d.value}`))
  .on("mousemove", (event)    => tooltip.style("top", (event.pageY - 10) + "px").style("left", (event.pageX + 10) + "px"))
  .on("mouseout",  ()         => 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);

Transitions & Animations

// Basic
circles.transition().duration(750).attr("r", 10);

// Staggered
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);

Responsive Sizing

function setupResponsiveChart(containerId, data) {
  const container = document.getElementById(containerId);
  const svg = d3.select(`#${containerId}`).append('svg');

  const updateChart = () => {
    const { width, height } = container.getBoundingClientRect();
    svg.attr('width', width).attr('height', height);
    drawChart(data, svg, width, height);
  };

  updateChart();
  window.addEventListener('resize', updateChart);
  return () => window.removeEventListener('resize', updateChart);
}

Scale Reference

Scale Use case
d3.scaleLinear() Continuous numeric data
d3.scaleLog() Exponential/logarithmic data
d3.scaleTime() Date/time axes
d3.scaleBand() Bar chart categories
d3.scaleOrdinal() Categorical colors
d3.scaleSequential() Single-hue color gradients
d3.scaleDiverging() Diverging color scales

Best Practices

  • Always validate data: filter nulls and NaN before binding
  • Clear previous render with svg.selectAll("*").remove() before redrawing
  • Use .join() (enter/update/exit in one call) instead of separate selections
  • Add ARIA labels (role="img", aria-label) for accessibility
  • For >1000 elements, consider Canvas rendering instead of SVG
  • Debounce resize handlers to avoid excessive redraws
  • Define color palettes upfront for visual consistency

Troubleshooting

Problem Solution
Axes not appearing Check for NaN in scale domain; verify group transform
Transitions not working Call .transition() before attribute changes
Responsive sizing broken Use ResizeObserver or update SVG width/height on resize
Performance issues Switch to Canvas, debounce resize, use .join()

Related Skills

  • generate-asset-price-chart — OHLC candlestick chart generation
  • trading-indicators-from-price-data — Compute indicators to feed into charts
  • free-geocoding-and-maps — Geographic data for map visualizations
Weekly Installs
7
GitHub Stars
87
First Seen
13 days ago
Installed on
kimi-cli7
github-copilot6
codex6
gemini-cli6
cursor6
amp6