ModsCodeMERGEBLE LLC Search Claude Code Mods

ModsCode / Gallery / Designs / Force-directed graph

Force-directed graph

Import graph of a small repo laid out by forces: dot size by number of links, the hub file in Claude orange, the busiest files named.

Force-directed graphForce-directed graph
Flows and links · 380×300 · sample data
hooks/designs/force-graph.mjs
// force-graph: the import graph of a small TypeScript repo, 30 files and 44 import links, laid out by
// d3-force (ISC, Mike Bostock): link, many-body, collide and two gentle x/y pulls, run as 300 ticks after
// .stop() so no timer is involved and its own seeded random source gives the same layout every time.
// The y pull is stronger than the x pull, so the cloud comes out wide like the stage, and the settled
// extent is then scaled into the plot. The graph itself is grown from a seeded walk (20261035): each new
// file imports one earlier file, picked in proportion to the square of (its links + 1), then fifteen
// extra imports join a file picked the same way to a random one; that rich-get-richer rule is what gives
// real import graphs their one big hub (plain proportional picking was tried and gave a flat mesh whose
// top file had 9 links). Names go to files by degree, so the most linked file is types.ts (13 links, the
// one Claude orange node). Dot radius grows with the square root of degree from candle's 2px dot, and a size key
// at the top right reads it back; links are 1px at 25% ink. The five most linked files carry their name
// in the mono face and their link count, each placed on whichever of eight sides clears every dot and
// earlier label and has the fewest links running through it. The graph is too dense for a side with
// none (a strict no-crossing pass found none for any of the five), so the labels get a 3px stage halo
// and a passing link stops short of the letters instead of striking them. Sample data only.
import { forceSimulation, forceLink, forceManyBody, forceCollide, forceX, forceY } from '../vendor/d3.mjs'

const W = 380
const H = 300
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 PLOT = { left: 24, right: 356, top: 34, bottom: 262 } // the band above holds the size key
const FILES = 30
const EXTRA = 15 // imports added after the tree, so 29 + 15 = 44 links
const LABELS = 5
// From most to least linked: shared types and helpers sit at the centre of a repo, entry points at the edge
const NAMES = [
  'types.ts', 'utils.ts', 'config.ts', 'client.ts', 'store.ts', 'logger.ts', 'session.ts', 'tokens.ts',
  'hooks.ts', 'router.ts', 'format.ts', 'cache.ts', 'errors.ts', 'schema.ts', 'auth.ts', 'pane.ts',
  'diff.ts', 'git.ts', 'agent.ts', 'tools.ts', 'prompt.ts', 'stream.ts', 'retry.ts', 'usage.ts',
  'theme.ts', 'keys.ts', 'paths.ts', 'env.ts', 'cli.ts', 'index.ts',
]
const f = n => n.toFixed(1)

function rng(seed) {
  return () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
}

const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
// halo: a 3px stage-coloured edge painted under the glyphs, so a link that has to pass a label stops
// short of its letters instead of striking through them
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start', font = FONT, halo = false } = {}) =>
  `<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${font}" fill="${fill}"${halo ? ` stroke="${STAGE}" stroke-width="3" stroke-linejoin="round" paint-order="stroke"` : ''}>${esc(s)}</text>`

const radius = deg => 2 + 1.3 * Math.sqrt(deg - 1)

// Preferential attachment, then extra links between distinct, unlinked pairs
function sample() {
  const rnd = rng(20261035)
  const deg = Array(FILES).fill(0)
  const links = []
  const has = new Set()
  const add = (a, b) => {
    links.push([a, b])
    has.add(`${Math.min(a, b)}-${Math.max(a, b)}`)
    deg[a]++
    deg[b]++
  }
  const pick = n => {
    const weight = j => (deg[j] + 1) ** 2
    let total = 0
    for (let j = 0; j < n; j++) total += weight(j)
    let r = rnd() * total
    for (let j = 0; j < n; j++) if ((r -= weight(j)) < 0) return j
    return n - 1
  }
  for (let i = 1; i < FILES; i++) add(i, pick(i))
  while (links.length < FILES - 1 + EXTRA) {
    const a = pick(FILES)
    const b = Math.floor(rnd() * FILES)
    if (a !== b && !has.has(`${Math.min(a, b)}-${Math.max(a, b)}`)) add(a, b)
  }
  // Names by degree: the most linked file takes the first name
  const rank = deg.map((d, i) => i).sort((p, q) => deg[q] - deg[p] || p - q)
  const nodes = deg.map(d => ({ deg: d, r: radius(d) }))
  rank.forEach((i, k) => { nodes[i].name = NAMES[k]; nodes[i].rank = k })
  return { nodes, links: links.map(([source, target]) => ({ source, target })) }
}

