ModsCode / Gallery / Designs / Waterfall chart
Waterfall chart
A month of cash from opening to closing balance: each rise and fall floats from the last level, joined by thin connectors.


hooks/designs/waterfall.mjs
// waterfall: one month of a small studio's cash in thousands of dollars, in candle's look. The opening
// and closing balances are ink bars from zero at 55%; the eight moves float from the level
// the last one left, UP or DOWN at candle's 65%, in candle's 62% slot, and a 1px connector at 35% ink carries
// each level across the gap to the next bar. Each value sits at its bar's tip (above a rise, below a
// fall, like the largest bar in bars) rather than inside: a 22px bar cannot hold a signed value at 9px.
// The numbers: open 22.0; sales +18.4, API usage -7.8, consulting gigs +9.6, contractor staff -14.5,
// Claude plans -2.4, book sales +2.1, cloud hosting -3.2, tax -6.0; close 18.2, a net fall of 3.8. The
// opening is small next to the moves so they fill the height, and the scale runs from 0 to 48, just past
// the highest level (42.2). Sample data only.
const W = 380
const H = 220
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const UP = '#2C7A39'
const DOWN = '#AB2B3F'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const PLOT = { left: 12, right: 368, top: 20, bottom: 180 }
const SCALE = 48
const OPEN = 22.0
const MOVES = [
['SALES', 18.4], ['API', -7.8], ['GIGS', 9.6], ['STAFF', -14.5],
['PLAN', -2.4], ['BOOK', 2.1], ['CLOUD', -3.2], ['TAX', -6.0],
]
const f = n => n.toFixed(1)
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 signed = v => (v > 0 ? '+' : '−') + Math.abs(v).toFixed(1)
function draw() {
// Each bar runs from `from` to `to`; the totals start at zero
const bars = [{ name: 'OPEN', from: 0, to: OPEN, total: true }]
let level = OPEN
for (const [name, v] of MOVES) {
bars.push({ name, from: level, to: level + v, v })
level += v
}
bars.push({ name: 'CLOSE', from: 0, to: level, total: true })
const slot = (PLOT.right - PLOT.left) / bars.length
const bw = slot * 0.62
const cx = i => PLOT.left + slot * (i + 0.5)
const y = v => PLOT.bottom - ((PLOT.bottom - PLOT.top) * v) / SCALE
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
parts.push(`<line x1="${PLOT.left}" x2="${PLOT.right}" y1="${PLOT.bottom + 0.5}" y2="${PLOT.bottom + 0.5}" stroke="${INK}" stroke-opacity="0.12" stroke-width="1"/>`)
// Connectors first, so a bar edge sits on top where they meet
bars.forEach((b, i) => {
if (i === bars.length - 1) return
const ly = f(y(b.to))
parts.push(`<line x1="${f(cx(i) + bw / 2)}" x2="${f(cx(i + 1) - bw / 2)}" y1="${ly}" y2="${ly}" stroke="${INK}" stroke-opacity="0.35" stroke-width="1"/>`)
})
bars.forEach((b, i) => {
const top = y(Math.max(b.from, b.to))
const h = Math.max(1, Math.abs(y(b.from) - y(b.to)))
const color = b.total ? INK : b.v > 0 ? UP : DOWN
parts.push(`<rect x="${f(cx(i) - bw / 2)}" y="${f(top)}" width="${f(bw)}" height="${f(h)}" fill="${color}" fill-opacity="${b.total ? 0.55 : 0.65}"/>`)
if (b.total) parts.push(text(cx(i), top - 5, b.to.toFixed(1), { size: 11, fill: INK, anchor: 'middle' }))
else if (b.v > 0) parts.push(text(cx(i), top - 4, signed(b.v), { fill: INK, anchor: 'middle' }))
else parts.push(text(cx(i), top + h + 11, signed(b.v), { fill: INK, anchor: 'middle' }))
parts.push(text(cx(i), PLOT.bottom + 14, b.name, { anchor: 'middle' }))
})
const net = level - OPEN
parts.push(`<text x="${W - 12}" y="${H - 8}" text-anchor="end" font-size="9" font-family="${FONT}" fill="${MUTED}">NET <tspan fill="${net >= 0 ? UP : DOWN}">${signed(net)}</tspan></text>`)
parts.push(text(12, H - 8, 'SAMPLE MONTH OF CASH, $K'))
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: 'waterfall',
title: 'Waterfall chart',
category: 'parts',
description: 'A month of cash from opening to closing balance: each rise and fall floats from the last level, joined by thin connectors.',
element: ui => ui.Svg({ source: (source ??= draw()), alt: 'Waterfall chart of a month of cash in thousands, from the opening balance through eight rises and falls to the closing balance, sample data', width: W, height: H }),
}
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).