ModsCodeMERGEBLE LLC Search Claude Code Mods

ModsCode / Gallery / Designs / Circle packing

Circle packing

Installed packages packed inside their groups, each circle sized by disk space, with names inside where they fit and group totals on the outlines.

Circle packingCircle packing
Parts of a whole · 380×340 · sample data
hooks/designs/circle-pack.mjs
// circle-pack: what a Claude Code mod's node_modules holds, 23 packages in six groups, in candle's
// look. The circles come from d3-hierarchy's pack() (ISC, Mike Bostock; Wang et al.'s front-chain
// packing), so a circle's area is its size on disk. Groups are 1px outlines at 18% ink, the card
// outline step, and packages are 25% ink, the area step, so the leaves carry the weight and the groups
// only gather them. Each group's name and total sit in a gap left in its outline, like a fieldset
// legend, so they need no room inside: at the top, or at the bottom when another group presses on the
// top (on a tangent point a label read as either circle's), and the leaves keep 3 units of padding so
// the label clears them. A package name is drawn only when it fits across its
// circle, and its size only when the circle is tall enough for a second line. The sizes add up: cloud 55,
// lang 33, test 24, build 24, runtime 18, lint 15 = 169 MB. Sample data only.
import { hierarchy, pack } from '../vendor/d3.mjs'

const W = 380
const H = 340
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const PLOT = { left: 12, top: 16, right: 368, bottom: 314 } // the groups are fitted to this box
const f = n => n.toFixed(1)

// Installed MB per package
const DEPS = {
  name: 'node_modules',
  children: [
    { name: 'CLOUD', children: [{ name: 'workerd', size: 30 }, { name: 'wrangler', size: 14 }, { name: 'miniflare', size: 7 }, { name: 'undici', size: 4 }] },
    { name: 'LANG', children: [{ name: 'typescript', size: 26 }, { name: 'tsx', size: 5 }, { name: '@types/node', size: 2 }] },
    { name: 'TEST', children: [{ name: 'vitest', size: 9 }, { name: 'playwright', size: 8 }, { name: 'jsdom', size: 4 }, { name: 'msw', size: 3 }] },
    { name: 'BUILD', children: [{ name: 'esbuild', size: 11 }, { name: 'vite', size: 6 }, { name: 'rollup', size: 5 }, { name: 'postcss', size: 2 }] },
    { name: 'RUNTIME', children: [{ name: 'react-dom', size: 5 }, { name: 'sdk', size: 4 }, { name: 'zod', size: 3 }, { name: 'katex', size: 3 }, { name: 'd3', size: 2 }, { name: 'hono', size: 1 }] },
    { name: 'LINT', children: [{ name: 'oxlint', size: 8 }, { name: 'eslint', size: 7 }] },
  ],
}

const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start' } = {}) =>
  `<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`

function draw() {
  const root = hierarchy(DEPS).sum(d => d.size || 0).sort((a, b) => b.value - a.value)
  pack().size([300, 300]).padding(d => (d.depth === 0 ? 6 : 3))(root)
  // pack() lays the groups out a little taller than wide, so they are turned a quarter (x and y swap,
  // which keeps every circle where it touches) and their own bounding box, not the root circle, is
  // fitted to the wide plot: the leaves come out about 18% larger, enough to name the big ones
  root.each(d => { const x = d.x; d.x = d.y; d.y = x })
  const gs = root.children
  const bx0 = Math.min(...gs.map(g => g.x - g.r))
  const bx1 = Math.max(...gs.map(g => g.x + g.r))
  const by0 = Math.min(...gs.map(g => g.y - g.r))
  const by1 = Math.max(...gs.map(g => g.y + g.r))
  const k = Math.min((PLOT.right - PLOT.left) / (bx1 - bx0), (PLOT.bottom - PLOT.top) / (by1 - by0))
  const ox = (PLOT.left + PLOT.right) / 2 - ((bx0 + bx1) / 2) * k
  const oy = (PLOT.top + PLOT.bottom) / 2 - ((by0 + by1) / 2) * k
  root.each(d => { d.x = ox + d.x * k; d.y = oy + d.y * k; d.r *= k })
  const X = d => d.x
  const Y = d => d.y
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  const legends = []

  // A group's label goes on its outline where no other group presses against it: the top, or the
  // bottom when a neighbour sits on the top. The clearance is sampled across the label's box.
  const clear = (g, y, w) => {
    let min = Infinity
    for (const o of root.children) if (o !== g) for (const dx of [-w / 2, 0, w / 2]) for (const dy of [-5, 5]) min = Math.min(min, Math.hypot(g.x + dx - o.x, y + dy - o.y) - o.r)
    return min
  }

  for (const g of root.children) {
    const label = `${g.data.name} ${g.value}`
    const w = label.length * 5 * 1.1
    const top = clear(g, Y(g) - g.r, w)
    const bottom = clear(g, Y(g) + g.r, w)
    const th = top >= 4 || top >= bottom ? -Math.PI / 2 : Math.PI / 2
    // The outline is one arc that stops 3px short of each side of the label, so the gap cuts only the
    // line; a stage-coloured patch was tried first and it also shaved the top leaf and a neighbour's outline
    const a = Math.asin(Math.min(1, (w / 2 + 3) / g.r))
    const [x0, y0] = [X(g) + g.r * Math.cos(th + a), Y(g) + g.r * Math.sin(th + a)]
    const [x1, y1] = [X(g) + g.r * Math.cos(th - a), Y(g) + g.r * Math.sin(th - a)]
    parts.push(`<path d="M${f(x0)} ${f(y0)}A${f(g.r)} ${f(g.r)} 0 1 1 ${f(x1)} ${f(y1)}" fill="none" stroke="${INK}" stroke-opacity="0.18"/>`)
    // Centred on the line but 1.5px outward, which keeps the caps clear of the leaf under them
    legends.push(text(X(g), Y(g) + Math.sin(th) * (g.r + 1.5) + 3, label, { anchor: 'middle' }))
    for (const d of g.children) {
      parts.push(`<circle cx="${f(X(d))}" cy="${f(Y(d))}" r="${f(d.r)}" fill="${INK}" fill-opacity="0.25"/>`)
      // About 5.6px a character at 10px with 10% slack, and 2px of air each side
      const nameW = d.data.name.length * 5.6 * 1.1
      if (2 * d.r < nameW + 4) continue
      const two = d.r >= 15
      parts.push(text(X(d), Y(d) + (two ? -1 : 3.5), d.data.name, { size: 10, fill: INK, anchor: 'middle' }))
      if (two) parts.push(text(X(d), Y(d) + 10, `${d.value} MB`, { anchor: 'middle' }))
    }
  }
  parts.push(...legends)

  // The total sits on the caption line, across from the caption, as in icicle
  const total = `${root.value} MB`
  parts.push(text(W - 12, H - 8, total, { size: 13, fill: INK, anchor: 'end' }))
  parts.push(text(W - 12 - total.length * 7.2 - 6, H - 8, 'TOTAL', { anchor: 'end' }))
  parts.push(text(12, H - 8, 'SAMPLE NODE_MODULES, MB'))
  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: 'circle-pack',
  title: 'Circle packing',
  category: 'parts',
  description: 'Installed packages packed inside their groups, each circle sized by disk space, with names inside where they fit and group totals on the outlines.',
  element: ui => ui.Svg({ source: (source ??= draw()), alt: 'Circle packing of installed packages grouped by purpose and sized by disk space, sample data', width: W, height: H }),
}

Imports d3-hierarchy 3.1.2 (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).