agentsclimarketplace

D3 force simulation

Skill cxcscmu/SkillLearnBench/skills/b1-one-shot-claude-haiku-4-5/stock-data-visualization/d3-force-simulation

D3.js force simulation for creating interactive bubble charts with clustering, collision detection, and physics-based positioningFrom its SKILL.md

Install
npx -y skills add cxcscmu/SkillLearnBench --skill d3-force-simulation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

4.3 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it

D3.js Force Simulation

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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.