Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created April 21, 2025 13:19
Show Gist options
  • Save shricodev/b444308d4330b1982d1e6ce1e5fb3cf3 to your computer and use it in GitHub Desktop.
Save shricodev/b444308d4330b1982d1e6ce1e5fb3cf3 to your computer and use it in GitHub Desktop.
o3 - Blog Dependency Visualizer
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>NPM Dependency‑Graph Visualizer</title>
<!-- Basic page styling -->
<style>
* {
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
header {
padding: 10px;
background: #24292f;
color: #fff;
}
header h1 {
margin: 0;
font-size: 18px;
}
#controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
textarea {
flex: 1;
height: 140px;
width: 100%;
resize: vertical;
padding: 8px;
font-family: monospace;
}
#fileInput {
display: inline-block;
}
button {
padding: 8px 14px;
font-size: 14px;
cursor: pointer;
}
#status {
margin-left: auto;
color: #0dd;
}
/* graph area */
#graphContainer {
flex: 1;
min-height: 200px;
}
svg {
width: 100%;
height: 100%;
background: #fafafa;
cursor: move;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
}
.node circle {
stroke: #fff;
stroke-width: 1.5px;
cursor: pointer;
}
.node text {
font-size: 10px;
pointer-events: none;
}
</style>
</head>
<body>
<header>
<h1>NPM Dependency‑Graph Visualizer (Pure HTML/CSS/JS)</h1>
<div id="controls">
<input type="file" id="fileInput" accept="application/json" />
<button id="exampleBtn">Load example</button>
<button id="buildBtn">Build graph</button>
<span id="status"></span>
</div>
</header>
<textarea
id="pkgTextarea"
placeholder="Paste your package.json here…"
></textarea>
<div id="graphContainer"></div>
<!-- D3.js (force‑simulation + zoom/pan) -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
/* ---------- helper UI bits ---------- */
const $ = (id) => document.getElementById(id);
const status = (msg) => ($("status").textContent = msg);
$("fileInput").addEventListener("change", (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => ($("pkgTextarea").value = ev.target.result);
reader.readAsText(file);
});
$("exampleBtn").addEventListener("click", () => {
$("pkgTextarea").value = `{
"name": "sample-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"chalk": "^5.3.0"
},
"devDependencies": {
"eslint": "^8.55.0"
}
}`;
});
$("buildBtn").addEventListener("click", async () => {
const txt = $("pkgTextarea").value.trim();
if (!txt) {
alert("Paste or choose a package.json first.");
return;
}
let pkg;
try {
pkg = JSON.parse(txt);
} catch (e) {
alert("Not valid JSON");
return;
}
try {
status("Resolving…");
const graph = await buildGraph(pkg);
status(`Fetched ${graph.nodes.length} pkgs`);
drawGraph(graph);
} catch (e) {
console.error(e);
alert("Error while building graph – see console");
status("");
}
});
/* ---------- dependency resolution (npm registry) ---------- */
async function fetchMeta(name) {
const res = await fetch(`https://registry.npmjs.org/${name}`);
if (!res.ok) throw new Error(`HTTP ${res.status} for ${name}`);
return res.json();
}
async function buildGraph(rootPkg) {
// Merge dependencies & devDependencies
const rootDeps = Object.assign(
{},
rootPkg.dependencies || {},
rootPkg.devDependencies || {},
);
const nodes = new Map(); // key -> node {id}
const links = []; // edge objects
const queue = []; // BFS queue
const visited = new Set(); // package names already resolved
for (const dep in rootDeps)
queue.push({ parent: rootPkg.name || "root", name: dep });
nodes.set(rootPkg.name || "root", { id: rootPkg.name || "root" });
// Fetch depth‑first with a cap to avoid runaway explosions
const MAX_NODES = 400;
while (queue.length && nodes.size < MAX_NODES) {
const { parent, name } = queue.shift();
links.push({ source: parent, target: name });
if (visited.has(name)) continue; // already fully resolved
visited.add(name);
nodes.set(name, { id: name });
try {
const meta = await fetchMeta(name);
const latest = meta["dist-tags"]?.latest;
const deps = meta.versions?.[latest]?.dependencies || {};
for (const child in deps) {
queue.push({ parent: name, name: child });
}
} catch (err) {
console.warn("Could not fetch", name, err);
}
}
return { nodes: [...nodes.values()], links };
}
/* ---------- graph drawing (D3) ---------- */
function drawGraph({ nodes, links }) {
// Clear existing svg
d3.select("#graphContainer").selectAll("*").remove();
const width = $("graphContainer").clientWidth;
const height = $("graphContainer").clientHeight || 500;
const svg = d3
.select("#graphContainer")
.append("svg")
.attr("viewBox", [0, 0, width, height]);
// Zoom / pan
const g = svg.append("g");
svg.call(
d3
.zoom()
.scaleExtent([0.1, 4])
.on("zoom", (event) => g.attr("transform", event.transform)),
);
// links
const link = g
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("class", "link")
.attr("stroke-width", 1.2);
// nodes
const node = g
.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.attr("class", "node")
.call(drag());
node
.append("circle")
.attr("r", 8)
.attr("fill", (d) => (d.id === "root" ? "#ff4136" : "#1f77b4"));
node
.append("text")
.text((d) => d.id)
.attr("x", 10)
.attr("y", 3);
// physics
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => d.id)
.distance(50),
)
.force("charge", d3.forceManyBody().strength(-160))
.force("center", d3.forceCenter(width / 2, height / 2));
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})`);
});
// drag behaviour
function drag() {
function dragstarted(event, d) {
if (!event.active) simulation.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) simulation.alphaTarget(0);
d.fx = d.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