agentsclimarketplace

Run3 build d3 bubble chart with force simulation

Skill cxcscmu/SkillLearnBench/skills/b3-teacher-feedback-claude-sonnet-4-6/stock-data-visualization/run3_build-d3-bubble-chart-with-force-simulation

Use when building a D3.js v6 bubble chart with force simulation, sector clustering, collision detection, tooltips, and interactive highlighting. Handles ETFs (no marketCap/sector) as a separate cluster.From its SKILL.md

Install
npx -y skills add cxcscmu/SkillLearnBench --skill run3_build-d3-bubble-chart-with-force-simulation

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

SKILL.md

3.0 KB, 828 tokens by cl100k_base, as published. Nobody here has run it

D3 v6 Bubble Chart with Force Simulation

Key Requirements

  • Bubbles sized by marketCap (ETFs get uniform size)
  • Colored by sector; ETFs assigned sector 'ETF' as fallback
  • Force simulation with forceX/forceY pulling toward cluster centers
  • forceCollide prevents overlap
  • Pre-tick simulation with .stop() + .tick(N) for static initial render
  • Tooltip on hover for non-ETF stocks only (detected by !d.marketCap)
  • Click interaction highlights corresponding table row

ETF Handling Pattern

// In data loading — assign fallback sector BEFORE building clusterCenters
data.forEach(d => {
  if (!d.sector || d.sector.trim() === '') {
    d.sector = 'ETF';
  }
  // Parse marketCap as number if present
  d.marketCapNum = d.marketCap ? parseFloat(d.marketCap) : null;
});

Cluster Centers Pattern

const sectors = [...new Set(data.map(d => d.sector))];
const numSectors = sectors.length;
// Arrange cluster centers in a grid or circle
const clusterCenters = {};
sectors.forEach((sector, i) => {
  const angle = (i / numSectors) * 2 * Math.PI;
  clusterCenters[sector] = {
    x: innerW / 2 + (innerW * 0.35) * Math.cos(angle),
    y: innerH / 2 + (innerH * 0.35) * Math.sin(angle)
  };
});

Force Simulation Pattern

const simulation = d3.forceSimulation(data)
  .force('x', d3.forceX(d => clusterCenters[d.sector].x).strength(0.15))
  .force('y', d3.forceY(d => clusterCenters[d.sector].y).strength(0.15))
  .force('collide', d3.forceCollide(d => radiusScale(d.marketCapNum || minBubbleR) + 2))
  .stop();

// Pre-tick for static render
for (let i = 0; i < 300; i++) simulation.tick();

// Validate positions
data.forEach(d => {
  if (isNaN(d.x) || isNaN(d.y)) {
    d.x = innerW / 2;
    d.y = innerH / 2;
  }
});

Tooltip Pattern (ETF-safe)

node.on('mouseover', function(event, d) {
  if (!d.marketCap) return;  // Skip ETFs — no marketCap means ETF
  tooltip
    .style('opacity', 1)
    .html(`<strong>${d.ticker}</strong><br>${d.name}<br>${d.sector}`)
    .style('left', (event.pageX + 12) + 'px')
    .style('top', (event.pageY - 28) + 'px');
})
.on('mouseout', function(event, d) {
  if (!d.marketCap) return;
  tooltip.style('opacity', 0);
});

Tooltip CSS

.tooltip {
  position: absolute;
  background: rgba(0,0,0,0.75);
  color: #fff;
  padding: 8px 12px;
  border-radius: 4px;
  pointer-events: none;
  font-size: 13px;
  opacity: 0;  /* hidden by default via opacity, not display:none */
  transition: opacity 0.15s;
}

SVG Container in HTML

<!-- Must exist before JS runs -->
<div id="bubble-chart"></div>

Color Scale Including ETF

const colorScale = d3.scaleOrdinal(d3.schemeTableau10)
  .domain(sectors);  // sectors already includes 'ETF' from fallback assignment

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.