F9 · 梯级瀑布图
瀑布 / 增减分解(≤6 级)的 Lupi Basics 真实参考实现。示例结论为“从毛收入到净收入,逐级拆解”,副标题已经把图例、单位或时间口径写在图旁:一根横档代表 $1k · 实线横档表示增加,空心横档表示扣减 · 梯子随步骤逐级下降
技术档案
| 项目 | 约定 |
|---|---|
| 数据形状 | 瀑布 / 增减分解(≤6 级) |
| 场合 | 财务/复盘 |
| 读者时间 | ~30s |
| 渲染引擎 | SVG |
| 交互与动效 | 滚入视野播放,点击图表重播;数据点带浏览器原生提示 |
| 示例来源行 | RUNG WATERFALL · MONO-BASIC · P&L |
实现与使用边界
- 页面直接复用 gallery 中 “From gross to net, step by step” 的卡片结构和同名渲染块,没有按截图重新绘制。
- 标题、副标题、图形、来源行四件套保持原样;数据编码、比例关系、灰阶和动画节奏由原实现决定。
- 使用时只替换数据与文案,并保持“瀑布 / 增减分解(≤6 级)”这一数据契约;若数据本体改变,应回到总目录重新选型。
- 颜色限于 Mono 灰阶,面积编码继续使用平方根换算;柱长、位置与角度编码不得断轴或视觉夸大。
模板来源:Lieflat Charts
F9 Rung Waterfall,真实骨架取自templates/basics-gallery.html的 “From gross to net, step by step” 卡片与对应渲染块。
f9-rung-waterfall.html —— 自包含单页源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lieflat Charts — F9 梯级瀑布图</title>
<link rel="stylesheet" href="../../../../vendor/fonts/fonts.css">
<style>
:root{--bg:#F0EFEB;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
.card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
.sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
.src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
.badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #B0AFA9;color:#8F8E88}
svg{width:100%;max-height:330px;display:block;margin:0 auto}
svg text{font-family:'Inter',sans-serif}
.pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
@keyframes pop{from{transform:scale(0)}to{transform:none}}
.fade{animation:fade .9s ease both}
@keyframes fade{from{opacity:0}}
.draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
@keyframes draw{to{stroke-dashoffset:0}}
.pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
.pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
.pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
html,body{min-height:100%;margin:0}
body{display:grid;place-items:center;padding:28px}
.pagehead{display:none}
.grid2{display:block;width:min(100%,920px);max-width:920px;margin:0 auto}
.card,.card.wide{display:block;width:100%;min-height:520px;padding:38px 42px 24px}
.card .split{grid-template-columns:minmax(190px,250px) 1fr}
.wiki-code{display:inline-block;margin-bottom:12px;padding:4px 11px;border:1px dashed #B0AFA9;border-radius:99px;color:#8F8E88;font-size:10px;font-weight:700;letter-spacing:.08em}
.card.dark .wiki-code{border-color:#55554F}
@media(max-width:620px){
body{padding:10px}
.card,.card.wide{min-height:500px;padding:26px 18px 20px}
.card .split{display:block}
.card .note,.card .legend{display:none}
}
</style>
</head>
<body>
<main class="grid2">
<div class="card">
<span class="wiki-code">LUPI 基础型 · F9</span>
<span class="badge">LUPI 基础型 · 瀑布</span>
<h2>从毛收入到净收入,逐级拆解</h2>
<div class="sub">一根横档代表 $1k · 实线横档表示增加,空心横档表示扣减 · 梯子随步骤逐级下降</div>
<svg id="fall" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">RUNG WATERFALL · MONO-BASIC · P&L</div>
</div>
</main>
<script>
// ── shared Mono tokens(与 lupi-gallery 同源) ──
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
const NS='http://www.w3.org/2000/svg';
const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
const D2R=Math.PI/180;
const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
const obsReveal=(id,fn)=>{
const n=document.getElementById(id);
const go=()=>{n.innerHTML='';fn(n)};
const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
io.observe(n);
n.style.cursor='pointer';
n.addEventListener('click',go);
};
// ════ C5 · rung waterfall ════
// 瀑布:从毛收入走到净利,每级一把小梯子。加项实档、减项空心档(stroke 描边格)。
// 层与层之间虚线台阶承接。
(()=>{
const D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];
obsReveal('fall',s=>{
const x0=i=>58+i*72,step=5.2,HW=11;
let run=0;
const levels=[];
D.forEach(([name,v],i)=>{levels.push([name,v,run]);if(v!==17||name!=='NET')run+=(name==='GROSS'?v:(name==='NET'?0:v))});
// recompute cleanly: running level after each step
let lv=0;const rows=[];
D.forEach(([name,v],i)=>{
if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}
else if(name==='NET'){rows.push([name,lv,0,lv])}
else{rows.push([name,v,lv+v,lv]);lv=lv+v}
});
const base=252,yOf=k=>base-k*step;
rows.forEach(([name,v,lo,hi],i)=>{
const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v<0&&!isTotal;
const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);
for(let k=0;k<n;k++){
const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;
if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:'#8F8E88','stroke-width':1,
'stroke-dasharray':'2.5 2.5',opacity:.7,class:'fade',
style:`animation-delay:${i*.12+k*.014}s`});
else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1,
opacity:.6+rnd(k+2,i+4)*.4,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});
}
// step hand-off: dashed hairline to the next column's start level
if(i<rows.length-1){
const nx=x0(i+1),yl=yOf(isTotal?v:hi- (neg?0:0));
const lvl=isTotal?v:(neg?lo:hi);
el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:'#C6C5BF',
'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',
style:`animation-delay:${.3+i*.12}s`});
}
const topY=yOf(Math.max(from,to));
const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
fill:neg?'#8F8E88':INK,'text-anchor':'middle',
class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));
tip(num,`${name} — ${neg?'−':''}$${Math.abs(v)}k`);
txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);
});
el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
'SOLID RUNGS ADD · DASHED RUNGS TAKE AWAY · ONE RUNG = $1K');
});
})();
</script>
</body>
</html>