D3.js Force Simulation

SkillDev tools

D3.js force simulation for creating interactive bubble charts with clustering, collision detection, and physics-based positioning

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the D3.js Force Simulation skill

What this skill tells your AI

The instructions your AI receives, as published by cxcscmu/skilllearnbench in skills/b1-one-shot-claude-haiku-4-5/stock-data-visualization/d3-force-simulation/SKILL.md and read by ahel’s review.

Overview

Force simulation in D3 uses physics-based algorithms to position nodes (bubbles) based on forces. Perfect for creating bubble charts with natural-looking clusters.

Core Concepts

1. Creating a Simulation

const simulation = d3.forceSimulation(nodes)
  .force("name", forceFunction)
  .on("tick", updatePositions);

// Start simulation (runs in background)
simulation.alpha(1);  // reset energy

2. Common Forces

forceX / forceY

Position bubbles toward target x/y coordinates (creates clustering):

// Group by sector - each sector gets target x position
const sectors = Array.from(new Set(data.map(d => d.sector)));
const sectorX = d3.scalePoint()
  .domain(sectors)
  .range([0, width]);

const simulation = d3.forceSimulation(nodes)
  .force("x", d3.forceX()
    .x(d => sectorX(d.sector))  // each sector pulled to its x position
    .strength(0.05)              // weak force (allows spreading)
  )
  .force("y", d3.forceY()
    .y(height / 2)               // all centered vertically
    .strength(0.03)
  );
forceCollide

Prevents bubbles from overlapping:

.force("collide", d3.forceCollide()
  .radius(d => radiusScale(d.marketCap) + 2)  // add padding
  .strength(0.5)  // collision strength (0-1)
)
forceManyBody

Repulsive or attractive force between all nodes:

.force("charge", d3.forceManyBody()
  .strength(-50)  // negative = repulsion, positive = attraction
)

3. Tick Events

Update positions on each simulation frame:

simulation.on("tick", () => {
  // Update circles
  circles
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);

  // Update text
  labels
    .attr("x", d => d.x)
    .attr("y", d => d.y);
});

4. Preventing Bubbles from Moving Off-Screen

Fix nodes to boundaries:

simulation.on("tick", () => {
  nodes.forEach(d => {
    // Clamp positions within bounds
    d.x = Math.max(d.radius, Math.min(width - d.radius, d.x));
    d.y = Math.max(d.radius, Math.min(height - d.radius, d.y));
  });

  // Update positions
  circles.attr("cx", d => d.x).attr("cy", d => d.y);
});

5. Interactive Dragging

Allow users to drag nodes:

function drag(simulation) {
  return d3.drag()
    .on("start", (event, d) => {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    })
    .on("drag", (event, d) => {
      d.fx = event.x;
      d.fy = event.y;
    })
    .on("end", (event, d) => {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    });
}

circles.call(drag(simulation));

Complete Example: Bubble Cluster Chart

// Setup
const nodes = data.map(d => ({
  id: d.ticker,
  sector: d.sector,
  value: d.marketCap,
  radius: Math.sqrt(d.marketCap / Math.PI)
}));

const width = 800, height = 600;

// Scales
const sectorX = d3.scalePoint()
  .domain(Array.from(new Set(data.map(d => d.sector))))
  .range([100, width - 100]);

const radiusScale = d3.scaleSqrt()
  .domain([0, d3.max(nodes, d => d.value)])
  .range([5, 50]);

const colorScale = d3.scaleOrdinal()
  .domain(Array.from(new Set(data.map(d => d.sector))))
  .range(d3.schemeCategory10);

// Simulation
const simulation = d3.forceSimulation(nodes)
  .force("x", d3.forceX().x(d => sectorX(d.sector)).strength(0.05))
  .force("y", d3.forceY().y(height / 2).strength(0.03))
  .force("collide", d3.forceCollide().radius(d => radiusScale(d.value) + 2))
  .force("charge", d3.forceManyBody().strength(-30))
  .on("tick", () => {
    circles
      .attr("cx", d => d.x)
      .attr("cy", d => d.y);
  });

// Render
const svg = d3.select("body").append("svg")
  .attr("width", width).attr("height", height);

const circles = svg.selectAll("circle")
  .data(nodes)
  .join("circle")
  .attr("r", d => radiusScale(d.value))
  .attr("fill", d => colorScale(d.sector));

Performance Tips

  • Use simulation.alphaMin(0.001) to stop simulation faster
  • Reduce strength values for smoother settling
  • Use radius in forceCollide matching actual visual radius
  • Call simulation.stop() when not needed to save CPU

Signals

GitHub stars
83
Forks
5
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
d3-force-simulation
Source
github.com/cxcscmu/skilllearnbench