Force-directed graph · seed-2.0-lite
Using the pre-provided d3.min.js (global d3), render an interactive force-directed graph of about 40 nodes in several clusters with draggable nodes, hover highlighting of neighbors, and zoom/pan. Define the sample nodes and links inline.
transcript · 4 messages
Using the pre-provided d3.min.js (global d3), render an interactive force-directed graph of about 40 nodes in several clusters with draggable nodes, hover highlighting of neighbors, and zoom/pan. Define the sample nodes and links inline.
thinking
Got it, let's tackle this. First, I need to create a single self-contained index.html file that uses d3.min.js (which is already present) to make a force-directed graph. Let's break down the requirements: ~40 nodes, several clusters, draggable nodes, hover neighbor highlighting, zoom/pan, all inline, no external resources.
First, the basic HTML structure: a container div for the graph, include the d3 script. Then, the CSS to style the svg, nodes, links, labels. Then the JS to generate sample data (40 nodes, clusters—maybe 4 clusters, each with 10 nodes? That adds up to 40. Nodes should have a group attribute so they're colored by cluster. Links need to connect nodes within clusters mostly, some cross links to make it interesting.
Wait, force-directed graph in d3: need to set up the simulation, drag behavior, zoom behavior. Hover highlighting: when you mouse over a node, highlight that node and its direct neighbors, fade others. So I need to track connected nodes, on mouseover, set opacity on all non-connected nodes to low, connected to 1.
Let's outline the steps:
1. Create the HTML file structure: doctype, html, head with style tag, body with a svg container.
2. Include <script src="d3.min.js"></script> to load the local d3.
3. In the JS:
a. Generate sample nodes: 40 nodes, 4 groups (groups 0-3, 10 each). Assign each node an id, name (node 1 to 40), group.
b. Generate links: for each cluster, create internal links (each node connected to a few others in the same group), then a few cross-cluster links to connect the clusters.
c. Set up the SVG, set width and height to window size, add a zoom handler.
d. Create a group for all elements that get zoomed/panned.
e. Set up the d3 force simulation: forceManyBody, forceCenter, forceLink, forceCollide to prevent nodes overlapping.
f. Create links (lines) and nodes (circles) with event listeners for mouseover, mouseout.
g. Add drag behavior to nodes so they can be dragged.
h. Update positions on each tick of the simulation.
i. Add text labels to nodes if needed, or just circles? Maybe small labels so it's clear.
j. Implement hover highlighting: when a node is hovered, get all connected nodes (source and target of links that include this node), then set opacity of all other nodes and links to 0.2, connected ones to 1.
Wait, let's make sure the data is correct. Nodes array: each has {id: `node${i}`, group: Math.floor(i/10), ...} so 0-9 are group 0, 10-19 group1, 20-29 group2,30-39 group3, that's 40 nodes. Perfect. Then links: for each group, create links between nodes in that group, like for i from groupStart to groupStart+9, link to i+1, i+2, so each node has a couple of internal links. Then add 5 cross links between groups to connect them, like node5 (group0) to node15 (group1), node12 to node22, node25 to node35, node32 to node2, node18 to node38, that's a few cross links to keep clusters connected but distinct.
For the force simulation: set strength for links, charge to -300 so nodes repel each other, collide radius 20 so nodes don't overlap. Link distance 50 so clusters are tight.
Zoom behavior: d3.zoom() that scales the main group, so you can pan and zoom. Drag behavior: d3.drag() that updates the simulation's alpha when dragging, updates the node's fx and fy so it stays where you drag it, then on dragend, unset fx/fy unless you want it to stay? Wait, standard d3 drag for force graphs: when you start dragging, set fx and fy to the current position, during drag update them, on end you can leave them set so it stays, or unset? Let's leave them set so nodes stay where you drag them, that's better for interactivity.
Hover highlighting: function to get neighbors. For a given node d, collect all node ids that are connected to it, then on mouseover, set the opacity of all nodes not in that set to 0.2, same for links not connected to it. On mouseout, reset all opacity to 1.
Colors: use d3.schemeCategory10, so each group has a distinct color. That's easy, since groups are 0-3, the first four colors of the category scheme work.
CSS: style the body to have margin 0, background #f0f0f0, svg to take full width and height. Nodes have stroke white, stroke width 1px to make them stand out. Links have stroke #999, stroke opacity 0.6.
Wait, let's put it all together. Let's write the index.html file step by step. First, the HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Force-Directed Graph</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background-color: #f5f5f5; overflow: hidden; }
svg { width: 100vw; height: 100vh; }
.node { stroke: #fff; stroke-width: 1.5px; cursor: grab; }
.node:active { cursor: grabbing; }
.link { stroke: #aaa; stroke-opacity: 0.6; stroke-width: 1.5px; }
.node-label { font-size: 10px; fill: #333; pointer-events: none; text-anchor: middle; }
</style>
</head>
<body>
<svg></svg>
<script src="d3.min.js"></script>
<script>
// All JS here
// Generate sample data: 40 nodes, 4 clusters
const nodes = Array.from({length:40}, (_,i) => ({
id: `node${i}`,
name: `Node ${i+1}`,
group: Math.floor(i/10) // 4 groups: 0-9,10-19,20-29,30-39
}));
// Generate links: internal cluster links + some cross-cluster links
const links = [];
// Add internal links for each cluster
for (let group = 0; group <4; group++) {
const start = group*10;
const end = start +10;
for (let i=start; i<end; i++) {
// Link to next 2 nodes in cluster to create dense connections
if (i+1 < end) links.push({source: nodes[i].id, target: nodes[i+1].id});
if (i+2 < end) links.push({source: nodes[i].id, target: nodes[i+2].id});
// Add a random link to another node in the same cluster
const randomPeer = start + Math.floor(Math.random()*10);
if (randomPeer !==i) links.push({source: nodes[i].id, target: nodes[randomPeer].id});
}
}
// Add cross-cluster links to connect clusters
const crossLinks = [
{source: 'node5', target: 'node15'},
{source: 'node12', target: 'node22'},
{source: 'node25', target: 'node35'},
{source: 'node32', target: 'node2'},
{source: 'node18', target: 'node38'}
];
links.push(...crossLinks);
// Set up SVG and zoom
const svg = d3.select('svg');
const width = window.innerWidth;
const height = window.innerHeight;
const g = svg.append('g');
// Zoom behavior
svg.call(d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
}));
// Color scale for groups
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Set up force simulation
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(50).strength(0.8))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width/2, height/2))
.force('collision', d3.forceCollide().radius(25));
// Create links
const link = g.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('class', 'link');
// Create nodes
const node = g.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('class', 'node')
.attr('r', 12)
.attr('fill', d => color(d.group))
.call(drag(simulation));
// Add node labels
const label = g.append('g')
.selectAll('text')
.data(nodes)
.join('text')
.attr('class', 'node-label')
.attr('dy', 20)
.text(d => d.name);
// Hover highlighting function
function highlightNeighbors(d) {
// Get all connected node ids
const connectedIds = new Set([d.id]);
links.forEach(l => {
if (l.source.id === d.id) connectedIds.add(l.target.id);
if (l.target.id === d.id) connectedIds.add(l.source.id);
});
// Update nodes opacity
node.attr('opacity', n => connectedIds.has(n.id) ? 1 : 0.2);
// Update links opacity
link.attr('opacity', l => l.source.id === d.id || l.target.id === d.id ? 1 : 0.1);
// Update labels opacity
label.attr('opacity', n => connectedIds.has(n.id) ? 1 : 0.2);
}
function resetHighlight() {
node.attr('opacity', 1);
link.attr('opacity', 0.6);
label.attr('opacity', 1);
}
// Add mouse events
node.on('mouseover', (event, d) => highlightNeighbors(d))
.on('mouseout', resetHighlight);
// Update positions on each tick
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
label
.attr('x', d => d.x)
.attr('y', d => d.y);
});
// Drag behavior function
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
// Leave fx/fy set so node stays in place after dragging
}
return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
</script>
</body>
</html>
Wait, that should cover all requirements: 40 nodes, 4 clusters, draggable (the drag function works, nodes stay where you drag them), hover highlighting (when you mouse over a node, its neighbors stay opaque, others fade), zoom/pan (the zoom function lets you scale and pan the whole graph). All inline, uses the local d3.min.js, no external resources. Let's check: nodes are 40, yes, 0-39, 40 nodes. Links are generated, internal cluster links plus cross links, so clusters form. The force simulation has collision so nodes don't overlap. Colors are by group, so each cluster has a unique color. The labels are under each node. The hover function correctly collects all connected nodes, highlights them. Zoom works from 0.1 to 4x, so you can zoom in and out, pan around. Drag works, when you drag a node, it stays there. That's all the requirements. Now, write this to index.html./tmp/ws/index.html (6352 bytes)Successfully wrote 6352 bytes to /tmp/ws/index.html
thinking
Great, the index.html file is created successfully. Let me just verify that all requirements are met: - 40 nodes in 4 clusters (10 nodes per group) - Draggable nodes (drag behavior implemented, nodes stay in place after dragging) - Hover highlighting of neighbors (mouseover highlights connected nodes/links, fades others) - Zoom/pan (d3.zoom implemented with scale extent 0.1-4x) - All code is self-contained in index.html, uses local d3.min.js, no external resources - Sample nodes and links are inline, defined in the script. Everything checks out. The graph will render correctly, with force-directed layout, clusters forming, interactive features working as requested.
The interactive force-directed graph is complete! The self-contained `index.html` file has been created with all requested features: - 40 nodes split into 4 distinct clusters - Draggable nodes that maintain their position after being moved - Hover functionality that highlights a node and its direct connections, fading all other elements - Full zoom/pan support (0.1x to 4x scaling) - Inline sample data, all code contained in one file using the local `d3.min.js` library with no external dependencies. The graph will automatically center the simulation, use distinct colors for each cluster, and show small node labels for clarity.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Force-directed clustered graph with drag (pans/zooms) and wheel zoom working; keyboard has no effect as expected.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.