ModsCodeMERGEBLE LLC Search Claude Code Mods

ModsCode / Gallery / Designs / Stacked area chart

Stacked area chart

Daily spend stacked by model in quiet ink steps, each band named at its right end with its total for the period.

Stacked area chartStacked area chart
Over time · 380×212 · sample data
hooks/designs/stacked-area.mjs
// stacked-area: thirty days of API spend split by model, in candle's look. Daily dollars for four
// models from Thu 2026-09-10 to Fri 2026-10-09; Opus climbs through the month and the interactive
// models drop to a third on weekends, so the four weekend dips carry the rhythm. The month sums to
// $278.86: Opus $157, Sonnet $83, Haiku $25, Other $14 (each label is its own rounded total, all
// computed from the dailies), with the busiest day $13.83 on Wed 10-07. Bands step down 0.25 / 0.18 /
// 0.12 / 0.08 ink from the baseline, area fills from the ladder rather than the donut's 0.9 category
// steps or a 0.55 body: a band that big at body strength out-shouts every other tile. A 1px
// stage seam parts the bands and the day total is a 1px line at 85% ink, as in sparks. Straight segments, not a smoothing curve,
// because the points are whole days. Label centres are pushed 14px apart (the row height of an 11px
// value) by a hand-written two-pass dodge. Stacking and paths: d3-shape 3.2.0 (ISC). Sample data only.
import { area, line, stack } from '../vendor/d3.mjs'
const W = 380
const H = 212
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const PLOT = { left: 44, right: 280, top: 40, bottom: 168 }
const DAYS = 30 // Thu 2026-09-10 to Fri 2026-10-09
const f = n => n.toFixed(1)
// Estimated text width: m, w and % run wide, spaces and dots narrow, the rest about 0.56 em
const wide = (s, size) => [...s].reduce((w, c) => w + size * (/[mwMW%]/.test(c) ? 0.85 : /[ .,:il1]/.test(c) ? 0.3 : 0.56), 0)

// A seeded walk, so the picture is the same on every render
function rng(seed) {
  return () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
}

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

// Bottom to top: the biggest spend sits on the baseline and takes the brightest ink step
const MODELS = [
  { key: 'Opus', opacity: 0.25, base: d => 4.6 + (3.2 * d) / (DAYS - 1), weekend: 0.35 },
  { key: 'Sonnet', opacity: 0.18, base: () => 3.3, weekend: 0.4 },
  { key: 'Haiku', opacity: 0.12, base: () => 0.95, weekend: 0.6 },
  { key: 'Other', opacity: 0.08, base: () => 0.45, weekend: 0.9 },
]

// Dollars per model per day; weekends are quieter for the interactive models
function sample() {
  const rnd = rng(20261013)
  const rows = []
  for (let d = 0; d < DAYS; d++) {
    const dow = (4 + d) % 7 // 2026-09-10 is a Thursday; 0 is Sunday
    const weekend = dow === 0 || dow === 6
    const row = {}
    for (const m of MODELS) row[m.key] = Math.round(m.base(d) * (weekend ? m.weekend : 1) * (0.82 + rnd() * 0.36) * 100) / 100
    rows.push(row)
  }
  return rows
}

// Pushes label centres at least `gap` apart inside [lo, hi], keeping their order
function dodge(ys, gap, lo, hi) {
  const order = ys.map((y, i) => [y, i]).sort((a, b) => a[0] - b[0])
  const out = order.map(([y]) => y)
  for (let i = 1; i < out.length; i++) out[i] = Math.max(out[i], out[i - 1] + gap)
  if (out[out.length - 1] > hi) out[out.length - 1] = hi
  for (let i = out.length - 2; i >= 0; i--) out[i] = Math.min(out[i], out[i + 1] - gap)
  if (out[0] < lo) for (let i = 0; i < out.length; i++) out[i] = Math.max(out[i], lo + i * gap)
  const placed = []
  order.forEach(([, idx], k) => (placed[idx] = out[k]))
  return placed
}

