Last active
February 25, 2026 17:24
-
-
Save capicue/7cef1dbbadbc106492ee7372a0b690cb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>D3 Force Graph (from Graphviz)</title> | |
| <style> | |
| html, body { height: 100%; margin: 0; } | |
| svg { width: 100%; height: 100%; display: block; background: #fff; } | |
| .link { stroke-width: 2.5px; stroke-opacity: 0.85; } | |
| .node circle { stroke: #111; stroke-width: 1.5px; fill: #fff; } | |
| .label { font: 16px system-ui, -apple-system, Segoe UI, Roboto, sans-serif; pointer-events: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <svg id="viz"></svg> | |
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <script> | |
| const nodes = [ | |
| { id: "A" }, { id: "B" }, { id: "C" }, { id: "D" }, { id: "E" }, { id: "F" }, | |
| { id: "G" }, { id: "H" }, { id: "I" }, { id: "J" }, { id: "K" }, { id: "L" }, | |
| ]; | |
| // Preserve duplicates. "Top" edges are black and 2x stronger attraction. | |
| const links = [ | |
| // top edges: black + 2x stronger attraction | |
| { source: "A", target: "B", strong: true, color: "black" }, | |
| { source: "D", target: "G", strong: true, color: "black" }, | |
| { source: "C", target: "F", strong: true, color: "black" }, | |
| { source: "F", target: "C", strong: true, color: "black" }, | |
| { source: "E", target: "I", strong: true, color: "black" }, | |
| { source: "K", target: "L", strong: true, color: "black" }, | |
| { source: "H", target: "J", strong: true, color: "black" }, | |
| { source: "J", target: "H", strong: true, color: "black" }, | |
| // colored edges | |
| { source: "E", target: "A", color: "red" }, | |
| { source: "E", target: "B", color: "red" }, | |
| { source: "E", target: "C", color: "red" }, | |
| { source: "E", target: "D", color: "red" }, | |
| { source: "E", target: "F", color: "red" }, | |
| { source: "I", target: "C", color: "orange" }, | |
| { source: "I", target: "D", color: "orange" }, | |
| { source: "I", target: "F", color: "orange" }, | |
| { source: "I", target: "G", color: "orange" }, | |
| { source: "I", target: "H", color: "orange" }, | |
| { source: "D", target: "B", color: "yellow" }, | |
| { source: "D", target: "E", color: "yellow" }, | |
| { source: "D", target: "F", color: "yellow" }, | |
| { source: "D", target: "I", color: "yellow" }, | |
| { source: "D", target: "J", color: "yellow" }, | |
| { source: "G", target: "A", color: "green" }, | |
| { source: "G", target: "E", color: "green" }, | |
| { source: "G", target: "H", color: "green" }, | |
| { source: "G", target: "I", color: "green" }, | |
| { source: "G", target: "J", color: "green" }, | |
| { source: "A", target: "C", color: "blue" }, | |
| { source: "A", target: "E", color: "blue" }, | |
| { source: "A", target: "F", color: "blue" }, | |
| { source: "A", target: "H", color: "blue" }, | |
| { source: "A", target: "I", color: "blue" }, | |
| { source: "L", target: "E", color: "purple" }, | |
| { source: "L", target: "H", color: "purple" }, | |
| { source: "L", target: "I", color: "purple" }, | |
| { source: "L", target: "J", color: "purple" }, | |
| { source: "L", target: "K", color: "purple" }, | |
| { source: "B", target: "E", color: "pink" }, | |
| { source: "B", target: "F", color: "pink" }, | |
| { source: "B", target: "G", color: "pink" }, | |
| { source: "B", target: "I", color: "pink" }, | |
| { source: "B", target: "J", color: "pink" }, | |
| { source: "K", target: "D", color: "brown" }, | |
| { source: "K", target: "E", color: "brown" }, | |
| { source: "K", target: "I", color: "brown" }, | |
| { source: "K", target: "J", color: "brown" }, | |
| { source: "K", target: "L", color: "brown" }, | |
| ]; | |
| const svg = d3.select("#viz"); | |
| const width = () => svg.node().clientWidth; | |
| const height = () => svg.node().clientHeight; | |
| // Zoom + pan | |
| const g = svg.append("g"); | |
| const zoom = d3.zoom() | |
| .scaleExtent([0.1, 10]) | |
| .on("zoom", (event) => g.attr("transform", event.transform)); | |
| svg.call(zoom); | |
| // Force parameters | |
| // (Bigger footprint: more distance + more repulsion) | |
| const baseDistance = 160; | |
| const baseStrength = 0.8; | |
| const simulation = d3.forceSimulation(nodes) | |
| .force("link", d3.forceLink(links) | |
| .id(d => d.id) | |
| .distance(l => l.strong ? baseDistance / 2 : baseDistance) // strong edges tighter | |
| .strength(l => l.strong ? baseStrength * 2 : baseStrength) // strong edges stiffer | |
| ) | |
| .force("charge", d3.forceManyBody().strength(-900)) | |
| // We'll set center to (0,0) so the zoom transform determines on-screen centering | |
| .force("center", d3.forceCenter(0, 0)) | |
| .force("collide", d3.forceCollide(26)); | |
| // Set initial zoom centered + zoomed in immediately (no animation, no later re-centering) | |
| const initialScale = 1.8; // tweak to taste | |
| svg.call( | |
| zoom.transform, | |
| d3.zoomIdentity | |
| .translate(width() / 2, height() / 2) | |
| .scale(initialScale) | |
| ); | |
| // Links | |
| const link = g.append("g") | |
| .attr("stroke-linecap", "round") | |
| .selectAll("line") | |
| .data(links) | |
| .join("line") | |
| .attr("class", "link") | |
| .attr("stroke", d => d.color || "#999"); | |
| // Nodes | |
| const node = g.append("g") | |
| .selectAll("g") | |
| .data(nodes) | |
| .join("g") | |
| .attr("class", "node") | |
| .call(drag(simulation)); | |
| node.append("circle").attr("r", 18); | |
| node.append("text") | |
| .attr("class", "label") | |
| .attr("text-anchor", "middle") | |
| .attr("dy", "0.35em") | |
| .text(d => d.id); | |
| 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("transform", d => `translate(${d.x},${d.y})`); | |
| }); | |
| window.addEventListener("resize", () => { | |
| // Keep whatever zoom/pan the user has; just keep the sim moving a bit. | |
| simulation.alpha(0.3).restart(); | |
| }); | |
| function drag(sim) { | |
| function dragstarted(event) { | |
| if (!event.active) sim.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) sim.alphaTarget(0); | |
| event.subject.fx = null; | |
| event.subject.fy = null; | |
| } | |
| return d3.drag() | |
| .on("start", dragstarted) | |
| .on("drag", dragged) | |
| .on("end", dragended); | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment