ModsCodeMERGEBLE LLC Search Claude Code Mods

ModsCode / Gallery / Designs / Sankey diagram

Sankey diagram

Where tokens go: each kind of token flows into the tools that spent it, link width by volume, with the largest flow in Claude orange.

Sankey diagramSankey diagram
Flows and links · 380×260 · sample data
hooks/designs/sankey.mjs
// sankey: where a day's tokens go, from four kinds of token (input, cache read, cache write, output) to
// the five things the model spent them on (Read, Edit, Bash, Search, Reply). Nine nodes and fourteen
// links in thousands of tokens: cache read is the big source (1,840K of 2,660K), as it is in a long
// Claude Code session, and its flow into Read (820K, re-reading files) is the one Claude orange flow, the thing
// to notice. The layout is d3-sankey (BSD-3-Clause, Mike Bostock) with its sankeyLinkHorizontal path;
// node order is fixed (nodeSort null) so the kinds keep their reading order. Links are strokes as wide
// as their value at the 14% track ink, so where they cross they build up instead of hiding each other
// (at 25% the 520K cache read to Edit link was one bright slab beside candle); nodes are 6px bars at
// candle's 55% body. The label columns sit outside the link area on both sides, the name at
// 10px and the value under it at 9px, so no text lies over a flow. Sample data only.
import { sankey, sankeyLinkHorizontal } from '../vendor/d3.mjs'

const W = 380
const H = 260
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const CLAUDE = '#D77757'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const PLOT = { left: 88, right: 318, top: 32, bottom: 226 } // the label columns sit outside these edges
const f = n => n.toFixed(1)

// Token kinds first, then tools; the order here is the order on the stage
const NODES = ['Input', 'Cache read', 'Cache write', 'Output', 'Read', 'Edit', 'Bash', 'Search', 'Reply']
// [from, to, thousands of tokens]
const LINKS = [
  ['Input', 'Read', 90], ['Input', 'Search', 70], ['Input', 'Bash', 60], ['Input', 'Edit', 40],
  ['Cache read', 'Read', 820], ['Cache read', 'Edit', 520], ['Cache read', 'Search', 260], ['Cache read', 'Reply', 240],
  ['Cache write', 'Read', 210], ['Cache write', 'Bash', 130], ['Cache write', 'Edit', 80],
  ['Output', 'Edit', 70], ['Output', 'Reply', 50], ['Output', 'Bash', 20],
]
const NOTICE = ['Cache read', 'Read'] // the one Claude orange flow
// The alt names the nodes from the data, so it stays true when the sample changes
const KINDS = NODES.filter(n => LINKS.some(l => l[0] === n))
const TOOLS = NODES.filter(n => !KINDS.includes(n))
const list = a => (a.length > 1 ? `${a.slice(0, -1).join(', ')} and ${a.at(-1)}` : a.join(''))

const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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}">${esc(s)}</text>`
const kTokens = v => (v >= 1000 ? `${(v / 1000).toFixed(2)}M` : `${Math.round(v)}K`)

function draw() {
  const graph = sankey()
    .nodeId(d => d.name)
    .nodeWidth(6)
    .nodePadding(16)
    .nodeSort(null)
    .extent([[PLOT.left, PLOT.top], [PLOT.right, PLOT.bottom]])({
      nodes: NODES.map(name => ({ name })),
      links: LINKS.map(([source, target, value]) => ({ source, target, value })),
    })
  const link = sankeyLinkHorizontal()
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  parts.push(text(PLOT.left - 6, 18, 'TOKEN KIND', { anchor: 'end' }))
  parts.push(text(PLOT.right + 6, 18, 'TOOL'))

  const isNotice = l => l.source.name === NOTICE[0] && l.target.name === NOTICE[1]
  // The Claude orange flow goes last, so it lies on top of the ink flows it crosses
  for (const l of [...graph.links].sort((a, b) => isNotice(a) - isNotice(b))) {
    const look = isNotice(l) ? `stroke="${CLAUDE}" stroke-opacity="0.55"` : `stroke="${INK}" stroke-opacity="0.14"`
    parts.push(`<path d="${link.digits(1)(l)}" fill="none" ${look} stroke-width="${f(Math.max(1, l.width))}"/>`)
  }

  for (const n of graph.nodes) {
    const h = Math.max(1, n.y1 - n.y0)
    parts.push(`<rect x="${f(n.x0)}" y="${f(n.y0)}" width="${f(n.x1 - n.x0)}" height="${f(h)}" fill="${INK}" fill-opacity="0.55"/>`)
    const mid = (n.y0 + n.y1) / 2
    const left = n.depth === 0
    const x = left ? n.x0 - 6 : n.x1 + 6
    const anchor = left ? 'end' : 'start'
    parts.push(text(x, mid - 1, n.name, { size: 10, fill: INK, anchor }))
    parts.push(text(x, mid + 10, kTokens(n.value), { anchor }))
  }

  // The Claude orange flow carries its own value where it is still level, just after it leaves its source
  const hot = graph.links.find(isNotice)
  parts.push(text(hot.source.x1 + 8, hot.y0 + 3, kTokens(hot.value), { fill: INK, size: 10 }))

  const total = graph.nodes.filter(n => n.depth === 0).reduce((s, n) => s + n.value, 0)
  parts.push(text(W - 12, H - 8, `TOTAL ${kTokens(total)}`, { anchor: 'end' }))
  parts.push(text(12, H - 8, 'SAMPLE TOKENS BY KIND AND TOOL'))
  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: 'sankey',
  title: 'Sankey diagram',
  category: 'flow',
  description: 'Where tokens go: each kind of token flows into the tools that spent it, link width by volume, with the largest flow in Claude orange.',
  element: ui => ui.Svg({ source: (source ??= draw()), alt: `Sankey diagram of tokens flowing from ${list(KINDS.map(k => k.toLowerCase()))} into ${list(TOOLS)}, sample data`, width: W, height: H }),
}

Imports d3-sankey 0.12.3 (BSD-3-Clause), 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).