function draw() {
  const rows = sample()
  const series = stack().keys(MODELS.map(m => m.key))(rows)
  const totals = MODELS.map(m => rows.reduce((s, r) => s + r[m.key], 0))
  const total = totals.reduce((a, b) => a + b, 0)
  const peak = Math.max(...series[series.length - 1].map(p => p[1]))
  const top = Math.ceil(peak / 5) * 5 // ticks every $5
  const x = d => PLOT.left + ((PLOT.right - PLOT.left) * d) / (DAYS - 1)
  const y = v => PLOT.bottom - ((PLOT.bottom - PLOT.top) * v) / top
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]

  // Header: the month's total, the sum of the four labels
  const hero = '$' + total.toFixed(2)
  parts.push(text(12, 24, hero, { size: 14, fill: INK }))
  parts.push(text(12 + wide(hero, 14) + 8, 24, `SPENT IN ${DAYS} DAYS`))

  // Left scale: 4px stubs, no axis line
  for (let v = 0; v <= top; v += 5) {
    parts.push(`<line x1="${PLOT.left - 4}" x2="${PLOT.left}" y1="${f(y(v))}" y2="${f(y(v))}" stroke="${MUTED}" stroke-width="1"/>`)
    parts.push(text(PLOT.left - 8, y(v) + 3, '$' + v, { anchor: 'end' }))
  }

  const band = area().x((p, i) => x(i)).y0(p => y(p[0])).y1(p => y(p[1])).digits(1)
  const edge = line().x((p, i) => x(i)).y(p => y(p[1])).digits(1)
  series.forEach((s, k) => parts.push(`<path d="${band(s)}" fill="${INK}" fill-opacity="${MODELS[k].opacity}"/>`))
  // A 1px seam in the stage colour on each band's top edge keeps neighbouring steps apart
  series.slice(0, -1).forEach(s => parts.push(`<path d="${edge(s)}" fill="none" stroke="${STAGE}" stroke-width="1"/>`))
  // The day's total is the top edge: sparks' 1px line at 85% ink
  parts.push(`<path d="${edge(series[series.length - 1])}" fill="none" stroke="${INK}" stroke-opacity="0.85" stroke-width="1"/>`)

  // Labels at the right end, beside each band's last midpoint, pushed 14px apart when bands are thin
  const mids = series.map(s => y((s[DAYS - 1][0] + s[DAYS - 1][1]) / 2))
  const ys = dodge(mids, 14, PLOT.top + 4, PLOT.bottom - 2)
  MODELS.forEach((m, k) => {
    const ly = ys[k]
    parts.push(`<rect x="${PLOT.right + 8}" y="${f(ly - 3)}" width="6" height="6" fill="${INK}" fill-opacity="${m.opacity}"/>`)
    parts.push(text(PLOT.right + 18, ly + 3.5, m.key, { size: 10 }))
    parts.push(text(W - 12, ly + 4, '$' + Math.round(totals[k]), { size: 11, fill: INK, anchor: 'end' }))
  })

  // Day ticks: the 10th, 20th and 30th of September, and the last day
  for (const [d, label] of [[0, 'SEP 10'], [10, 'SEP 20'], [20, 'SEP 30'], [29, 'OCT 9']]) {
    parts.push(`<line x1="${f(x(d))}" x2="${f(x(d))}" y1="${PLOT.bottom + 2}" y2="${PLOT.bottom + 6}" stroke="${MUTED}" stroke-width="1"/>`)
    parts.push(text(x(d), PLOT.bottom + 17, label, { anchor: 'middle' }))
  }
  parts.push(text(12, H - 6, 'SAMPLE DAILY SPEND BY MODEL'))
  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: 'stacked-area',
  title: 'Stacked area chart',
  category: 'time',
  description: 'Daily spend stacked by model in quiet ink steps, each band named at its right end with its total for the period.',
  element: ui => ui.Svg({ source: (source ??= draw()), alt: 'Stacked area chart of daily API spend split by model, with each model named and totalled at the right, sample data', width: W, height: H }),
}

Imports d3-shape 3.2.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).