ModsCode / Gallery / Designs / Icicle chart
Icicle chart
A web bundle split into group, package and module from left to right, sized by bytes, names where they fit and the largest module marked.


hooks/designs/icicle.mjs
// icicle: what fills a 1,842 KB minified web bundle, laid out left to right in candle's look: group,
// then package or folder, then module, 42 modules in all. The cells come from d3-hierarchy's
// partition() (ISC, Mike Bostock) with its axes swapped, so depth runs across and size runs down. Each
// column is one depth on the same quiet ink steps as sunburst (0.25 / 0.14 / 0.08); cells are inset to
// leave 2px gutters between columns and 1px between neighbours, since 42 modules in 240px average under
// 6px each. A name and its size are drawn only when the cell is tall and wide enough for them. The one
// Claude orange cell is the largest module, katex's core at 176 KB, a math renderer one docs page uses.
// The sizes add up: node_modules 1,108 + src 512 + assets 222 = 1,842 KB. Sample data only.
import { hierarchy, partition } 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 PLOT = { top: 32, bottom: 272 }
// Left and right edge of each depth's column: node_modules and @anthropic-ai/sdk set the first two widths
const COLS = [null, [12, 98], [100, 240], [242, 368]]
const HEAD = ['', 'GROUP', 'PACKAGE', 'MODULE']
const STEP = [0, 0.25, 0.14, 0.08]
const f = n => n.toFixed(1)
// Minified KB; leaves carry size, folders add their children up
const BUNDLE = {
name: 'app.js',
children: [
{ name: 'node_modules', children: [
{ name: 'katex', children: [{ name: 'core', size: 176, accent: true }, { name: 'mhchem', size: 52 }, { name: 'contrib', size: 36 }] },
{ name: 'react-dom', children: [{ name: 'client', size: 132 }, { name: 'server', size: 28 }, { name: 'shared', size: 12 }] },
{ name: 'highlight.js', children: [{ name: 'languages', size: 104 }, { name: 'core', size: 52 }] },
{ name: 'd3', children: [{ name: 'scale', size: 46 }, { name: 'shape', size: 38 }, { name: 'array', size: 26 }, { name: 'hierarchy', size: 18 }] },
{ name: '@anthropic-ai/sdk', children: [{ name: 'resources', size: 61 }, { name: 'core', size: 27 }, { name: 'streaming', size: 18 }, { name: 'lib', size: 12 }] },
{ name: 'zod', children: [{ name: 'core', size: 52 }, { name: 'classic', size: 28 }, { name: 'locales', size: 14 }] },
{ name: 'date-fns', children: [{ name: 'locale', size: 44 }, { name: 'format', size: 18 }] },
{ name: 'lodash-es', size: 57 },
{ name: 'marked', size: 41 },
{ name: 'react', size: 16 },
] },
{ name: 'src', children: [
{ name: 'components', children: [{ name: 'Gallery', size: 64 }, { name: 'Pane', size: 48 }, { name: 'ModCard', size: 36 }, { name: 'Search', size: 30 }, { name: 'Header', size: 18 }] },
{ name: 'routes', children: [{ name: 'mods', size: 46 }, { name: 'designs', size: 41 }, { name: 'docs', size: 36 }, { name: 'home', size: 25 }] },
{ name: 'lib', children: [{ name: 'api', size: 34 }, { name: 'store', size: 28 }, { name: 'hooks', size: 20 }, { name: 'format', size: 14 }] },
{ name: 'styles', children: [{ name: 'theme', size: 48 }, { name: 'tokens', size: 24 }] },
] },
{ name: 'assets', children: [{ name: 'inter.woff2', size: 96 }, { name: 'sprite.svg', size: 90 }, { name: 'logo.svg', size: 36 }] },
],
}
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>`
const kb = n => String(n).replace(/\B(?=(\d{3})+$)/g, ',') // thousands commas without Intl, which is slow to load
function draw() {
const root = hierarchy(BUNDLE).sum(d => d.size || 0).sort((a, b) => b.value - a.value)
partition().size([PLOT.bottom - PLOT.top, 1])(root)
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
for (let i = 1; i < COLS.length; i++) parts.push(text(COLS[i][0] + 6, 22, HEAD[i]))
for (const d of root.descendants()) {
if (d.depth === 0) continue
const [x0, x1] = COLS[d.depth]
const y = PLOT.top + d.x0 + 0.5
const h = Math.max(0.5, d.x1 - d.x0 - 1)
const w = x1 - x0
const fill = d.data.accent ? `fill="${CLAUDE}" fill-opacity="0.55"` : `fill="${INK}" fill-opacity="${STEP[d.depth]}"`
parts.push(`<rect x="${x0}" y="${f(y)}" width="${w}" height="${f(h)}" ${fill}/>`)
// About 5.6px a character at 10px and 5px at 9px, with 10% slack; the size steps aside first
const name = d.data.name
const value = kb(d.value)
const nameW = name.length * 5.6 * 1.1
const valueW = value.length * 5 * 1.1
if (h < 11 || nameW + 12 > w) continue
const valueFill = d.depth === 1 || d.data.accent ? INK : MUTED
if (nameW + valueW + 18 <= w) {
// One line: the name on the left, the size at the right edge
const ty = h >= 26 ? y + 13 : y + h / 2 + 3.5
parts.push(text(x0 + 6, ty, name, { size: 10, fill: INK }))
parts.push(text(x1 - 6, ty, value, { fill: valueFill, anchor: 'end' }))
} else {
// Too narrow for both: the size drops under the name when the cell is tall enough
parts.push(text(x0 + 6, h >= 26 ? y + 13 : y + h / 2 + 3.5, name, { size: 10, fill: INK }))
if (h >= 30) parts.push(text(x0 + 6, y + 25, value, { fill: valueFill }))
}
}
// The total sits on the caption line, across from the caption
const total = `${kb(root.value)} KB`
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 BUNDLE, KB MINIFIED'))
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: 'icicle',
title: 'Icicle chart',
category: 'parts',
description: 'A web bundle split into group, package and module from left to right, sized by bytes, names where they fit and the largest module marked.',
element: ui => ui.Svg({ source: (source ??= draw()), alt: 'Icicle chart of a minified web bundle by group, package and module, 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).