agentsclimarketplace

D3 bubble chart

Skill cxcscmu/SkillLearnBench/skills/b4-skill-creator-claude-sonnet-4-6/stock-data-visualization/d3-bubble-chart

How to build a D3.js v6 force-directed bubble chart with sector clustering, size encoding, color legends, tooltips, and interactive selection. Use this skill whenever the user wants a bubble chart, force simulation, cluster layout, or interactive SVG circles with D3.From its SKILL.md

Install
npx -y skills add cxcscmu/SkillLearnBench --skill d3-bubble-chart

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, 872 tokens by cl100k_base, as published. Nobody here has run it

D3 v6 Force-Directed Bubble Chart

Core Pattern

const simulation = d3.forceSimulation(nodes)
  .force("x", d3.forceX(d => sectorX(d.sector)).strength(0.08))
  .force("y", d3.forceY(d => sectorY(d.sector)).strength(0.08))
  .force("collide", d3.forceCollide(d => d.r + 2).strength(0.9))
  .force("charge", d3.forceManyBody().strength(-5))
  .on("tick", ticked);

Sizing Bubbles by Market Cap

Use d3.scaleSqrt so that area (not radius) encodes value — this is perceptually correct:

const radiusScale = d3.scaleSqrt()
  .domain([0, d3.max(data, d => d.marketCap)])
  .range([minR, maxR]);  // e.g. [8, 60]

ETFs (no market cap) get a fixed uniform radius (e.g. 18).

Sector Clustering with forceX/forceY

Pre-compute sector center positions on a grid or circle, then use them as force targets:

const sectors = [...new Set(data.map(d => d.sector))];
const sectorCenters = {};
sectors.forEach((s, i) => {
  const angle = (i / sectors.length) * 2 * Math.PI;
  sectorCenters[s] = {
    x: cx + Math.cos(angle) * clusterRadius,
    y: cy + Math.sin(angle) * clusterRadius
  };
});

Strength 0.05–0.12 creates visible clustering without forcing everything to a single point.

Tick Handler

function ticked() {
  node.attr("cx", d => d.x).attr("cy", d => d.y);
  labels.attr("x", d => d.x).attr("y", d => d.y);
}

Color Legend

const legend = svg.append("g").attr("transform", `translate(${legendX}, ${legendY})`);
sectors.forEach((sector, i) => {
  legend.append("circle").attr("r", 7).attr("cx", 0).attr("cy", i * 22).attr("fill", color(sector));
  legend.append("text").attr("x", 12).attr("y", i * 22 + 5).text(sector);
});

Tooltip

Create a div tooltip with position: absolute; pointer-events: none:

const tooltip = d3.select("body").append("div").attr("class", "tooltip");
node.on("mouseover", (event, d) => {
  tooltip.style("display", "block")
    .html(`<b>${d.ticker}</b><br>${d.name}<br>${d.sector}`)
    .style("left", event.pageX + 12 + "px")
    .style("top",  event.pageY - 28 + "px");
}).on("mousemove", (event) => {
  tooltip.style("left", event.pageX + 12 + "px")
         .style("top",  event.pageY - 28 + "px");
}).on("mouseout", () => tooltip.style("display", "none"));

Click Selection

Dispatch a custom event so other components can react:

node.on("click", (event, d) => {
  d3.selectAll(".bubble").classed("selected", false);
  d3.select(event.currentTarget).classed("selected", true);
  document.dispatchEvent(new CustomEvent("stockSelected", { detail: d.ticker }));
});

Key Tips

  • Run the simulation with alphaDecay(0.02) and velocityDecay(0.3) for smoother settling.
  • Call .stop() then manually run ticks with simulation.tick(300) for static initial layout.
  • Clamp node positions within the SVG viewBox to prevent overflow.
  • Use viewBox + preserveAspectRatio for responsive sizing.

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.