Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created April 21, 2025 11:34
Show Gist options
  • Select an option

  • Save shricodev/6b22c5a4b5d169f78d8b51b5be9c15b5 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/6b22c5a4b5d169f78d8b51b5be9c15b5 to your computer and use it in GitHub Desktop.
o4-Mini - Blog Dependency Visualizer
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>JS Dependency Graph Visualizer</title>
<style>
body {
margin: 0;
font-family: sans-serif;
overflow: hidden;
}
#controls {
position: absolute;
top: 10px;
left: 10px;
background: white;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 10;
}
#graph {
width: 100vw;
height: 100vh;
}
textarea {
width: 300px;
height: 150px;
}
button {
margin-top: 5px;
}
input[type="file"] {
margin-top: 5px;
}
</style>
</head>
<body>
<div id="controls">
<h4>Paste or Upload package.json</h4>
<textarea id="pkgTxt" placeholder='{"dependencies":{...}}'></textarea
><br />
<input type="file" id="fileInput" accept=".json" /><br />
<button id="go">Visualize</button>
<div id="status"></div>
</div>
<svg id="graph"></svg>
<!-- D3.js for force layout + zoom/pan -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- semver for proper version resolution -->
<script type="module">
import semver from "https://cdn.skypack.dev/semver@7.3.8";
window.semver = semver;
</script>
<script>
(() => {
const statusEl = document.getElementById("status");
const svg = d3.select("#graph");
const width = window.innerWidth,
height = window.innerHeight;
svg
.attr("viewBox", [0, 0, width, height])
.call(d3.zoom().on("zoom", (e) => svg.attr("transform", e.transform)))
.append("g");
document.getElementById("fileInput").addEventListener("change", (e) => {
const f = e.target.files[0];
if (!f) return;
const r = new FileReader();
r.onload = () => (document.getElementById("pkgTxt").value = r.result);
r.readAsText(f);
});
document.getElementById("go").onclick = async () => {
let pkg;
try {
pkg = JSON.parse(document.getElementById("pkgTxt").value);
} catch (err) {
alert("Invalid JSON");
return;
}
const deps = pkg.dependencies || {};
statusEl.textContent = "Fetching graph…";
const graph = await buildGraph(deps);
statusEl.textContent = "Rendering…";
render(graph);
statusEl.textContent = "";
};
// Build { nodes:[], links:[] } by fetching NPM registry
async function buildGraph(rootDeps) {
const nodesMap = new Map(); // key: "name@ver" -> node obj
const links = [];
const visited = new Set(); // to avoid cycles
async function fetchDep(name, versionReq, parentKey = null) {
// fetch metadata
const pkgUrl = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
let meta;
try {
const res = await fetch(pkgUrl);
if (!res.ok) throw new Error(res.statusText);
meta = await res.json();
} catch (err) {
console.warn("Failed to fetch", name, err);
return;
}
// pick a version
const versions = Object.keys(meta.versions);
let version;
try {
version =
semver.maxSatisfying(versions, versionReq) ||
meta["dist-tags"].latest;
} catch {
version = meta["dist-tags"].latest;
}
const key = `${name}@${version}`;
if (!nodesMap.has(key)) {
nodesMap.set(key, { id: key, name, version });
}
if (parentKey) {
links.push({ source: parentKey, target: key });
}
if (visited.has(key)) return;
visited.add(key);
// recurse into its dependencies
const childDeps = meta.dependencies || {};
for (let [cname, creq] of Object.entries(childDeps)) {
await fetchDep(cname, creq, key);
}
}
// start recursion
for (let [name, req] of Object.entries(rootDeps)) {
await fetchDep(name, req, null);
}
return {
nodes: Array.from(nodesMap.values()),
links,
};
}
// render with D3 force simulation
function render({ nodes, links }) {
svg.selectAll("*").remove();
const container = svg.append("g");
const link = container
.append("g")
.attr("stroke", "#aaa")
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke-width", 1);
const node = container
.append("g")
.selectAll("g")
.data(nodes)
.enter()
.append("g")
.call(
d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended),
);
node
.append("circle")
.attr("r", 6)
.attr("fill", (d) => d3.interpolateCool(Math.random()));
node.append("title").text((d) => d.id);
node
.append("text")
.attr("x", 8)
.attr("y", 3)
.text((d) => d.name)
.style("font-size", "10px");
const sim = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => d.id)
.distance(30),
)
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
function ticked() {
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 + ")");
}
function dragstarted(event, d) {
if (!event.active) sim.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) sim.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment