Files
mcn-short-video/project/短视频脚本创作/V1.0/subskills/lieflat-charts/templates/basics-gallery.html
T
maogeigei 5c37e489ea 工作台视频列表页新增点赞/评论趋势折线图(lieflat-charts G8 骨架)
- 引入 lieflat-charts 第三方图表技能到 subskills/(SKILL.md+catalog+templates,已删嵌套 .git)
- ECharts 6.1.0 本地化至 mcn-work-shop/public/vendor/(零 CDN 依赖,离线可用)
- 账号详情视频列表 tab 上方新增趋势图:X=发布时间升序,点赞/评论双序列面积折线
  (G8 Rainfall 骨架:双 grid 独立量级、同 X 联动 pointer、tipLight tooltip、quarticOut 入场)
- 全量视频数据单独拉取(pageSize=500,不随分页变化);numOf 解析 'x.x万' 文本
- V1.0/SKILL.md 登记 lieflat-charts 外部技能依赖
2026-09-02 11:09:12 +08:00

944 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lieflat Charts — Basics Gallery · 17 张基础型参考实现</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
<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}
.card.wide{grid-column:1/-1}
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}
.chartbox{width:100%;height:360px}
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}
</style>
</head>
<body>
<div class="pagehead">
<h1>基础型组(F1–F17)· Lupi 语法 × 基础图型剪影</h1>
<p>柱 / 折线 / 面积 / 环形 / 横条 / 分组 / 堆叠 / 散点 / 瀑布 / 热力 / 进度 / 哑铃 / 矩形树图。远看认得出图型剪影,近看每个单位可数(1 档 = $1k · 1 tick = 1 人 / 1%)。为稀疏聚合数据而设:数据不用多,密度来自单位分解。代码检索方式同 lupi-gallery:按 <code>// ════ 图型名 ════</code> 注释块。</p>
</div>
<div class="grid2">
<!-- B1 · rung bars -->
<div class="card">
<span class="badge">LUPI 基础型 · 柱状</span>
<h2>Revenue by plan, rung by rung</h2>
<div class="sub">one rung = one $k of MRR · the bar is a ladder you can count</div>
<svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">RUNG BARS · MONO-BASIC · BILLING</div>
</div>
<!-- B2 · hairline line -->
<div class="card">
<span class="badge">LUPI 基础型 · 折线</span>
<h2>Thirty days of sign-ups</h2>
<div class="sub">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest</div>
<svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">HAIRLINE LINE · MONO-BASIC · GROWTH</div>
</div>
<!-- B3 · hairline area -->
<div class="card">
<span class="badge">LUPI 基础型 · 面积</span>
<h2>Concurrent users, filled with days</h2>
<div class="sub">one hairline = one day, floor to peak · the area is made of days, not paint</div>
<svg id="hairarea" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">HAIRLINE AREA · MONO-BASIC · LIVE OPS</div>
</div>
<!-- B4 · tick donut -->
<div class="card">
<span class="badge">LUPI 基础型 · 环形</span>
<h2>Where the traffic comes from</h2>
<div class="sub">one tick = one percent · four sources shade the dial · read it like a clock</div>
<svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">TICK DONUT · MONO-BASIC · ACQUISITION</div>
</div>
<!-- C1 · tick rows (hbar) -->
<div class="card">
<span class="badge">LUPI 基础型 · 横向条形</span>
<h2>Six teams, shipped and counted</h2>
<div class="sub">one tick = one release this quarter · the row is a queue, not a bar</div>
<svg id="tickrows" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">TICK ROWS · MONO-BASIC · RELEASE LOG</div>
</div>
<!-- C2 · paired rungs (grouped bar) -->
<div class="card">
<span class="badge">LUPI 基础型 · 分组柱</span>
<h2>This year against last, plan by plan</h2>
<div class="sub">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k</div>
<svg id="pairrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">PAIRED RUNGS · MONO-BASIC · BILLING YoY</div>
</div>
<!-- C3 · stacked rungs -->
<div class="card">
<span class="badge">LUPI 基础型 · 堆叠柱</span>
<h2>Where each region's revenue sits</h2>
<div class="sub">one rung = $1k · three products stack up the ladder, darkest carries the most</div>
<svg id="stackrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">STACKED RUNGS · MONO-BASIC · REVENUE MIX</div>
</div>
<!-- C4 · plumb scatter -->
<div class="card">
<span class="badge">LUPI 基础型 · 散点</span>
<h2>Price against satisfaction, twelve products</h2>
<div class="sub">every dot hangs a plumb line to the floor · read x by the line, y by the height</div>
<svg id="plumb" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">PLUMB SCATTER · MONO-BASIC · PRODUCT PANEL</div>
</div>
<!-- C5 · rung waterfall -->
<div class="card">
<span class="badge">LUPI 基础型 · 瀑布</span>
<h2>From gross to net, step by step</h2>
<div class="sub">one rung = $1k · solid rungs add, hollow rungs take away · the ladder walks down</div>
<svg id="fall" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">RUNG WATERFALL · MONO-BASIC · P&L</div>
</div>
<!-- C6 · dot heat -->
<div class="card">
<span class="badge">LUPI 基础型 · 热力</span>
<h2>When support gets loud</h2>
<div class="sub">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible</div>
<svg id="dotheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">DOT HEAT · MONO-BASIC · SUPPORT DESK</div>
</div>
<!-- C7 · tick gauge -->
<div class="card">
<span class="badge">LUPI 基础型 · 进度</span>
<h2>How far to the quarter's goal</h2>
<div class="sub">one tick = one percent of target · inked = earned · the dial reads left to right</div>
<svg id="gauge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">TICK GAUGE · MONO-BASIC · QUARTERLY TARGET</div>
</div>
<!-- C8 · dumbbell queue -->
<div class="card">
<span class="badge">LUPI 基础型 · 哑铃对比</span>
<h2>Onboarding, before and after the redesign</h2>
<div class="sub">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable</div>
<svg id="dumbbell" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">DUMBBELL QUEUE · MONO-BASIC · UX TIMING</div>
</div>
<!-- C9 · nested treemap -->
<div class="card wide">
<span class="badge">LUPI 基础型 · 矩形树图</span>
<h2>Where the work went</h2>
<div class="sub">1,280 shipped hours · area = effort · parent bands preserve the product hierarchy</div>
<div id="treemap" class="chartbox" role="img" aria-label="Treemap of shipped hours nested by product family"></div>
<div class="src">NESTED TREEMAP · MONO-BASIC · WORK PORTFOLIO</div>
</div>
<!-- F14 · rung histogram -->
<div class="card">
<span class="badge">LUPI 基础型 · 直方图</span>
<h2>Most tickets resolve within six hours</h2>
<div class="sub">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile</div>
<svg id="histo" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">RUNG HISTOGRAM · MONO-BASIC · SUPPORT DESK</div>
</div>
<!-- F15 · tick box -->
<div class="card">
<span class="badge">LUPI 基础型 · 箱线图</span>
<h2>Reply times, boxed by plan</h2>
<div class="sub">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed</div>
<svg id="boxplot" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">TICK BOX · MONO-BASIC · SUPPORT DESK</div>
</div>
<!-- F16 · stream ribbon -->
<div class="card wide">
<span class="badge">LUPI 基础型 · STREAMGRAPH</span>
<h2>Three products trade the same river</h2>
<div class="sub">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows</div>
<svg id="stream" viewBox="0 0 840 300" preserveAspectRatio="xMidYMid meet" style="max-height:300px"></svg>
<div class="src">STREAM RIBBON · MONO-BASIC · PRODUCT ANALYTICS</div>
</div>
<!-- F17 · candlestick -->
<div class="card">
<span class="badge">LUPI 基础型 · K 线</span>
<h2>Six weeks of the token, candle by candle</h2>
<div class="sub">body = open to close · wick = the day's full reach · ink = closed down · hollow = closed up</div>
<svg id="candle" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">CANDLESTICK · MONO-BASIC · MARKET DATA</div>
</div>
</div>
<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);
};
// ════ B1 · rung bars ════
// 柱状图的 Lupi 化:柱身 = 一格格横档,1 档 = 1 个诚实单位。
// 远看是柱状图剪影,近看每一档都数得出来。档长、透明度 rnd 抖动。
(()=>{
const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
obsReveal('rungs',s=>{
const x0=i=>56+i*56,base=266,step=5.6,HW=14;
D.forEach(([name,v],i)=>{
const x=x0(i);
for(let k=0;k<v;k++){
const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1,
opacity:.5+rnd(k+2,i+4)*.5,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
}
const topY=base-(v-1)*step;
const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:INK,'text-anchor':'middle',
class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
tip(num,`${name} — $${v}k MRR`);
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*.08}s`},name);
});
el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');
});
})();
// ════ B2 · hairline line ════
// 折线图的 Lupi 化:barcode 的日历地板(每天一根发丝,无论有没有事)+
// 发丝折线 + 逐日圆点(周末空心)。峰值两个点放大标数。
(()=>{
const N=30;
const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
obsReveal('dayline',s=>{
const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
// calendar floor
for(let d=0;d<N;d++)
el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:'#CFCEC7','stroke-width':.6,
class:'fade',style:`animation-delay:${d*.008}s`});
el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
const vs=Array.from({length:N},(_,d)=>val(d));
// top-2 peaks, apart
const top=[];
for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
if(top.length===2)break;
}
// hairline path
const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
el(s,'path',{d:'M'+pts,fill:'none',stroke:INK,'stroke-width':1,pathLength:1,
class:'draw',style:'animation-duration:1.2s'});
vs.forEach((v,d)=>{
const weekend=d%7===5||d%7===6,big=top.includes(d);
const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1:0,
class:'pop',style:`animation-delay:${.2+d*.03}s`});
tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${1+d*.01}s`,
class:'fade'},Math.round(v));
});
[[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.1em',class:'fade'},m));
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
});
})();
// ════ B3 · hairline area ════
// 面积图的 Lupi 化:填充不是色块,是一天一根发丝立到自己的峰值。
// 面积由日子组成。顶边一根细线收轮廓,峰值日加点标数。
(()=>{
const N=45;
const val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;
obsReveal('hairarea',s=>{
const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;
el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
const vs=Array.from({length:N},(_,d)=>val(d));
const peak=vs.indexOf(Math.max(...vs));
vs.forEach((v,d)=>{
el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),
stroke:d===peak?INK:'#8F8E88','stroke-width':d===peak?1.1:.55,
opacity:d===peak?1:.5+rnd(d+1,7)*.45,
class:'fade',style:`animation-delay:${d*.014}s`});
});
const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
el(s,'path',{d:'M'+pts,fill:'none',stroke:INK,'stroke-width':1.2,pathLength:1,
class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});
const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:INK,
class:'pop',style:'animation-delay:1.2s'});
tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);
txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.3s`,
class:'fade'},Math.round(vs[peak])+'k');
[[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>
txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.1em',class:'fade'},m));
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
'ONE HAIRLINE = ONE DAY, FLOOR TO PEAK');
});
})();
// ════ B4 · tick donut ════
// 环形图的 Lupi 化:ballot tally 的百人队列绕成整圈表盘,1 tick = 1%,
// 四段来源用灰阶 ladder 分段,段间留一格呼吸。中心只放总数和单位说明。
(()=>{
const D=[['ORGANIC',37,INK],['PAID',28,'#55554F'],['REFERRAL',21,'#8F8E88'],['SOCIAL',14,'#B0AFA9']];
obsReveal('tickdonut',s=>{
const cx=200,cy=148,R0=64;
let k0=0;
D.forEach(([name,v,shade],si)=>{
for(let k=0;k<v;k++){
const idx=k0+k,a=idx*3.6-90;
const len=10+rnd(idx+1,si+2)*6;
const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1,
class:'fade',style:`animation-delay:${idx*.012}s`});
if(idx%10===0){
const [dx,dy]=pol(cx,cy,R0-5,a);
el(s,'circle',{cx:dx,cy:dy,r:.8,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${idx*.012}s`});
}
}
// segment label at mid-angle, outside, tied by a dotted hairline
const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:'#C6C5BF','stroke-width':.7,
'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.65+si*.1}s`,
class:'fade'},`${name} · ${v}`);
tip(lab,`${name} — ${v}% of traffic`);
k0+=v;
});
txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:INK,'text-anchor':'middle',
class:'fade',style:'animation-delay:.9s'},'100');
txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
});
})();
// ════ C1 · tick rows ════
// 横向条形:一行 = 一支队伍的发布队列,1 tick = 1 次发布。
// tick 高度/透明度抖动,每 5 根一个点标,行尾大数。
(()=>{
const D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];
obsReveal('tickrows',s=>{
const y0=i=>52+i*44,X0=104,PX=6.9;
D.forEach(([name,v],i)=>{
const y=y0(i);
txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:'#6A6963','text-anchor':'end',
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
class:'fade',style:`animation-delay:${i*.08}s`});
for(let k=0;k<v;k++){
const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:INK,'stroke-width':.9,
opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
}
const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
tip(lab,`${name} — ${v} releases`);
});
txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');
});
})();
// ════ C2 · paired rungs ════
// 分组柱:每类两把梯子并肩,今年实档、去年淡档。差值一眼可数。
(()=>{
const D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];
obsReveal('pairrungs',s=>{
const x0=i=>64+i*66,base=258,step=5.4,HW=10;
D.forEach(([name,was,now],i)=>{
const xa=x0(i)-13,xb=x0(i)+13;
for(let k=0;k<was;k++){
const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;
el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:'#B0AFA9','stroke-width':1,
opacity:.5+rnd(k+2,i+3)*.4,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
}
for(let k=0;k<now;k++){
const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;
el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:INK,'stroke-width':1,
opacity:.6+rnd(k+2,i+8)*.4,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});
}
const topB=base-(now-1)*step;
const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:INK,'text-anchor':'middle',
class:'fade',style:`animation-delay:${.5+i*.08}s`},now);
tip(num,`${name} — $${was}k → $${now}k`);
txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:'#B0AFA9','text-anchor':'middle',
class:'fade',style:`animation-delay:${.5+i*.08}s`},was);
txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}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:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
'FAINT = 2025 · INK = 2026 · ONE RUNG = $1K');
});
})();
// ════ C3 · stacked rungs ════
// 堆叠柱:同一把梯子分三段灰阶,段间空一格呼吸,段值贴段中。
(()=>{
const D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];
const SHADE=[INK,'#8F8E88','#C0BFB8'];
const SEG=['CORE','ADD-ONS','SERVICES'];
obsReveal('stackrungs',s=>{
const x0=i=>72+i*76,base=262,step=5.2,HW=13;
D.forEach(([name,segs],i)=>{
const x=x0(i);let k0=0;
segs.forEach((v,si)=>{
for(let k=0;k<v;k++){
const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;
el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:SHADE[si],'stroke-width':1,
opacity:.6+rnd(k+2,i+si+4)*.4,class:'fade',
style:`animation-delay:${i*.09+(k0+k)*.012}s`});
}
const midY=base-(k0+v/2+si)*step;
const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:SHADE[si]===`#C0BFB8`?'#8F8E88':SHADE[si],
class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);
tip(lab,`${name} ${SEG[si]} — $${v}k`);
k0+=v;
});
const total=segs[0]+segs[1]+segs[2];
txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:INK,'text-anchor':'middle',
class:'fade',style:`animation-delay:${.6+i*.09}s`},total);
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*.09}s`},name);
});
el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
'DARKEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');
});
})();
// ════ C4 · plumb scatter ════
// 散点:每个点垂一根发丝铅垂线到地板,x 读线脚、y 读高度。
// 地板是 barcode 刻度。最好/最差两个点标名。
(()=>{
const P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],
['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],
['Hub',76,32],['Forms',34,38]];
obsReveal('plumb',s=>{
const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;
// barcode floor
for(let g=0;g<=20;g++){
const x=X0+g/20*(X1-X0);
el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:'#CFCEC7','stroke-width':.6,
class:'fade',style:`animation-delay:${g*.01}s`});
}
el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:'#C6C5BF',class:'fade'},'CHEAP');
txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'end',
class:'fade'},'PREMIUM');
const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]<a[2]?b:a);
P.forEach(([name,px,sat],i)=>{
const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];
el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:'#B0AFA9','stroke-width':.55,
opacity:.6,class:'fade',style:`animation-delay:${.2+i*.05}s`});
const dot=el(s,'circle',{cx:x,cy:y,r:hero?4.6:2.6,fill:hero?INK:'#55554F',
class:'pop',style:`animation-delay:${.25+i*.05}s`});
tip(dot,`${name} — price ${px} · satisfaction ${sat}`);
if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:INK,'text-anchor':'middle',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.8}s`,
class:'fade'},`${name} · ${sat}`);
});
txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:'#C6C5BF',
transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');
});
})();
// ════ 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');
});
})();
// ════ C6 · dot heat ════
// 热力图:punch card 的 Lupi 皮。7×12 网格,点面积 = 工单量,
// 静默格留一粒极小点(沉默可见)。最热格标数。
(()=>{
const DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];
obsReveal('dotheat',s=>{
const x0=j=>64+j*27,y0=i=>58+i*30;
let max=0,mi=0,mj=0;
const v=(i,j)=>{
const day=i<5?1:.32; // weekends quiet
const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5); // two peaks
const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);
return Math.round(raw);
};
for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
for(let i=0;i<7;i++){
txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
for(let j=0;j<12;j++){
const t=v(i,j),x=x0(j),y=y0(i);
if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:'#D8D6CE',
class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
const hero=i===mi&&j===mj;
const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
fill:t>max*.66?INK:t>max*.33?'#6A6963':'#B0AFA9',
class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);
if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
stroke:INK,'stroke-width':1,'stroke-dasharray':'2 3',
class:'fade',style:'animation-delay:1s'});
}
}
for(let j=0;j<12;j+=2)
txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
`DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);
});
})();
// ════ C7 · tick gauge ════
// 进度:ballot tick 弯成 210° 表盘,1 tick = 1% 目标。走到 73 上墨收笔。
// 里程碑 25/50/75/100 点标 + 小字。中心大数 + 剩余量小字。
(()=>{
const GOAL=73;
obsReveal('gauge',s=>{
const cx=200,cy=190,R0=104,A0=-195,SW=210; // -195° → +15°
for(let k=0;k<100;k++){
const a=A0+k/100*SW,inked=k<GOAL;
const len=inked?13+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
el(s,'line',{x1,y1,x2,y2,stroke:inked?INK:'#CFCEC7','stroke-width':inked?1:.6,
class:'fade',style:`animation-delay:${k*.012}s`});
}
[25,50,75,100].forEach(m=>{
const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
el(s,'circle',{cx:dx,cy:dy,r:1,fill:'#B0AFA9',class:'fade',style:'animation-delay:.8s'});
txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
class:'fade',style:'animation-delay:.85s'},m);
});
// inked tip bead
const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
el(s,'circle',{cx:ex,cy:ey,r:2.4,fill:INK,class:'pop',style:'animation-delay:1.1s'});
const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:INK,'text-anchor':'middle',
class:'fade',style:'animation-delay:1s'},GOAL+'%');
tip(num,`$730k of the $1M quarter target`);
txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');
txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
'ONE TICK = 1% OF TARGET · INKED = EARNED');
});
})();
// ════ C8 · dumbbell queue ════
// 哑铃对比:空心点=改版前,实心点=改版后,两点之间的轨道上串珠,
// 一珠 = 省下的一分钟,可数。全部向左走(变快),没有交叉没有 X。
(()=>{
const D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];
obsReveal('dumbbell',s=>{
const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);
D.forEach(([name,was,now],i)=>{
const y=y0(i),xa=mapX(was),xb=mapX(now);
txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:'#E3E2DB','stroke-width':.7,
class:'fade',style:`animation-delay:${i*.08}s`});
// beads: one per minute saved, strung between the two dots
const n=was-now;
for(let k=0;k<n;k++){
const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;
el(s,'circle',{cx:x,cy:yy,r:1.5+rnd(k+2,i+4)*.9,fill:'#8F8E88',opacity:.85,
class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});
}
el(s,'circle',{cx:xa,cy:y,r:4.2,fill:PAPER,stroke:INK,'stroke-width':1.3,
class:'pop',style:`animation-delay:${.2+i*.08}s`});
const after=el(s,'circle',{cx:xb,cy:y,r:4.6,fill:INK,
class:'pop',style:`animation-delay:${.6+i*.08}s`});
tip(after,`${name} — ${was} min → ${now} min`);
txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:'#B0AFA9',
class:'fade',style:`animation-delay:${.3+i*.08}s`},was);
txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:INK,'text-anchor':'end',
class:'fade',style:`animation-delay:${.7+i*.08}s`},now);
});
txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:'#C6C5BF',class:'fade'},'FASTER ←');
txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'end',
class:'fade'},'MINUTES');
txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · INK = AFTER');
});
})();
// ════ C9 · nested treemap ════
// 面积只编码工时;Mono 不用随机灰阶区分类目,父级靠标题带和组间留白分开。
(()=>{
const TOTAL=1280,FILL='#55554F';
const leaf=(name,value)=>({name,value,itemStyle:{color:FILL},label:{color:PAPER}});
const DATA=[
{name:'PRODUCT · 50%',children:[leaf('Core app',260),leaf('Collaboration',190),leaf('Search',110),leaf('Mobile',80)]},
{name:'PLATFORM · 31%',children:[leaf('AI systems',170),leaf('Data infra',120),leaf('APIs',105)]},
{name:'GROWTH · 19%',children:[leaf('Acquisition',90),leaf('Retention',80),leaf('Onboarding',75)]},
];
const host=document.getElementById('treemap');
let chart;
const draw=()=>{
chart=chart||echarts.init(host,null,{renderer:'svg'});
chart.clear();
chart.setOption({animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},
tooltip:{backgroundColor:PAPER,borderColor:INK,borderWidth:1,textStyle:{color:INK,fontSize:12},
formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}<br><b>${i.value} hours</b> · ${(i.value/TOTAL*100).toFixed(1)}%`},
series:[{type:'treemap',data:DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,
breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,
label:{show:true,position:'insideTopLeft',padding:[10,8],color:INK,fontSize:12,fontWeight:700,lineHeight:18,
formatter:p=>p.treePathInfo.length<=1?'':p.treePathInfo.length===2?p.name:`${p.name}\n${p.value} h`},
upperLabel:{show:true,height:32,padding:[0,10],color:INK,fontSize:10,fontWeight:700,backgroundColor:PAPER},
itemStyle:{borderColor:PAPER,borderWidth:2,gapWidth:2},
emphasis:{focus:'ancestor',itemStyle:{borderColor:INK,borderWidth:1.5}},
levels:[{itemStyle:{borderWidth:0,gapWidth:5}},
{upperLabel:{show:true,height:32},itemStyle:{borderColor:PAPER,borderWidth:3,gapWidth:3}},
{upperLabel:{show:false},itemStyle:{borderColor:PAPER,borderWidth:2,gapWidth:2}}]}]});
};
const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});
io.observe(host);
host.style.cursor='pointer';
host.addEventListener('click',draw);
addEventListener('resize',()=>chart?.resize());
})();
// ── New chart-family helpers (scoped to the added templates) ──
(()=>{
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;
// ════ F14 · rung histogram ════
// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单
// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,
// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。
(()=>{
const BIN=[6,14,22,19,13,9,6,4,3,2,1,1]; // 2h bins, sums to 100
obsReveal('histo',s=>{
const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;
BIN.forEach((v,i)=>{
const x=X0+i*PW+PW/2;
for(let k=0;k<v;k++){
const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;
el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1,
opacity:.55+rnd(k+2,i+4)*.45,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
}
});
// peak label
const pk=BIN.indexOf(Math.max(...BIN));
const px=X0+pk*PW+PW/2;
const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,
'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);
tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);
// bin edges: the continuous axis is the histogram's identity
el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
for(let e=0;e<=12;e++){
const x=X0+e*PW;
el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'#CFCEC7','stroke-width':.6,
class:'fade',style:`animation-delay:${e*.02}s`});
if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');
}
// median flag: cumulative crosses 50 inside bin 2
let acc=0,mb=0;for(let i=0;i<12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}
const mx=X0+mb*PW+PW*.7;
el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#8F8E88','stroke-width':.9,
'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'#6A6963',
class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
'ONE RUNG = ONE TICKET IN A HUNDRED · BINS OF TWO HOURS · DOT MARKS EVERY FIFTH');
});
})();
// ════ F15 · tick box ════
// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,
// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。
(()=>{
const G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],
['PRO',[.8,2.1,3.3,5,7.8],[10.5]],
['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],
['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];
const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9'];
obsReveal('boxplot',s=>{
const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);
const colX=g=>78+g*84,BW=24;
[0,6,12,18,24].forEach(h=>{
el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'#E3E2DB','stroke-width':.8,
class:'fade',style:`animation-delay:${h*.008}s`});
txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
class:'fade',style:`animation-delay:${h*.008}s`},h+'h');
});
G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{
const x=colX(g);
el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:'#8F8E88','stroke-width':.8,
pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});
[mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),
stroke:'#8F8E88','stroke-width':1,class:'fade',style:`animation-delay:${.2+g*.12}s`}));
const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,
fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});
tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);
el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,
class:'fade',style:`animation-delay:${.5+g*.12}s`});
txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':9.5,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');
outs.forEach((o,k)=>{
const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,
stroke:'#6A6963','stroke-width':1.1,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});
tip(d,`${name} outlier — ${o}h`);
});
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:${g*.12}s`},name);
});
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · HOLLOW = OUTLIER · DARKEST = FASTEST');
});
})();
// ════ F16 · stream ribbon ════
// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴
// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。
// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。
(()=>{
const N=48;
const mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>
Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));
const SERIES=[
['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'#C6C5BF'],
['BOARDS',mk(26,.10,11,4.2,5),'#8F8E88'],
['FLOWS',mk(12,.78,10,3.1,8),INK],
];
obsReveal('stream',s=>{
const X0=36,X1=744,CY=138,SC=1.15;
const x=t=>X0+t/(N-1)*(X1-X0);
const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));
// stacked offsets, silhouette baseline: top of stack starts at -total/2
const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);
const smooth=pts=>{
let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
for(let k=1;k<pts.length-1;k++){
const p=pts[k],q=pts[k+1];
d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
}
return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;
};
let run=y0.slice();
SERIES.forEach(([name,vals,shade],si)=>{
const top=run.map((v,t)=>[x(t),v]);
const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);
run=bot.map(p=>p[1]);
const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';
const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,
class:'fade',style:`animation-delay:${si*.16}s`});
tip(band,`${name} — weekly active accounts, 48 weeks`);
// label at the band's widest week — clamped off both ends so the
// paper-colored text never spills past the band onto paper (invisible)
let wk=4;vals.forEach((v,t)=>{if(t>=4&&t<=N-6&&v>vals[wk])wk=t});
const midY=(top[wk][1]+bot[wk][1])/2;
const dark=shade===INK||shade==='#4A4944';
txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',
fill:dark?PAPER:'#4A4944','letter-spacing':'.06em',
class:'fade',style:`animation-delay:${.5+si*.14}s`},name);
});
// month floor: barcode ticks + labels every 8 weeks
const base=252;
el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
for(let t=0;t<N;t++)
el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'#CFCEC7','stroke-width':.6,
class:'fade',style:`animation-delay:${t*.006}s`});
['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>
txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.1em',class:'fade'},m));
txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
'BAND WIDTH = WEEKLY ACTIVES · DARKEST = TODAY’S LEADER · THE RIVER IS THE TOTAL');
});
})();
// ════ F17 · candlestick ════
// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,
// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。
// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。
(()=>{
const N=30;
// deterministic random walk with a dip-and-recover arc
const DAYS=[];
let px=52;
for(let d=0;d<N;d++){
const drift=d<10?.4:d<18?-1.5:1.3;
const open=px;
const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);
const hi=Math.max(open,close)+rnd(d+2,7)*2.6;
const lo=Math.min(open,close)-rnd(d+3,11)*2.6;
DAYS.push([open,close,hi,lo]);
px=close;
}
obsReveal('candle',s=>{
const X0=40,PW=11.2,BASE=258,TOP=44;
const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));
const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);
const x=d=>X0+d*PW+PW/2;
// price grid
for(let g=0;g<=4;g++){
const v=vmin+g/4*(vmax-vmin);
el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'#E3E2DB','stroke-width':.8,
class:'fade',style:`animation-delay:${g*.03}s`});
txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
class:'fade',style:`animation-delay:${g*.03}s`},'$'+Math.round(v));
}
let hiD=0,loD=0;
DAYS.forEach(([o,c,h,l],d)=>{
if(h>DAYS[hiD][2])hiD=d;
if(l<DAYS[loD][3])loD=d;
});
DAYS.forEach(([o,c,h,l],d)=>{
const cx=x(d),up=c>=o;
const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));
// wick: one hairline, full reach
el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'#6A6963','stroke-width':.7,
class:'fade',style:`animation-delay:${d*.03}s`});
// body: capsule; hollow = up, ink = down
const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,
...(up?{fill:PAPER,stroke:INK,'stroke-width':1.1}:{fill:INK}),
class:'fade',style:`animation-delay:${.05+d*.03}s`});
tip(body,`Day ${d+1} — open $${o.toFixed(1)} · close $${c.toFixed(1)} · high $${h.toFixed(1)} · low $${l.toFixed(1)}`);
// extremes, labeled with hairline flags
if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
class:'fade'},'$'+Math.round(h));
if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'#6A6963','text-anchor':'middle',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
class:'fade'},'$'+Math.round(l));
});
// week ticks on the floor
el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':.8,class:'fade'});
for(let d=0;d<N;d++)
el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'#CFCEC7','stroke-width':.6,
class:'fade',style:`animation-delay:${d*.008}s`});
['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>
txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.08em',class:'fade'},w));
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');
});
})();
})();
</script>
</body>
</html>