agentsclimarketplace

Run2 d3 force cluster

Skill cxcscmu/SkillLearnBench/skills/b2-self-feedback-claude-sonnet-4-6/stock-data-visualization/run2_d3-force-cluster

D3.js v6 force simulation for clustered bubbles by category using forceX/forceY anchors, forceCollide for no overlap, with truly deterministic initial positions.From its SKILL.md

Install
npx -y skills add cxcscmu/SkillLearnBench --skill run2_d3-force-cluster

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

SKILL.md

3.2 KB, 874 tokens by cl100k_base, as published. Nobody here has run it

D3.js v6 Force Cluster Simulation (Improved)

Key Principles

  1. Never use Math.random() — use deterministic golden-angle spiral per sector
  2. Run fixed ticks, then stop — don't animate
  3. Clamp final positions within SVG bounds after simulation
  4. Use forceX + forceY strength 0.10–0.15 for cohesive clusters

Sector Center Layout

With 5 sectors, use a ring with one sector on top and four around it:

const sectors = Object.keys(SECTOR_COLORS);  // sorted, predictable
const W = 800, H = 600;
const cx = W / 2, cy = H / 2;

const sectorCenters = {};
sectors.forEach((sector, i) => {
    // Elliptical placement; adjust rx/ry for chart proportions
    const angle = (i / sectors.length) * 2 * Math.PI - Math.PI / 2;
    sectorCenters[sector] = {
        x: cx + (W * 0.28) * Math.cos(angle),
        y: cy + (H * 0.28) * Math.sin(angle)
    };
});

Deterministic Initial Positions (Critical!)

D3's simulation starts from node x/y. Seed them deliberately:

const sectorCounters = {};
// Sort nodes deterministically before seeding
const nodes = [...data].sort((a, b) => a.ticker.localeCompare(b.ticker));
nodes.forEach(d => {
    const center = sectorCenters[d.sector];
    const i = sectorCounters[d.sector] = (sectorCounters[d.sector] || 0);
    sectorCounters[d.sector]++;
    // Golden-angle spiral — fully deterministic, no Math.random()
    const angle = i * 2.39996;   // golden angle ≈ 137.5°
    const r = 6 * Math.sqrt(i + 1);
    d.x = center.x + r * Math.cos(angle);
    d.y = center.y + r * Math.sin(angle);
});

Force Simulation Setup

const simulation = d3.forceSimulation(nodes)
    .force('x', d3.forceX(d => sectorCenters[d.sector].x).strength(0.12))
    .force('y', d3.forceY(d => sectorCenters[d.sector].y).strength(0.12))
    .force('collide', d3.forceCollide(d => d.radius + 2).strength(1.0))
    .force('charge', d3.forceManyBody().strength(-10))
    .alphaDecay(0.02)
    .stop();  // Don't auto-run

// Run exactly 400 ticks for determinism
for (let i = 0; i < 400; i++) simulation.tick();

// Clamp to SVG bounds
nodes.forEach(d => {
    d.x = Math.max(d.radius + 4, Math.min(W - d.radius - 4, d.x));
    d.y = Math.max(d.radius + 4, Math.min(H - d.radius - 4, d.y));
});

Placing SVG Elements After Simulation

Since we stop before rendering, just set attributes directly (no ticking):

circles.attr('cx', d => d.x).attr('cy', d => d.y);
labels.attr('x', d => d.x).attr('y', d => d.y);

Responsive SVG

Use viewBox on the SVG and measure container at draw time:

const W = container.clientWidth || 700;
const H = container.clientHeight || 560;
svg.attr('viewBox', `0 0 ${W} ${H}`)
   .attr('preserveAspectRatio', 'xMidYMid meet');

Tuning Guidance

  • More nodes in a sector → increase W * 0.28 slightly so clusters have more room
  • Large radius variance → reduce forceX/Y strength to 0.08 so big bubbles don't push out
  • Overlap still present → increase collide strength or reduce MAX_RADIUS

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.