ModsCodeMERGEBLE LLC Search Claude Code Mods

ModsCode / Gallery / Designs / Hexbin plot

Hexbin plot

Thousands of turns binned into hexagons by time and output tokens, brighter where turns pile up, with the busiest cell in Claude orange.

Hexbin plotHexbin plot
Spread · 380×260 · sample data
hooks/designs/hexbin.mjs
// hexbin: 3,000 Claude Code turns as turn time against output tokens, binned into hexagons of radius 8
// in candle's look. Binning is d3-hexbin's hexbin() (d3-hexbin 0.2.2, BSD-3-Clause, Mike Bostock); the
// cells are drawn here as six rounded vertices at radius 7.3, so a hairline of stage runs between
// neighbours the way the heatmap keeps a gap between its squares. Each cell is ink at the heatmap's ramp,
// 0.06 + 0.79 x v, with v the square root of count / max: the quick-edit core holds about eight times
// as many turns a cell as the agent cloud, and a straight ramp left that cloud near the idle step. The
// legend reads the same ramp from LESS to MORE, and the busiest cell is the one Claude orange outline. The sample
// is seeded (20261032) as two clusters: 1,900 quick edits near 10s and 1.2k tokens, and 1,100 agent runs
// near 38s and 4.8k tokens, both sloped (longer turns write more); a draw outside 2 to 58s or 0.2k to 8k
// tokens is drawn again, so the counts stay exact. d3-hexbin lays its grid from the stage's corner, which
// left a row of cells hanging below the zero line onto the tick stubs; here the grid is laid from the
// plot instead (a row centre 8px above zero, a column centre on the left edge), and with those draw
// limits (no real turn is under 2s or 200 tokens) no cell crosses the zero line or either side of the
// plot; the top row may rise above the plot's top but stays 16px inside the stage. Sample data only.
import { hexbin } 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: 46, right: 366, top: 24, bottom: 212 }
const MAX_S = 60
const MAX_K = 8
const KEEP = { s: [2, 58], k: [0.2, 8] } // a draw outside these is redrawn; see the header
const R = 8 // bin radius
const CELL = 7.3 // drawn radius
const f = n => n.toFixed(1)
const comma = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')

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

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>`

const ink = v => (0.06 + 0.79 * v).toFixed(2)
const hex = (cx, cy, r) => {
  let d = ''
  for (let i = 0; i < 6; i++) {
    const a = (Math.PI / 3) * i - Math.PI / 2 // pointy top, as hexbin lays its rows
    d += `${i ? 'L' : 'M'}${f(cx + r * Math.cos(a))} ${f(cy + r * Math.sin(a))}`
  }
  return `${d}Z`
}

const sx = s => PLOT.left + ((PLOT.right - PLOT.left) * s) / MAX_S
const sy = k => PLOT.bottom - ((PLOT.bottom - PLOT.top) * k) / MAX_K

// Seeded turns in px, from [seconds, thousand output tokens]; the third field marks a quick edit
function sample() {
  const rnd = rng(20261032)
  const normal = () => Math.sqrt(-2 * Math.log(1 - rnd())) * Math.cos(2 * Math.PI * rnd())
  const pts = []
  while (pts.length < 3000) {
    const a = normal()
    const b = normal()
    const quick = pts.length < 1900 // a draw outside the plot is redrawn, so each cluster keeps its count
    const p = quick ? [10 + 5.5 * a, 1.2 + 0.35 * a + 0.4 * b] : [38 + 7 * a, 4.8 + 0.7 * a + 0.75 * b]
    if (p[0] > KEEP.s[0] && p[0] < KEEP.s[1] && p[1] > KEEP.k[0] && p[1] < KEEP.k[1]) pts.push([sx(p[0]), sy(p[1]), quick])
  }
  return pts
}

function draw() {
  const pts = sample()
  // Binned in plot-local px so the grid starts at the plot, then moved back to stage px
  const bins = hexbin()
    .radius(R)
    .x(p => p[0] - PLOT.left)
    .y(p => p[1] - PLOT.bottom + R)(pts)
    .map(b => ({ x: b.x + PLOT.left, y: b.y + PLOT.bottom - R, length: b.length }))
  const max = Math.max(...bins.map(b => b.length))
  const peak = bins.find(b => b.length === max)
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  for (const b of bins) parts.push(`<path d="${hex(b.x, b.y, CELL)}" fill="${INK}" fill-opacity="${ink(Math.sqrt(b.length / max))}"/>`)

  // The peak's label steps up out of the cloud: a 1px leader to just above the column's top cell
  const column = bins.filter(b => Math.abs(b.x - peak.x) < R)
  const lift = Math.min(...column.map(b => b.y)) - CELL - 8
  parts.push(`<line x1="${f(peak.x)}" x2="${f(peak.x)}" y1="${f(peak.y - CELL)}" y2="${f(lift)}" stroke="${CLAUDE}" stroke-width="1"/>`)
  parts.push(`<path d="${hex(peak.x, peak.y, CELL - 0.5)}" fill="none" stroke="${CLAUDE}" stroke-width="1"/>`)
  parts.push(text(peak.x, lift - 4, `PEAK ${max}`, { fill: CLAUDE, anchor: 'middle' }))

  // Each cluster is named in the empty space beside it, with the turns it holds
  const quick = pts.filter(p => p[2]).length
  parts.push(text(sx(33), sy(0.8), 'Quick edits', { size: 10, fill: INK }))
  parts.push(text(sx(33), sy(0.8) + 12, `${comma(quick)} TURNS`))
  parts.push(text(sx(27), sy(6.9), 'Agent runs', { size: 10, fill: INK, anchor: 'end' }))
  parts.push(text(sx(27), sy(6.9) + 12, `${comma(pts.length - quick)} TURNS`, { anchor: 'end' }))

  for (const k of [0, 2, 4, 6, 8]) {
    parts.push(`<line x1="${PLOT.left - 12}" x2="${PLOT.left - 8}" y1="${f(sy(k))}" y2="${f(sy(k))}" stroke="${MUTED}" stroke-width="1"/>`)
    parts.push(text(PLOT.left - 15, sy(k) + 3, k ? `${k}k` : '0', { anchor: 'end' }))
  }
  for (const s of [0, 15, 30, 45, 60]) {
    parts.push(`<line x1="${f(sx(s))}" x2="${f(sx(s))}" y1="${PLOT.bottom + 8}" y2="${PLOT.bottom + 12}" stroke="${MUTED}" stroke-width="1"/>`)
    parts.push(text(sx(s), PLOT.bottom + 23, s === MAX_S ? `${s} s` : String(s), { anchor: s === MAX_S ? 'end' : 'middle' }))
  }
  parts.push(text(12, 14, 'OUTPUT TOKENS'))
  parts.push(text(W - 12, PLOT.bottom - 4, 'TURN TIME', { anchor: 'end' }))

  // The legend uses the cells' own ramp, read from idle to busy
  const STEPS = [0, 0.25, 0.5, 0.75, 1]
  const lx = 288
  parts.push(text(lx - 8, H - 8, 'LESS', { anchor: 'end' }))
  STEPS.forEach((v, i) => parts.push(`<path d="${hex(lx + 5 + i * 11, H - 11, 5)}" fill="${INK}" fill-opacity="${ink(v)}"/>`))
  parts.push(text(W - 12, H - 8, 'MORE', { anchor: 'end' }))
  parts.push(text(12, H - 8, 'SAMPLE 3,000 TURNS'))
  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: 'hexbin',
  title: 'Hexbin plot',
  category: 'spread',
  description: 'Thousands of turns binned into hexagons by time and output tokens, brighter where turns pile up, with the busiest cell in Claude orange.',
  element: ui => ui.Svg({ source: (source ??= draw()), alt: 'Hexbin plot of turns by turn time and output tokens in two clusters, quick edits and agent runs, sample data', width: W, height: H }),
}

Imports d3-hexbin 0.2.2 (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).