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.


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).