| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943 |
- <!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>
|