function draw() {
  const { nodes, links } = sample()
  forceSimulation(nodes)
    .randomSource(rng(20261036))
    .force('link', forceLink(links).distance(20))
    .force('charge', forceManyBody().strength(-46))
    .force('collide', forceCollide(n => n.r + 3))
    .force('x', forceX(0).strength(0.05))
    .force('y', forceY(0).strength(0.075))
    .stop()
    .tick(300)

  // Fit the settled cloud into the plot with one scale for both axes, centred
  const x0 = Math.min(...nodes.map(n => n.x - n.r))
  const x1 = Math.max(...nodes.map(n => n.x + n.r))
  const y0 = Math.min(...nodes.map(n => n.y - n.r))
  const y1 = Math.max(...nodes.map(n => n.y + n.r))
  const k = Math.min((PLOT.right - PLOT.left) / (x1 - x0), (PLOT.bottom - PLOT.top) / (y1 - y0))
  const ox = (PLOT.left + PLOT.right) / 2 - (k * (x0 + x1)) / 2
  const oy = (PLOT.top + PLOT.bottom) / 2 - (k * (y0 + y1)) / 2
  for (const n of nodes) {
    n.px = ox + k * n.x
    n.py = oy + k * n.y
  }

  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  const d = links.map(l => `M${f(l.source.px)} ${f(l.source.py)}L${f(l.target.px)} ${f(l.target.py)}`).join('')
  parts.push(`<path d="${d}" fill="none" stroke="${INK}" stroke-opacity="0.25"/>`)
  for (const n of [...nodes].sort((a, b) => a.deg - b.deg)) {
    const look = n.rank === 0 ? `fill="${CLAUDE}" fill-opacity="0.9"` : `fill="${INK}" fill-opacity="0.55"`
    parts.push(`<circle cx="${f(n.px)}" cy="${f(n.py)}" r="${f(n.r)}" ${look}/>`)
  }

  // Size key: three dots read back the radius rule, right-aligned at W - 12
  const KEY = [1, 5, 10]
  let kx = W - 12
  for (const v of [...KEY].reverse()) {
    const label = String(v)
    parts.push(text(kx, 20, label, { anchor: 'end' }))
    kx -= label.length * 5 + 3 + radius(v)
    parts.push(`<circle cx="${f(kx)}" cy="17" r="${f(radius(v))}" fill="${INK}" fill-opacity="0.55"/>`)
    kx -= radius(v) + 10
  }
  parts.push(text(kx + 4, 20, 'LINKS', { anchor: 'end' }))

  // Labels: name (mono 10px, about 6px a character) then degree (9px); the first clear side wins
  const boxes = []
  const hitsDot = (b, self) => nodes.some(n => n !== self && n.px + n.r > b.x0 && n.px - n.r < b.x1 && n.py + n.r > b.y0 && n.py - n.r < b.y1)
  const hitsBox = b => boxes.some(o => o.x1 > b.x0 && o.x0 < b.x1 && o.y1 > b.y0 && o.y0 < b.y1)
  // Liang-Barsky clip: does the link's segment enter the box at all
  const crosses = (b, l) => {
    const x = l.source.px
    const y = l.source.py
    const dx = l.target.px - x
    const dy = l.target.py - y
    let t0 = 0
    let t1 = 1
    for (const [p, q] of [[-dx, x - b.x0], [dx, b.x1 - x], [-dy, y - b.y0], [dy, b.y1 - y]]) {
      if (p === 0) { if (q < 0) return false; continue }
      const t = q / p
      if (p < 0) t0 = Math.max(t0, t)
      else t1 = Math.min(t1, t)
      if (t0 > t1) return false
    }
    return true
  }
  const linksIn = b => links.filter(l => crosses(b, l)).length
  for (const n of [...nodes].sort((a, b) => a.rank - b.rank).slice(0, LABELS)) {
    const wName = n.name.length * 6
    const w = wName + 4 + String(n.deg).length * 5
    const g = n.r * 0.7 + 3 // a diagonal side starts this far off the centre on each axis
    const sides = [
      [n.px + n.r + 4, n.py + 3.5], // right
      [n.px - n.r - 4 - w, n.py + 3.5], // left
      [n.px - w / 2, n.py - n.r - 5], // above
      [n.px - w / 2, n.py + n.r + 12], // below
      [n.px + g, n.py - g], // up right
      [n.px + g, n.py + g + 8], // down right
      [n.px - g - w, n.py - g], // up left
      [n.px - g - w, n.py + g + 8], // down left
    ]
    // Of the sides clear of the stage edge, every dot and earlier labels, the one fewest links run
    // through wins (the earlier side on a tie); the graph is too dense for a side with none
    let best = null
    for (const [lx, ly] of sides) {
      const b = { x0: lx - 2, x1: lx + w * 1.1 + 2, y0: ly - 9, y1: ly + 3 } // 10% slack on the width
      if (b.x0 < 8 || b.x1 > W - 8 || b.y0 < 28 || b.y1 > H - 22) continue
      if (hitsDot(b, n) || hitsBox(b)) continue
      const cut = linksIn(b)
      if (!best || cut < best.cut) best = { lx, ly, b, cut }
    }
    if (!best) continue
    boxes.push(best.b)
    parts.push(text(best.lx, best.ly, n.name, { size: 10, fill: n.rank === 0 ? CLAUDE : INK, font: MONO, halo: true }))
    parts.push(text(best.lx + wName + 4, best.ly, n.deg, { halo: true }))
  }

  parts.push(text(W - 12, H - 8, `${FILES} FILES, ${links.length} IMPORTS`, { anchor: 'end' }))
  parts.push(text(12, H - 8, 'SAMPLE IMPORT GRAPH'))
  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: 'force-graph',
  title: 'Force-directed graph',
  category: 'flow',
  description: 'Import graph of a small repo laid out by forces: dot size by number of links, the hub file in Claude orange, the busiest files named.',
  element: ui => ui.Svg({ source: (source ??= draw()), alt: `Force-directed import graph of ${FILES} files and ${FILES - 1 + EXTRA} links, with the most linked file as the Claude orange hub, sample data`, width: W, height: H }),
}

Imports d3-force 3.0.0 (ISC), bundled with its licence in look's hooks/vendor/.

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).