| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220 |
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <title>Lieflat Charts — Lupi Gallery · 20 张参考实现</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">
- <style>
- :root{--bg:#F0EFEB;--dark:#1C1C1A;--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:1400px;margin:0 auto}
- .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
- .card.dark{background:var(--dark);color:#F0EFEB}
- .card.dark .sub{color:#8F8E88}
- .card.dark .src{color:#55554F}
- .card.wide{grid-column:1/-1}
- .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}
- 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}
- .note{font-size:11px;color:#6A6963;line-height:1.65;margin-bottom:16px}
- .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}
- svg{width:100%;max-height:330px;display:block;margin:0 auto}
- svg text{font-family:'Inter',sans-serif}
- .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
- @keyframes pop{from{transform:scale(0)}to{transform:none}}
- .fade{animation:fade .9s ease both}
- @keyframes fade{from{opacity:0}}
- .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
- @keyframes draw{to{stroke-dashoffset:0}}
- @media (prefers-reduced-motion:reduce){
- .pop,.fade{animation:none}
- .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
- }
- .pagehead{max-width:1400px;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}
- .pagehead .badge{margin:0 2px}
- .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
- padding:3px 10px;border-radius:99px;margin-bottom:10px}
- .badge.del{background:#1C1C1A;color:#F0EFEB}
- .badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}
- .badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}
- .card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}
- .card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}
- .card.dark .badge.pair{border-color:#55554F;color:#8F8E88}
- </style>
- </head>
- <body>
- <div class="pagehead">
- <h1>Lupi 系(细读)· 20 张参考实现</h1>
- <p>发丝线 · 逐记录 · 30 秒阅读。</p>
- </div>
- <div class="grid2">
- <!-- 1 · launch fan -->
- <div class="card">
- <h2>Twelve features, fanned out</h2>
- <div class="sub">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since</div>
- <svg id="fan" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">LAUNCH FAN · MONO-EDITORIAL · PRODUCT ANALYTICS</div>
- </div>
- <!-- 4 · dot cascade on dark -->
- <div class="card dark">
- <h2>What breaks, stacked and ranked</h2>
- <div class="sub">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending</div>
- <svg id="cascade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">DOT CASCADE · MONO-EDITORIAL · INCIDENT LOG</div>
- </div>
- <!-- 2 · barcode lollipop, wide + editorial text column -->
- <div class="card wide">
- <div class="split">
- <div>
- <h2>Ninety days as a barcode</h2>
- <div class="sub">peak concurrent users, daily · Apr – Jun 2026</div>
- <div class="note">Every hairline is a day, whether or not anything
- happened in it. The dot marks the day's peak; the stem below it is
- just gravity. Read the field, not the numbers — the season has a
- texture before it has a value.</div>
- <div class="legend">
- ● WEEKDAY PEAK<br>
- ○ WEEKEND PEAK<br>
- │ ONE CALENDAR DAY<br>
- ◉ TOP-3, LABELED
- </div>
- </div>
- <div>
- <svg id="barcode" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
- <div class="src">BARCODE LOLLIPOP · MONO-EDITORIAL · LIVE OPS</div>
- </div>
- </div>
- </div>
- <!-- 3 · arc bubble matrix -->
- <div class="card">
- <h2>Eight products land in twelve cities</h2>
- <div class="sub">bubble = paying accounts · each row bows like a horizon</div>
- <svg id="arcmatrix" viewBox="0 0 430 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">ARC MATRIX · MONO-EDITORIAL · SALES GEO</div>
- </div>
- <!-- 5 · radial convergence -->
- <div class="card">
- <h2>48 requests pull toward five themes</h2>
- <div class="sub">every rim node = a feature request · lines bundle into the theme that claimed it</div>
- <svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">RADIAL CONVERGENCE · MONO-EDITORIAL · FEEDBACK BOARD</div>
- </div>
- <!-- 6 · cluster field, wide -->
- <div class="card wide">
- <h2>The contributor field</h2>
- <div class="sub">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution</div>
- <svg id="clusters" viewBox="0 0 840 320" preserveAspectRatio="xMidYMid meet" style="max-height:360px"></svg>
- <div class="src">CLUSTER FIELD · MONO-EDITORIAL · CONTRIBUTOR GRAPH</div>
- </div>
- <!-- 1 · brand spectrum -->
- <div class="card">
- <h2>Where the brand sits</h2>
- <div class="sub">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors</div>
- <svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">BRAND SPECTRUM · MONO-EDITORIAL2 · BRAND SURVEY</div>
- </div>
- <!-- 2 · dotty matrix -->
- <div class="card">
- <h2>Four squads, stacked in space</h2>
- <div class="sub">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed</div>
- <svg id="dotty" viewBox="0 0 400 372" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
- <div class="src">DOTTY MATRIX · MONO-EDITORIAL2 · SPRINT BOARD</div>
- </div>
- <!-- 3 · ticket almanac, wide -->
- <div class="card wide">
- <h2>Eight years of tickets, one almanac</h2>
- <div class="sub">bubble area = support tickets per product area, per year · bubbles overlap where years pile up · dark core = escalations · dashed ring = still in beta</div>
- <svg id="almanac" viewBox="0 0 840 400" preserveAspectRatio="xMidYMid meet" style="max-height:420px"></svg>
- <div class="src">BUBBLE ALMANAC · MONO-EDITORIAL2 · SUPPORT DESK</div>
- </div>
- <!-- 4 · deploy patchwork -->
- <div class="card">
- <h2>A quarter of deploys, overlaid</h2>
- <div class="sub">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up</div>
- <svg id="patchwork" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">RADIAL PATCHWORK · MONO-EDITORIAL2 · DEPLOY LOG</div>
- </div>
- <!-- 6 · trend lineage -->
- <div class="card">
- <h2>Features rise, fall, come back</h2>
- <div class="sub">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today</div>
- <svg id="lineage" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">TREND LINEAGE · MONO-EDITORIAL2 · PRODUCT HISTORY</div>
- </div>
- <!-- 8 · type colonnade -->
- <div class="card">
- <h2>Forty-four repos, ten owners</h2>
- <div class="sub">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew</div>
- <svg id="colonnade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">TYPE COLONNADE · MONO-EDITORIAL2 · CODE OWNERSHIP</div>
- </div>
- <!-- 9 · funnel hourglass -->
- <div class="card">
- <h2>The funnel, poured</h2>
- <div class="sub">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort</div>
- <svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">HOURGLASS STREAM · MONO-EDITORIAL2 · GROWTH</div>
- </div>
- <!-- 14 · hundred field -->
- <div class="card">
- <span class="badge pair">小数据 · 单位分解</span>
- <h2>A hundred of us, four minds</h2>
- <div class="sub">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey</div>
- <svg id="hundredfield" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">HUNDRED FIELD · MONO-SMALL · ATTITUDE SURVEY</div>
- </div>
- <!-- 15 · ballot tally -->
- <div class="card">
- <span class="badge pair">小数据 · 单位分解</span>
- <h2>What they fear, tick by tick</h2>
- <div class="sub">one tick = one respondent in a hundred · inked = picked it · they could pick several</div>
- <svg id="ballottally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">BALLOT TALLY · MONO-SMALL · WORKPLACE SURVEY</div>
- </div>
- <!-- L16 · matrix heat -->
- <div class="card">
- <span class="badge">LUPI 编辑型 · 矩阵热力</span>
- <h2>Which features get used together</h2>
- <div class="sub">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair</div>
- <svg id="matheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">MATRIX HEAT · MONO-EDITORIAL · PRODUCT ANALYTICS</div>
- </div>
- <!-- L17 · calendar heat -->
- <div class="card wide">
- <span class="badge">LUPI 编辑型 · 日历热力</span>
- <h2>A year of deploys, day by day</h2>
- <div class="sub">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025</div>
- <svg id="calheat" viewBox="0 0 840 232" preserveAspectRatio="xMidYMid meet" style="max-height:260px"></svg>
- <div class="src">CALENDAR HEAT · MONO-EDITORIAL · DEPLOY LOG</div>
- </div>
- <!-- L18 · beeswarm -->
- <div class="card">
- <span class="badge">LUPI 编辑型 · 蜂群</span>
- <h2>A hundred and twenty deals, swarming</h2>
- <div class="sub">one dot = one closed deal · the swarm piles up where deals cluster · hollow = enterprise · dashed = median</div>
- <svg id="beeswarm" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">BEESWARM · MONO-EDITORIAL · SALES OPS</div>
- </div>
- <!-- L19 · ridgeline -->
- <div class="card">
- <span class="badge">LUPI 编辑型 · RIDGELINE</span>
- <h2>Five pipelines, five tempos</h2>
- <div class="sub">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak</div>
- <svg id="ridge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">RIDGELINE · MONO-EDITORIAL · DATA PLATFORM</div>
- </div>
- <!-- L20 · parallel coordinates -->
- <div class="card">
- <span class="badge">LUPI 编辑型 · 平行坐标</span>
- <h2>Twelve products, four dimensions</h2>
- <div class="sub">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere</div>
- <svg id="parallel" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
- <div class="src">PARALLEL COORDS · MONO-EDITORIAL · PRODUCT PANEL</div>
- </div>
- </div>
- <script>
- // ── shared Mono tokens ──
- 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; // deterministic
- const D2R=Math.PI/180;
- const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
- const sect=(cx,cy,r0,r1,a0,a1)=>{
- const big=a1-a0>180?1:0;
- const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
- const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
- return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
- };
- // unified reveal: draw when scrolled into view; click replays
- 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);
- };
- // ════ 1 · launch fan ════
- // spokes sorted by launch week → the big dots cascade along a diagonal arc
- (()=>{
- const FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];
- const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19]; // week of launch
- const CX=40,CY=304,rOf=w=>56+w*9.7;
- obsReveal('fan',s=>{
- const ang=i=>-84+i*(76/11);
- // dotted week guides
- [5,10,15,20].forEach((w,gi)=>{
- const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
- el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
- stroke:'#D8D6CE','stroke-width':1,'stroke-dasharray':'2 5',
- class:'fade',style:`animation-delay:${gi*.1}s`});
- const [lx,ly]=pol(CX,CY,r,-87.5);
- txt(s,{x:lx,y:ly,'font-size':6.5,fill:'#B0AFA9','text-anchor':'middle',
- class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);
- });
- FEAT.forEach((name,i)=>{
- const a=ang(i),L=LAUNCH[i];
- const mau=Math.round(48-i*3+rnd(i+1,3)*10);
- // hairline spoke from launch to rim
- const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
- el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:'#E3E2DB','stroke-width':.7,
- class:'fade',style:`animation-delay:${.15+i*.05}s`});
- // weekly texture dots
- for(let w=L+1;w<=20;w++){
- const [x,y]=pol(CX,CY,rOf(w),a);
- el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:'#B0AFA9',opacity:.8,
- class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
- }
- // launch dot, sized by MAU
- const [bx,by]=pol(CX,CY,rOf(L),a);
- const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:INK,
- class:'pop',style:`animation-delay:${.15+i*.07}s`});
- tip(big,`${name} — launched W${L} · ${mau}k MAU`);
- // rim label along the spoke
- const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
- txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:'#6A6963',
- transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
- class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
- });
- });
- })();
- // ════ 4 · dot cascade on dark ════
- (()=>{
- const CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];
- const COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];
- obsReveal('cascade',s=>{
- const x=i=>34+i*22.2, base=i=>272-i*7.5;
- // dotted diagonal baseline
- el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,
- stroke:'#4A4944','stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});
- CAUSE.forEach((name,i)=>{
- const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);
- for(let k=0;k<n-1;k++)
- el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:'#B3B0A4',opacity:.9,
- class:'pop',style:`animation-delay:${i*.05+k*.03}s`});
- const topY=by-12-(n-1)*8.8;
- const top=el(s,'circle',{cx:bx,cy:topY,r:4.6,fill:PAPER,
- class:'pop',style:`animation-delay:${i*.05+n*.03}s`});
- tip(top,`${name} — ${COUNT[i]} incidents`);
- txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:PAPER,'text-anchor':'middle',
- class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);
- txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:'#6A6963','text-anchor':'end',
- transform:`rotate(-90 ${bx} ${by+12})`,
- class:'fade',style:`animation-delay:${.1+i*.05}s`},name);
- });
- });
- })();
- // ════ 2 · barcode lollipop ════
- (()=>{
- const N=90;
- const val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;
- obsReveal('barcode',s=>{
- // the barcode: one hairline per day, full height
- for(let d=0;d<N;d++){
- const x=18+d*8.53;
- el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,
- class:'fade',style:`animation-delay:${d*.004}s`});
- }
- const vs=Array.from({length:N},(_,d)=>val(d));
- // top-3 peaks, kept at least 6 days apart so labels never collide
- const top3=[];
- for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
- if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
- if(top3.length===3)break;
- }
- for(let d=0;d<N;d++){
- const x=18+d*8.53,v=vs[d],y=252-v*.75;
- const weekend=d%7===5||d%7===6;
- const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);
- el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:INK,'stroke-width':1.1,
- class:'fade',style:`animation-delay:${.3+d*.008}s`});
- const big=top3.includes(d);
- const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,
- fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1.2:0,
- class:'pop',style:`animation-delay:${.3+d*.008}s`});
- tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);
- if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:INK,'text-anchor':'middle',
- class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');
- }
- [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>
- txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUTED,
- 'letter-spacing':'.12em',class:'fade'},m));
- });
- })();
- // ════ 3 · arc bubble matrix ════
- (()=>{
- const PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];
- const CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];
- const W=[9,8,7,7,6,5,5,4,4,3,3,2]; // market weight
- obsReveal('arcmatrix',s=>{
- const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);
- const v=(i,j)=>{
- if(rnd(i*13+1,j*7+3)<.09)return 0; // a few true absences
- const age=1-i*.085; // older rows reach further
- return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));
- };
- // find top-4 cells for labels
- const all=[];
- PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));
- const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);
- PROD.forEach((p,i)=>{
- // the horizon arc
- const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');
- el(s,'path',{d,fill:'none',stroke:'#E3E2DB','stroke-width':1,pathLength:1,
- class:'draw',style:`animation-delay:${i*.08}s`});
- txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',
- class:'fade',style:`animation-delay:${i*.08}s`},p);
- CITY.forEach((c,j)=>{
- const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);
- if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:'#D8D6CE',
- class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}
- const fill=vv>=25?INK:vv>=12?'#6A6963':'#B0AFA9';
- const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,
- class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});
- tip(dot,`${p} · ${c} — ${vv} accounts`);
- if(top.includes(i*100+j))
- txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:INK,
- 'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);
- });
- });
- CITY.forEach((c,j)=>{
- const x=colX(j),y=74+dy(j)-24;
- txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.08em',
- transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);
- });
- });
- })();
- // ════ 5 · radial convergence ════
- (()=>{
- const N=48,CX=200,CY=162,R=116;
- const THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];
- obsReveal('converge',s=>{
- // hub assignment: contiguous blocks with a little organic leakage
- const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
- const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
- const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
- const counts=[0,0,0,0,0];
- for(let i=0;i<N;i++)counts[hubOf(i)]++;
- for(let i=0;i<N;i++){
- const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
- const h=hubOf(i),[hx,hy]=hubPt(h);
- const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
- const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
- el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
- fill:'none',stroke:'#A8A7A0','stroke-width':.7,opacity:.55,pathLength:1,
- class:'draw',style:`animation-delay:${i*.018}s`});
- el(s,'circle',{cx:px,cy:py,r:1.6,fill:'#6A6963',
- class:'pop',style:`animation-delay:${i*.018}s`});
- // tiny rim code, flipped on the left half so it stays readable
- const flip=deg>90&°<270,[lx,ly]=pol(CX,CY,R+7,deg);
- txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUTED,
- 'text-anchor':flip?'end':'start','dominant-baseline':'middle',
- transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
- class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
- }
- THEME.forEach(([name,,deg],h)=>{
- const [hx,hy]=hubPt(h);
- const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:INK,
- class:'pop',style:`animation-delay:${.5+h*.08}s`});
- tip(hub,`${name} — ${counts[h]} requests`);
- // theme label parked outside the rim, past the code ring
- const [tx,ty]=pol(CX,CY,R+34,deg);
- txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
- 'dominant-baseline':'middle','letter-spacing':'.08em',
- style:`animation-delay:${.6+h*.08}s`,
- class:'fade'},`${name} · ${counts[h]}`);
- // hairline tying the label to its hub, skimming past the rim
- const [gx,gy]=pol(CX,CY,R+22,deg);
- el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:'#C6C5BF','stroke-width':.7,
- 'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
- });
- });
- })();
- // ════ 6 · cluster field ════
- (()=>{
- const SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];
- obsReveal('clusters',s=>{
- const HX=300,HY=172;
- // dotted cross-contribution paths, hub → each island
- SAT.forEach(([x,y],ci)=>{
- const mx=(HX+x)/2,my=(HY+y)/2-30;
- el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',
- stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,
- class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});
- });
- // main hub: 80 golden-angle spokes
- for(let i=0;i<80;i++){
- const a=i*137.508,len=28+rnd(i+1,7)*88;
- const [x,y]=pol(HX,HY,len,a);
- el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:'#C6C5BF','stroke-width':.6,pathLength:1,
- class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});
- const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:INK,opacity:.85,
- class:'pop',style:`animation-delay:${.15+i*.012}s`});
- tip(dot,`core contributor #${i+1}`);
- }
- el(s,'circle',{cx:HX,cy:HY,r:6.5,fill:INK,class:'pop'});
- txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
- 'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px`,
- class:'fade'},'CORE · 80');
- // satellite islands
- SAT.forEach(([x,y,name,n],ci)=>{
- el(s,'circle',{cx:x,cy:y,r:3.5,fill:'#4A4944',
- class:'pop',style:`animation-delay:${.9+ci*.12}s`});
- for(let k=0;k<n;k++){
- const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;
- const [dx,dy]=pol(x,y,rr,a);
- if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:'#D8D6CE','stroke-width':.5,
- class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});
- el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:'#6A6963',
- class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});
- }
- txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
- 'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px`,
- class:'fade'},`${name} · ${n}`);
- });
- });
- })();
- // ════ 1 · brand spectrum ════
- // paired opposites; the pale ribbon threads our position down the page
- (()=>{
- const ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];
- const US=[.74,.80,.40,.22]; // 0 = left trait, 1 = right trait
- const COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];
- const X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);
- obsReveal('spectrum',s=>{
- // the ribbon first, underneath everything
- let d=`M${px(US[0])} ${Y(0)}`;
- for(let i=1;i<4;i++)
- d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;
- el(s,'path',{d,fill:'none',stroke:'#F0EFEB','stroke-width':30,'stroke-linecap':'round',
- 'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
- style:'animation-duration:1.4s'});
- ROWS.forEach(([a,b],i)=>{
- const y=Y(i);
- el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:'#B0AFA9','stroke-width':1,
- class:'fade',style:`animation-delay:${i*.08}s`});
- [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:'#B0AFA9','stroke-width':1,
- class:'fade',style:`animation-delay:${i*.08}s`}));
- txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',
- 'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
- txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963',
- 'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
- COMP[i].forEach((t,k)=>{
- const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:'#8F8E88',
- class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});
- tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);
- });
- const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:INK,
- class:'pop',style:`animation-delay:${.5+i*.1}s`});
- tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);
- });
- txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.12em',
- class:'fade',style:'animation-delay:.9s'},'● US ● COMPETITORS A · B · C');
- });
- })();
- // ════ 2 · dotty matrix — isometric stacked dot planes ════
- // pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:
- // each plane reads as its own quiet grid instead of a smudge
- (()=>{
- const SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];
- const SHADE=['#C6C5BF','#A8A7A0','#6A6963',INK];
- const P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];
- obsReveal('dotty',s=>{
- for(let k=0;k<4;k++){
- // plane slab: paper fill so upper decks occlude lower ones
- const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];
- el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',
- fill:PAPER,'fill-opacity':.96,stroke:'#DEDDD6','stroke-width':.9,
- class:'fade',style:`animation-delay:${k*.16}s`});
- for(let r=0;r<6;r++)for(let c=0;c<6;c++){
- const load=rnd(k*37+r*6+c+1,k+2);
- const v=load<.3?0:Math.round(load*12); // sparser: silence is part of the texture
- const [x,y]=P(c,r,k);
- if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:'#D8D6CE',
- class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}
- const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],
- class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});
- tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);
- }
- // label off the plane's right corner, tied by a short hairline
- const [cxr,cyr]=P(5.8,-.8,k);
- el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:'#C6C5BF','stroke-width':.8,
- class:'fade',style:`animation-delay:${.2+k*.16}s`});
- txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?INK:'#8F8E88',
- 'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);
- }
- });
- })();
- // ════ 3 · bubble almanac ════
- // the vintage-almanac feel lives in three things: ledger-paper hairlines
- // under everything, bubbles big enough to collide, and marginalia. do all three.
- (()=>{
- const AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];
- const BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025}; // beta until this year
- const BORN={EDITOR:2019,BOARDS:2019,DOCS:2019,CHAT:2020,FLOWS:2020,VAULT:2021,PAGES:2021,SYNC:2022,GRID:2022,VIEWS:2023,HUB:2024,FORMS:2024};
- obsReveal('almanac',s=>{
- const rowY=i=>62+i*34,colX=j=>132+j*57;
- // ledger paper: hairlines every 6.8px, edge to edge, under everything
- for(let y=40;y<=318;y+=6.8)
- el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:'#E4E3DC','stroke-width':.5,
- class:'fade',style:`animation-delay:${(y-40)*.001}s`});
- const vals=[];
- for(let i=0;i<8;i++){
- const yr=2019+i;
- el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:'#D3D2CA','stroke-width':.9,
- class:'fade',style:`animation-delay:${i*.04}s`});
- txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:'#8F8E88','text-anchor':'end',
- class:'fade',style:`animation-delay:${i*.04}s`},yr);
- AREAS.forEach((a,j)=>{
- if(yr<BORN[a])return;
- const age=yr-BORN[a];
- // steep spread: quiet areas stay pinpricks, busy ones balloon
- const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
- vals.push([Math.max(1,v),i,j,a,yr]);
- });
- }
- // draw small over large so the giants sit underneath as washes
- vals.sort((a,b)=>b[0]-a[0]);
- const top3=vals.slice(0,3);
- // hand-drawn circle: radius wobbles around the rim on two slow sine waves,
- // so every bubble is its own slightly-off print — never a compass circle
- const blob=(x,y,r,seed)=>{
- const n=Math.max(14,Math.round(r*1.6)),pts=[];
- for(let t=0;t<n;t++){
- const a=t/n*Math.PI*2;
- const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
- pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
- }
- let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
- for(let t=0;t<n;t++){
- const p=pts[t],q=pts[(t+1)%n];
- 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+' Z';
- };
- vals.forEach(([v,i,j,a,yr],k)=>{
- const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10, // jitter off the gridline
- y=rowY(i),r=Math.sqrt(v)*3.8; // big: rows are 34 apart, giants reach ~28 and collide
- const beta=BETA[a]&&yr<=BETA[a];
- const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
- const seed=i*12+j;
- const dot=el(g,'path',beta
- ?{d:blob(x,y,r,seed),fill:'none',stroke:'#8F8E88','stroke-width':1,'stroke-dasharray':'3 3'}
- :{d:blob(x,y,r,seed),fill:INK,'fill-opacity':.09+rnd(i+2,j+4)*.1,
- stroke:INK,'stroke-opacity':.3+rnd(i+5,j+7)*.35,'stroke-width':.6+rnd(i+3,j+11)*.9});
- tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);
- // escalation core: small wobbly blot, nudged off-center like a press mark
- if(!beta){
- const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
- el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:INK});
- }
- if(top3.includes(vals[k]))
- txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:INK,'text-anchor':'middle',
- class:'fade',style:'animation-delay:1s'},v*10);
- });
- AREAS.forEach((a,j)=>{
- const x=colX(j);
- txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
- transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
- });
- // marginalia: two annotations with pointer hairlines, like the original's notes
- el(s,'path',{d:`M${colX(3)+30} ${rowY(5)-14} C${colX(3)+70} ${rowY(5)-34} ${colX(3)+96} ${rowY(5)-38} ${colX(3)+118} ${rowY(5)-38}`,
- fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
- txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:'#6A6963','font-style':'italic',
- class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');
- el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
- fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
- txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:'#6A6963','font-style':'italic',
- class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');
- // footer strip: important events, the almanac's timeline shelf
- const evY=356;
- el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:'#C6C5BF','stroke-width':.8,
- class:'fade',style:'animation-delay:1.2s'});
- txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:'#8F8E88','letter-spacing':'.16em',
- class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');
- [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]
- .forEach(([yr,note],k)=>{
- const x=90+k*196;
- el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:'#8F8E88','stroke-width':1,
- class:'fade',style:`animation-delay:${1.3+k*.08}s`});
- txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:INK,
- class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
- txt(s,{x,y:evY+15,'font-size':7,fill:'#8F8E88',
- class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
- });
- });
- })();
- // ════ 4 · radial patchwork ════
- (()=>{
- const CX=200,CY=168,N=46;
- obsReveal('patchwork',s=>{
- // 24h rim ticks
- for(let h=0;h<96;h++){
- const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
- el(s,'line',{x1,y1,x2,y2,stroke:'#C6C5BF','stroke-width':h%4===0?1:.5,
- class:'fade',style:`animation-delay:${h*.006}s`});
- }
- [0,6,12,18].forEach(h=>{
- const [x,y]=pol(CX,CY,157,-90+h*15);
- txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUTED,'text-anchor':'middle',
- class:'fade'},String(h).padStart(2,'0'));
- });
- // the patchwork: translucent wedges pile into ink
- const wedges=[];
- for(let i=0;i<N;i++){
- // deploys cluster around 11h and 16h, thin overnight
- const peak=rnd(i+1,2)>.5?11:16;
- const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;
- const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
- const r1=34+rnd(i+1,5)*100;
- wedges.push([a0,sw,r1,hour,i]);
- }
- wedges.forEach(([a0,sw,r1,hour,i])=>{
- const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:INK,
- 'fill-opacity':.07+rnd(i+1,6)*.09,class:'fade',
- style:`animation-delay:${.2+i*.022}s`});
- tip(w,`deploy #${i+1} — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')} · ${Math.round(r1*6)} files`);
- });
- // three outlined wedges: the ones that paged somebody
- [4,17,31].forEach((i,k)=>{
- const [a0,sw,r1]=wedges[i];
- const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
- stroke:INK,'stroke-width':1.1,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
- tip(w,`deploy #${i+1} — triggered an incident`);
- });
- el(s,'circle',{cx:CX,cy:CY,r:3,fill:INK,class:'pop',style:'animation-delay:1.3s'});
- txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
- 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');
- });
- })();
- // ════ 6 · trend lineage ════
- (()=>{
- // [name, events [year, type f|o], aliveToday]
- const TR=[
- ['Dark mode',[[2017,'f'],[2022,'o']],true],
- ['Kanban',[[2016,'f']],true],
- ['Wikis',[[2016,'f'],[2019,'o']],false],
- ['Slash cmds',[[2018,'f']],true],
- ['Templates',[[2019,'f'],[2024,'o']],true],
- ['Inbox',[[2020,'f']],false],
- ['Live cursors',[[2020,'f'],[2023,'o']],true],
- ['AI drafts',[[2023,'f'],[2025,'o']],true],
- ['Voice notes',[[2021,'f']],false],
- ['Offline',[[2018,'f'],[2026,'o']],true],
- ];
- const Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;
- obsReveal('lineage',s=>{
- for(let yr=Y0;yr<=2026;yr++){
- el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:'#E3E2DB','stroke-width':.8,
- class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
- if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUTED,'text-anchor':'end',
- class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
- }
- TR.forEach(([name,ev,alive],c)=>{
- const x=colX(c);
- // segments between events: solid while fresh, dashed once dormant
- for(let k=0;k<ev.length-1;k++){
- const gap=ev[k+1][0]-ev[k][0];
- el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
- stroke:'#B0AFA9','stroke-width':1,
- 'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
- class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
- }
- const last=ev[ev.length-1][0];
- if(alive)
- el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:'#B0AFA9','stroke-width':1,
- pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
- ev.forEach(([yr,t])=>{
- const node=el(s,'circle',t==='f'
- ?{cx:x,cy:yY(yr),r:5.5,fill:INK}
- :{cx:x,cy:yY(yr),r:5.5,fill:PAPER,stroke:INK,'stroke-width':1.4});
- node.setAttribute('class','pop');
- node.style.animationDelay=(.35+c*.07)+'s';
- tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);
- });
- // terminal dot: alive lands on today's baseline, dead stops where it stopped
- el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
- fill:alive?INK:'#C6C5BF',class:'pop',style:`animation-delay:${.6+c*.07}s`});
- const ly=yY(2026)+26;
- txt(s,{x,y:ly,'font-size':6.8,'font-weight':600,fill:alive?'#4A4944':'#B0AFA9',
- transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',
- class:'fade',style:`animation-delay:${.7+c*.05}s`},name);
- });
- });
- })();
- // ════ 8 · type colonnade ════
- (()=>{
- const PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];
- const SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];
- const TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];
- obsReveal('colonnade',s=>{
- const N=44;
- const teamY=j=>36+j*28,repoY=i=>16+i*6.7;
- const counts=Array(10).fill(0);
- const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard
- for(let i=0;i<N;i++)counts[owner(i)]++;
- for(let i=0;i<N;i++){
- const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');
- const yi=repoY(i),j=owner(i),yj=teamY(j);
- txt(s,{x:112,y:yi+2,'font-size':5,fill:'#8F8E88','text-anchor':'end',
- class:'fade',style:`animation-delay:${i*.012}s`},name);
- el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:'#B0AFA9',
- class:'fade',style:`animation-delay:${i*.012}s`});
- el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',
- stroke:'#A8A7A0','stroke-width':.6,opacity:.6,pathLength:1,
- class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});
- }
- TEAM.forEach((t,j)=>{
- const y=teamY(j);
- el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:counts[j]>=7?INK:counts[j]>=4?'#6A6963':'#B0AFA9',
- class:'pop',style:`animation-delay:${.6+j*.05}s`});
- const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:'#4A4944',
- class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);
- tip(lab,`${t} owns ${counts[j]} repos`);
- });
- });
- })();
- // ════ 9 · hourglass stream ════
- (()=>{
- const STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];
- obsReveal('hourglass',s=>{
- const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
- STAGE.forEach(([name,c],k)=>{
- const y=sy(k),hw=w(c)/2;
- // barcode strip: one tick ≈ 40 people
- const n=Math.round(c/40);
- for(let t=0;t<n;t++){
- const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
- el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:INK,'stroke-width':.8,
- opacity:.45+rnd(t+2,k+5)*.5,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
- }
- // threads trickling to the next strip
- if(k<4){
- const hw1=w(STAGE[k+1][1])/2;
- for(let t=0;t<34;t++){
- const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
- const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
- el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
- fill:'none',stroke:'#B0AFA9','stroke-width':.5,opacity:.32,pathLength:1,
- class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
- }
- // conversion between stages, parked in the left margin
- const pct=Math.round(STAGE[k+1][1]/c*100);
- txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':8.5,'font-weight':800,fill:'#8F8E88',
- class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
- txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:'#C6C5BF',
- 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');
- }
- // stage label, tied to the strip edge
- el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:'#DEDDD6','stroke-width':.8,
- class:'fade',style:`animation-delay:${.3+k*.12}s`});
- txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:'#4A4944',
- 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
- txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:INK,
- class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
- });
- });
- })();
- // ════ 14 · hundred field ════
- // small data, unit-decomposed: four shares of 100% become 100 people.
- // phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.
- (()=>{
- const SEG=[['CHARGED',41,INK],['TORN',35,'#55554F'],['ADRIFT',12,'#8F8E88'],['AVERSE',12,'#B0AFA9']];
- const POS=[[132,140],[276,116],[186,252],[322,238]];
- obsReveal('hundredfield',s=>{
- // dashed constellation hairlines tying the cluster cores
- [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
- el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
- stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',
- class:'fade',style:`animation-delay:${.9+k*.1}s`});
- });
- SEG.forEach(([name,v,shade],ci)=>{
- const [cx,cy]=POS[ci];
- let edge=0;
- for(let k=0;k<v;k++){
- const a=k*137.508+ci*55;
- const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
- edge=Math.max(edge,rr);
- const [x,y]=pol(cx,cy,rr,a);
- // hairline spoke for every 5th person, core to dot
- if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:'#CDCCC5','stroke-width':.6,
- class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
- const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,
- class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
- tip(dot,`${name} — one of ${v} in 100`);
- }
- el(s,'circle',{cx,cy,r:2.4,fill:INK,class:'pop',style:`animation-delay:${ci*.14}s`});
- txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
- 'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
- class:'fade'},`${name} · ${v}`);
- });
- txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
- 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');
- });
- })();
- // ════ 15 · ballot tally ════
- // multi-select survey (each option independent, 0–100): every option is a row of
- // 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.
- (()=>{
- const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
- obsReveal('ballottally',s=>{
- ASK.forEach(([name,v],i)=>{
- const base=78+i*62;
- txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:'#6A6963',
- 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
- el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
- class:'fade',style:`animation-delay:${i*.1}s`});
- for(let k=0;k<100;k++){
- const x=28+k*3.44,picked=k<v;
- const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
- el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
- stroke:picked?INK:'#CFCEC7','stroke-width':picked?.9:.55,
- class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
- if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',
- class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
- }
- const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':11,'font-weight':800,fill:INK,
- style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+i*.1}s`,
- class:'fade'},v);
- tip(lab,`${v} of 100 picked this — they could pick several`);
- });
- txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
- 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
- });
- })();
- // ── 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;
- // ════ L16 · matrix heat ════
- // 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,
- // 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。
- // 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,
- // 值是占比/强度而不是可数件数。
- (()=>{
- const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];
- const v=(i,j)=>{
- if(i===j)return -1;
- const a=Math.min(i,j),b=Math.max(i,j);
- const pop=(8-a)/8*(8-b)/8; // 头部功能重合度高
- const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);
- return Math.round(raw);
- };
- const shade=t=>t>36?INK:t>24?'#4A4944':t>14?'#8F8E88':t>6?'#B0AFA9':'#D8D7D1';
- obsReveal('matheat',s=>{
- const X0=118,Y0=70,P=26,C=22;
- let max=0,mi=0,mj=0;
- for(let i=0;i<8;i++)for(let j=0;j<8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
- FEAT.forEach((f,i)=>{
- txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'#6A6963','text-anchor':'end',
- 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);
- const cy=Y0-10;
- txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',
- transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);
- });
- for(let i=0;i<8;i++)for(let j=0;j<8;j++){
- const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;
- if(t<0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'#D8D6CE',
- class:'fade',style:`animation-delay:${d}s`});continue}
- if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'#D8D6CE',
- class:'pop',style:`animation-delay:${d}s`});continue}
- const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),
- class:'pop',style:`animation-delay:${d}s`});
- tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);
- if(i===mi&&j===mj){
- el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',
- stroke:INK,'stroke-width':1,'stroke-dasharray':'2 3',
- class:'fade',style:'animation-delay:.9s'});
- txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',
- class:'fade',style:'animation-delay:1s'},t);
- }
- }
- // shade legend, countable buckets
- const LG=[['#D8D7D1','1–6'],['#B0AFA9','7–14'],['#8F8E88','15–24'],['#4A4944','25–36'],[INK,'37+']];
- LG.forEach(([c,lab],k)=>{
- const x=118+k*54;
- el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});
- txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#8F8E88',
- class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');
- });
- txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.1em',
- class:'fade',style:'animation-delay:1s'},'SHADE = %');
- });
- })();
- // ════ L17 · calendar heat ════
- // 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,
- // 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,
- // 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。
- (()=>{
- const MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
- const DAY=['MON','WED','FRI','SUN'];
- const val=(w,d)=>{
- const wd=d<5;
- const season=1+.55*Math.sin((w-8)/9); // 春秋两个发布季
- if(!wd)return rnd(w*7+d+1,3)>.82?1:0; // 周末基本静默
- const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);
- return Math.round(raw);
- };
- obsReveal('calheat',s=>{
- const X0=56,Y0=42,P=14.8;
- let max=0,mw=0,md=0;
- for(let w=0;w<52;w++)for(let d=0;d<7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}
- DAY.forEach((lab,k)=>{
- txt(s,{x:X0-10,y:Y0+[0,2,4,6][k]*P+3,'font-size':6.5,'font-weight':700,fill:'#8F8E88','text-anchor':'end',
- 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);
- });
- MON.forEach((m,k)=>{
- const x=X0+Math.round(k*52/12)*P;
- txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
- class:'fade',style:`animation-delay:${k*.03}s`},m);
- el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'#C6C5BF','stroke-width':.7,
- class:'fade',style:`animation-delay:${k*.03}s`});
- });
- for(let w=0;w<52;w++)for(let d=0;d<7;d++){
- const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;
- if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'#D8D6CE',
- class:'pop',style:`animation-delay:${delay}s`});continue}
- const r=1.1+Math.sqrt(t)*1.55;
- const dot=el(s,'circle',{cx:x,cy:y,r,
- fill:t>max*.66?INK:t>max*.33?'#6A6963':'#B0AFA9',
- class:'pop',style:`animation-delay:${delay}s`});
- tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);
- }
- // peak: dashed ring + marginalia hairline
- const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;
- el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:INK,'stroke-width':1,
- 'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});
- el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,
- fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
- txt(s,{x:px+78,y:py+39,'font-size':7,fill:'#6A6963','font-style':'italic',
- class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);
- txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
- 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
- 'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');
- });
- })();
- // ════ L18 · beeswarm ════
- // 蜂群:violin 的 Lupi 姊妹。不聚合密度,把 120 条真实记录沿数值轴
- // 逐颗排开,撞上了就往两边让——堆得高的地方就是分布的峰。
- // 空心 = enterprise 单,虚线 = 中位数。每颗点背后是一条真实成交。
- (()=>{
- const N=120;
- const deals=Array.from({length:N},(_,i)=>{
- const u=rnd(i+1,3),ent=rnd(i+2,11)>.86;
- const v=Math.round(4+(ent?60:6)+150*Math.pow(u,2.6)+rnd(i+3,7)*10);
- return [Math.min(178,v),ent];
- });
- obsReveal('beeswarm',s=>{
- const X0=36,X1=376,BASE=246,R=4,GAP=R*2+.8;
- const mapX=v=>X0+v/180*(X1-X0);
- // axis rail + barcode ticks
- el(s,'line',{x1:X0-4,y1:BASE,x2:X1+4,y2:BASE,stroke:GRID,'stroke-width':.8,class:'fade'});
- for(let g=0;g<=9;g++){
- const x=X0+g/9*(X1-X0);
- el(s,'line',{x1:x,y1:BASE,x2:x,y2:BASE+(g%3===0?7:4),stroke:'#CFCEC7','stroke-width':.6,
- class:'fade',style:`animation-delay:${g*.02}s`});
- if(g%3===0)txt(s,{x,y:BASE+18,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
- class:'fade',style:`animation-delay:${g*.02}s`},'$'+Math.round(g/9*180)+'k');
- }
- // swarm as clean stacks: x snaps to a GAP-wide lane, dots pile upward —
- // the pile height IS the density. no half-overlaps, every dot countable.
- const lanes={};
- const sorted=deals.map((d,i)=>[d[0],d[1],i]).sort((a,b)=>a[0]-b[0]);
- sorted.forEach(([v,ent,i])=>{
- const lane=Math.round((mapX(v)-X0)/GAP);
- const x=X0+lane*GAP;
- const row=lanes[lane]||0;lanes[lane]=row+1;
- const y=BASE-R-1-row*GAP;
- const dot=ent
- ?el(s,'circle',{cx:x,cy:y,r:R,fill:PAPER,stroke:INK,'stroke-width':1.1,
- class:'pop',style:`animation-delay:${.1+i*.007}s`})
- :el(s,'circle',{cx:x,cy:y,r:R,fill:INK,opacity:.82,
- class:'pop',style:`animation-delay:${.1+i*.007}s`});
- tip(dot,`deal #${i+1} — $${v}k${ent?' · enterprise':''}`);
- });
- // median
- const vs=deals.map(d=>d[0]).sort((a,b)=>a-b);
- const med=vs[Math.floor(N/2)];
- const mx=mapX(med);
- el(s,'line',{x1:mx,y1:52,x2:mx,y2:BASE+8,stroke:'#8F8E88','stroke-width':.9,
- 'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
- txt(s,{x:mx,y:44,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
- style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.1s`,
- class:'fade'},'MEDIAN $'+med+'k');
- 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 DOT = ONE DEAL · THE PILE IS THE DISTRIBUTION · HOLLOW = ENTERPRISE');
- });
- })();
- // ════ L19 · ridgeline ════
- // 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成
- // (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上
- // 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。
- (()=>{
- const ROWS=[['MOBILE',3.2,1.3],['DESKTOP',5.1,1.9],['API',7.4,2.3],['IMPORTS',10.8,3.2],['BATCH',15.2,4]];
- obsReveal('ridge',s=>{
- const X0=92,X1=372,MAXH=24,N=72,LIFT=54;
- const rowY=i=>96+i*44;
- ROWS.forEach(([name,c,sp],i)=>{
- const y0=rowY(i);
- const dens=Array.from({length:N},(_,k)=>{
- const h=k/(N-1)*MAXH;
- return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));
- });
- const dmax=Math.max(...dens);
- const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);
- const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');
- el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,
- class:'fade',style:`animation-delay:${i*.1}s`});
- pts.forEach(([px,py],k)=>{
- if(k%2)return;
- el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:'#8F8E88','stroke-width':.5,
- opacity:.28+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});
- });
- el(s,'path',{d:crest,fill:'none',stroke:INK,'stroke-width':.9,pathLength:1,
- class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});
- el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':.7,
- class:'fade',style:`animation-delay:${i*.1}s`});
- const pk=dens.indexOf(dmax),[px,py]=pts[pk];
- const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:INK,class:'pop',style:`animation-delay:${.6+i*.1}s`});
- tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);
- txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:INK,
- style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,
- class:'fade'},c.toFixed(1)+'h');
- txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
- 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
- });
- const yb=rowY(4);
- [0,6,12,18,24].forEach(h=>{
- const x=X0+h/MAXH*(X1-X0);
- el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'#CFCEC7','stroke-width':.6,
- class:'fade',style:'animation-delay:.8s'});
- txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
- class:'fade',style:'animation-delay:.85s'},h+'h');
- });
- 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:1.1s'},
- 'EACH CREST = ONE PIPELINE · THE AREA IS MADE OF HAIRLINES · DOT = THE PEAK');
- });
- })();
- // ════ L20 · parallel coordinates ════
- // 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。
- // 线束的形状就是数据结构(哪两维相关、谁全线领先)。
- // 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。
- (()=>{
- const DIMS=[['PRICE $',8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];
- const P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],
- ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],
- ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],
- ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];
- obsReveal('parallel',s=>{
- const AX=[64,156,248,340],TOP=64,BOT=252;
- const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};
- // hero: judged on csat+retention+growth (price is a fact, not a virtue)
- const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;
- const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);
- DIMS.forEach(([name,lo,hi],di)=>{
- el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#B0AFA9','stroke-width':1,
- class:'fade',style:`animation-delay:${di*.06}s`});
- [[TOP-8],[BOT+8]].forEach(([yy])=>
- el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#B0AFA9','stroke-width':1,
- class:'fade',style:`animation-delay:${di*.06}s`}));
- txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'#6A6963','text-anchor':'middle',
- 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);
- txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'#C6C5BF',
- class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);
- txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'#C6C5BF',
- class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);
- });
- P.forEach((p,i)=>{
- const isHero=p===hero;
- const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];
- let d=`M${AX[0]} ${ys[0].toFixed(1)}`;
- for(let k=0;k<3;k++){
- const mx=(AX[k]+AX[k+1])/2;
- d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;
- }
- const ln=el(s,'path',{d,fill:'none',
- stroke:isHero?INK:'#8F8E88','stroke-width':isHero?2:.65,
- opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,
- class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});
- tip(ln,`${p[0]} — $${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);
- ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,
- fill:isHero?INK:'#8F8E88',opacity:isHero?1:.7,
- class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));
- if(isHero)
- txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:INK,
- style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
- class:'fade'},p[0]);
- });
- 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'},
- 'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');
- });
- })();
- })();
- </script>
- </body>
- </html>
|