- 引入 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 外部技能依赖
944 lines
48 KiB
HTML
944 lines
48 KiB
HTML
<!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>
|