ModsCode / Gallery / Designs / Dependency graph
Dependency graph
Packages of a workspace as a layered graph from left to right: longest-path columns, barycentre order, versions, one outdated package.


hooks/designs/dependency-graph.mjs
// dependency-graph: twelve packages of a mods workspace and their sixteen "depends on" edges, drawn as
// a layered graph from left to right. The layout is the classic layered (Sugiyama-style) recipe written
// out by hand here, with no ported code: a package's column is its longest path from a root, an edge
// that skips a column gets a hidden waypoint in each column it crosses, and the order inside a column
// comes from four down-and-up barycentre sweeps (each node moves to the mean place of its neighbours in
// the column before or after). Cards are 64 x 28 with kanban's 18% outline, the name at 10px and the
// version in the mono face; edges are 1px curves at 35% ink with level ends, so the columns carry the
// direction and no arrowheads are needed. schema is a major version behind and is the one Claude orange item.
// Static sample graph. Sample data only.
const W = 380
const H = 256
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const CLAUDE = '#D77757'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const MONO = "ui-monospace,'Cascadia Mono',Consolas,monospace"
const f = n => n.toFixed(1)
const CARD = { w: 64, h: 28 }
const X0 = 12
const MID = 134 // the centre line every column is centred on: the tallest column (five slots) spans 36 to 218
const PITCH = 42 // between two slots in a column
// name, version, and (for the outdated one) the latest release
const PACKAGES = [
['look', '0.5.0'], ['multichat', '1.2.1'],
['pane-kit', '0.9.4'], ['d3-sankey', '0.12.3'], ['ledger', '0.3.2'],
['d3-shape', '3.2.0'], ['hooks-api', '2.1.0'], ['tokens', '1.6.0'], ['d3-array', '3.2.4'],
['d3-path', '3.1.0'], ['schema', '1.4.2', '2.0.0'], ['internmap', '2.0.3'],
]
const EDGES = [
['look', 'pane-kit'], ['look', 'd3-sankey'], ['look', 'ledger'], ['look', 'd3-shape'],
['multichat', 'pane-kit'], ['multichat', 'ledger'],
['pane-kit', 'hooks-api'], ['pane-kit', 'tokens'],
['d3-sankey', 'd3-shape'], ['d3-sankey', 'd3-array'],
['ledger', 'tokens'], ['ledger', 'schema'],
['d3-shape', 'd3-path'], ['hooks-api', 'schema'], ['tokens', 'schema'], ['d3-array', 'internmap'],
]
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start', font = FONT } = {}) =>
`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${font}" fill="${fill}">${s}</text>`
function layout() {
const nodes = PACKAGES.map(([name, version, latest]) => ({ name, version, latest, layer: 0, ins: [], outs: [] }))
const byName = Object.fromEntries(nodes.map(n => [n.name, n]))
// Longest-path layering: relax every edge until no column grows (12 nodes, so at most 12 passes)
for (let pass = 0; pass < nodes.length; pass++) {
let grew = false
for (const [a, b] of EDGES) if (byName[b].layer < byName[a].layer + 1) { byName[b].layer = byName[a].layer + 1; grew = true }
if (!grew) break
}
// Waypoints: an edge over k columns becomes a chain through k - 1 hidden nodes
const chains = EDGES.map(([a, b]) => {
const chain = [byName[a]]
for (let l = byName[a].layer + 1; l < byName[b].layer; l++) {
const w = { name: `${a}>${b}@${l}`, layer: l, hidden: true, ins: [], outs: [] }
nodes.push(w)
chain.push(w)
}
chain.push(byName[b])
for (let i = 1; i < chain.length; i++) { chain[i - 1].outs.push(chain[i]); chain[i].ins.push(chain[i - 1]) }
return chain
})
const depth = Math.max(...nodes.map(n => n.layer)) + 1
const layers = Array.from({ length: depth }, (_, l) => nodes.filter(n => n.layer === l))
// Place is measured from the column's centre, so columns of different lengths compare fairly
const place = () => layers.forEach(col => col.forEach((n, k) => { n.pos = k - (col.length - 1) / 2 }))
const mean = (list, fallback) => (list.length ? list.reduce((s, m) => s + m.pos, 0) / list.length : fallback)
place()
for (let sweep = 0; sweep < 4; sweep++) {
for (let l = 1; l < depth; l++) { layers[l].sort((p, q) => mean(p.ins, p.pos) - mean(q.ins, q.pos)); place() }
for (let l = depth - 2; l >= 0; l--) { layers[l].sort((p, q) => mean(p.outs, p.pos) - mean(q.outs, q.pos)); place() }
}
const gap = (W - 2 * X0 - depth * CARD.w) / (depth - 1)
for (const n of nodes) {
n.x = X0 + n.layer * (CARD.w + gap)
n.y = MID + n.pos * PITCH
}
return { nodes, chains, depth }
}
function draw() {
const { nodes, chains, depth } = layout()
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
for (let l = 0; l < depth; l++) parts.push(text(X0 + l * ((W - 2 * X0 - CARD.w) / (depth - 1)) + CARD.w / 2, 22, `DEPTH ${l}`, { anchor: 'middle' }))
// One path per edge: a level-ended curve between columns, and a straight run across each column the
// edge passes through as a waypoint
for (const chain of chains) {
let d = ''
for (let i = 1; i < chain.length; i++) {
const a = chain[i - 1]
const b = chain[i]
const x1 = a.x + CARD.w // a waypoint's straight run ends here too
const mx = (x1 + b.x) / 2
if (i === 1) d += `M${f(x1)} ${f(a.y)}`
d += `C${f(mx)} ${f(a.y)} ${f(mx)} ${f(b.y)} ${f(b.x)} ${f(b.y)}`
if (b.hidden) d += `H${f(b.x + CARD.w)}`
}
parts.push(`<path d="${d}" fill="none" stroke="${INK}" stroke-opacity="0.35"/>`)
}
for (const n of nodes) {
if (n.hidden) continue
const top = n.y - CARD.h / 2
const edge = n.latest ? `stroke="${CLAUDE}" stroke-opacity="0.9"` : `stroke="${INK}" stroke-opacity="0.18"`
// The stage fill keeps a curve that ends at the card from showing through its outline
parts.push(`<rect x="${f(n.x + 0.5)}" y="${f(top + 0.5)}" width="${CARD.w - 1}" height="${CARD.h - 1}" rx="3" fill="${STAGE}" ${edge}/>`)
parts.push(text(n.x + 8, n.y - 1, n.name, { size: 10, fill: INK }))
parts.push(text(n.x + 8, n.y + 9, n.version, { font: MONO, fill: n.latest ? CLAUDE : MUTED }))
if (n.latest) parts.push(text(n.x + CARD.w, top + CARD.h + 11, `LATEST ${n.latest}`, { anchor: 'end' }))
}
parts.push(text(12, H - 8, `SAMPLE ${PACKAGES.length} PACKAGES, ${EDGES.length} EDGES`))
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}
let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
id: 'dependency-graph',
title: 'Dependency graph',
category: 'code',
description: 'Packages of a workspace as a layered graph from left to right: longest-path columns, barycentre order, versions, one outdated package.',
element: ui => ui.Svg({ source: (source ??= draw()), alt: `Layered dependency graph of ${PACKAGES.length} packages and ${EDGES.length} edges with their versions and the outdated one marked, sample data`, width: W, height: H }),
}
To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.
Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).