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

1136 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lieflat Charts — Glance Gallery · 22 张参考实现</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
<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}
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}
.ch{height:320px}
.wrap{position:relative;height:320px}
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}}
.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>Glance 系(快读)· 22 张参考实现</h1>
<p>粗笔画 · 提前聚合 · 3 秒读完。每张卡是一个图型的参考实现;代码在页内 <code>// ════ 图型名 ════</code> 注释块里,按 catalog.md 的卡内标题检索。</p>
</div>
<div class="grid2">
<!-- ══ mono-demo ══ -->
<div class="card">
<h2>Daily active range</h2>
<div class="sub">min – max concurrent users · 1–14 Feb</div>
<div class="wrap"><canvas id="c1"></canvas></div>
<div class="src">RANGE CAPSULES · MONO-DEMO · LIVE OPS</div>
</div>
<div class="card dark">
<h2>How releases made us feel</h2>
<div class="sub">reactions tagged in retro · H1 2026</div>
<div class="ch" id="rose"></div>
<div class="src">PETAL ROSE · MONO-DEMO · TEAM RETRO BOARD</div>
</div>
<div class="card">
<h2>Revenue by plan</h2>
<div class="sub">Q2 2026 · MRR, $K</div>
<div class="wrap"><canvas id="c3"></canvas></div>
<div class="src">CHUNKY BARS · MONO-DEMO · BILLING</div>
</div>
<!-- ══ mono-redesign · dot waffle ══ -->
<div class="card">
<h2>Where sign-ups come from</h2>
<div class="sub">Q2 2026 · every dot = 1% of new accounts</div>
<svg id="waffle" viewBox="0 0 380 300" width="100%" style="max-height:320px"></svg>
<div class="src">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION</div>
</div>
<!-- ══ mono-fancy ══ -->
<div class="card">
<h2>Trees planted, year by year</h2>
<div class="sub">reforestation program · thousands of trees</div>
<div class="ch" id="forest"></div>
<div class="src">PICTORIAL BAR · MONO-FANCY · GREEN OPS</div>
</div>
<div class="card dark">
<h2>Who works with whom</h2>
<div class="sub">cross-team collaborations · Q2 2026 · line = shared project</div>
<div class="ch" id="circular"></div>
<div class="src">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB</div>
</div>
<div class="card">
<h2>Everything the platform ships</h2>
<div class="sub">product areas and capabilities · Jun 2026</div>
<div class="ch" id="tree"></div>
<div class="src">TREE · MONO-FANCY · PRODUCT CATALOG</div>
</div>
<div class="card">
<h2>Campaigns rain down, sign-ups flow</h2>
<div class="sub">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days</div>
<div class="ch" id="rainfall"></div>
<div class="src">DUAL AREA · MONO-FANCY · GROWTH + ADS</div>
</div>
<div class="card">
<h2>One dataset, three views</h2>
<div class="sub">watch the same 12 products fly between views · every 3s</div>
<div class="ch" id="morph"></div>
<div class="src">SCATTER MORPH · MONO-FANCY · COHORTS</div>
</div>
<!-- ══ mono-fancy2 ══ -->
<div class="card">
<h2>Where we gained, where we bled</h2>
<div class="sub">net accounts per segment · Q2 2026 · churn left, growth right</div>
<div class="ch" id="negbar"></div>
<div class="src">DIVERGING BAR · MONO-FANCY2 · CRM</div>
</div>
<div class="card dark">
<h2>Integrations, pulled into orbit</h2>
<div class="sub">force layout · node = monthly syncs · drag a node, the web fights back</div>
<div class="ch" id="force"></div>
<div class="src">FORCE GRAPH · NEW · INTEGRATION HUB</div>
</div>
<div class="card">
<h2>Fifty markets, one wave</h2>
<div class="sub">sign-ups per market · bars cascade in · click to replay</div>
<div class="ch" id="wave"></div>
<div class="src">STAGGER DELAY · MONO-FANCY2 · GROWTH</div>
</div>
<!-- ══ mono-fancy3 ══ -->
<div class="card">
<h2>Big slice, deep engagement</h2>
<div class="sub">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min</div>
<div class="ch" id="custompie"></div>
<div class="src">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS</div>
</div>
<div class="card">
<h2>Support load, day by day</h2>
<div class="sub">tickets per hour · each row is its own axis · dot = volume</div>
<div class="ch" id="singleaxis"></div>
<div class="src">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK</div>
</div>
<div class="card">
<h2>Response times, spread out</h2>
<div class="sub">every dot = one ticket · jitter reveals the pile-up · by priority</div>
<div class="ch" id="jitter"></div>
<div class="src">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK</div>
</div>
<!-- ══ mono-fancy4 ══ -->
<div class="card">
<h2>Eight products race for revenue</h2>
<div class="sub">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay</div>
<div class="ch" id="race"></div>
<div class="src">BAR RACE · MONO-FANCY4 · BILLING</div>
</div>
<div class="card">
<h2>Concurrent users, streaming</h2>
<div class="sub">one new reading every 300ms · window of 50 · click to restart</div>
<div class="ch" id="stream"></div>
<div class="src">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS</div>
</div>
<div class="card">
<h2>H1 revenue, drawn in one stroke</h2>
<div class="sub">cumulative ARR · the line draws, the number counts · click to replay</div>
<div class="ch" id="stroke"></div>
<div class="src">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE</div>
</div>
<!-- G19 · violin -->
<div class="card">
<span class="badge">GLANCE 分布型 · 小提琴</span>
<h2>How fast each plan gets an answer</h2>
<div class="sub">hours to first reply · silhouette = where tickets pile up · paper tick = median</div>
<svg id="violin" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">VIOLIN · MONO-GLANCE · SUPPORT DESK</div>
</div>
<!-- G20 · matrix heat -->
<div class="card">
<span class="badge">GLANCE 热力型 · 通用矩阵</span>
<h2>Adoption runs hot on the new versions</h2>
<div class="sub">5 versions × 6 features · shade = % of accounts using it · every cell carries its number</div>
<svg id="gheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">MATRIX HEAT (GLANCE) · MONO-GLANCE · PRODUCT ANALYTICS</div>
</div>
<!-- G21 · rank strip -->
<div class="card">
<span class="badge">GLANCE 排名型 · RANK STRIP</span>
<h2>Flows climbs to the top</h2>
<div class="sub">one row per product, one cell per quarter · darker = higher rank · a bar race you can print</div>
<svg id="rankstrip" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">RANK STRIP · MONO-GLANCE · BILLING</div>
</div>
<!-- G22 · aggregate sankey -->
<div class="card">
<span class="badge">GLANCE 流向型 · 聚合桑基</span>
<h2>Channels pour into plans</h2>
<div class="sub">ribbon width = accounts · channels on the left, the plans they land in on the right</div>
<svg id="sankey" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">AGGREGATE SANKEY · MONO-GLANCE · ATTRIBUTION</div>
</div>
</div>
<script>
// ── shared Mono tokens ──
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
const L=['#1C1C1A','#4A4944','#6A6963','#8F8E88','#B0AFA9','#C6C5BF'];
const LAD=['#1C1C1A','#4A4944','#8F8E88','#B0AFA9','#D8D7D1'];
const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
// ── unified reveal: every chart waits to be scrolled into view; click replays.
// fn charts manage their own timers via keep(); they're cleared on replay.
const timers={};
const obsReveal=(id,fn)=>{
const el=document.getElementById(id);
const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
const io=new IntersectionObserver(es=>{
if(es[0].isIntersecting){go();io.disconnect()}
},{threshold:.35});
io.observe(el);
el.style.cursor='pointer';
el.addEventListener('click',go);
};
const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
const eReveal=(id,opt)=>obsReveal(id,el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();g.setOption(opt);
});
const cjs={};
const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
cjs[id]?.destroy();
cjs[id]=new Chart(el,mkCfg());
});
Chart.defaults.color=MUTED;
Chart.defaults.font.family='Inter';
Chart.defaults.font.size=10.5;
Chart.defaults.borderColor=GRID;
Chart.defaults.plugins.legend.display=false;
Chart.defaults.plugins.tooltip.backgroundColor=INK;
Chart.defaults.plugins.tooltip.titleColor='#F0EFEB';
Chart.defaults.plugins.tooltip.bodyColor='#A5A49E';
Chart.defaults.plugins.tooltip.padding=12;
Chart.defaults.plugins.tooltip.cornerRadius=12;
Chart.defaults.plugins.tooltip.displayColors=false;
// ════ mono-demo · 1: floating range capsules ════
(()=>{
const ranges=[[195,235],[165,192],[150,232],[73,168],[122,202],[182,192],[138,178],[162,227],[195,235],[228,305],[218,232],[165,232],[118,210],[195,232]];
cReveal('c1',()=>({
type:'bar',
data:{labels:ranges.map((_,i)=>i+1),
datasets:[{data:ranges,backgroundColor:INK,borderRadius:99,borderSkipped:false,barPercentage:.32}]},
options:{
maintainAspectRatio:false,
animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*40:0},
plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+'K – '+i.raw[1]+'K users'}}},
scales:{
x:{grid:{display:false},border:{display:false},
ticks:{padding:6,font:{size:9},callback:v=>[0,6,13].includes(v)?(v+1)+' FEB':''}},
y:{min:50,max:320,grid:{display:false},border:{display:false},
ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'K',font:{size:9.5}}},
},
},
}));
})();
// ════ mono-demo · 2: petal rose on dark ════
(()=>{
const EMO=[
['Happiness',12],['Awe',10],['Admiration',5],['Surprise',12],
['Sadness',6],['Fear',4],['Anger',2],['Anticipation',5],
];
const MAXV=12;
const petal=v=>{const t=v/MAXV;return t>.8?'#F2F1ED':t>.6?'#E4E3DD':t>.35?'#C9C8C1':'#A8A7A0'};
eReveal('rose',{
animationDuration:1100,animationEasing:'quarticOut',
tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]},
series:[
{type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
itemStyle:{color:'#32312D',borderRadius:16,borderColor:'#1C1C1A',borderWidth:5},
label:{show:false},emphasis:{scale:false},
data:EMO.map(([n])=>({name:n,value:1})),z:1},
{type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
itemStyle:{color:'transparent'},label:{position:'inside'},
data:EMO.map(([n,v])=>({name:n,value:1,
label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,color:v>=8?'#1C1C1A':'#C8C7C1'},
l:{fontSize:9.5,fontFamily:'Inter',color:v>=8?'#55554F':'#8F8E88'},
}}})),z:3},
{type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
itemStyle:{borderRadius:14,borderColor:'#1C1C1A',borderWidth:5},label:{show:false},
data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
],
});
})();
// ════ mono-demo · 3: chunky bars, values on top ════
(()=>{
cReveal('c3',()=>({
type:'bar',
data:{labels:['STARTER','PRO','TEAM','ENT'],
datasets:[{data:[182,486,391,274],
backgroundColor:['#C6C5BF',INK,'#8F8E88','#B0AFA9'],
borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
options:{
maintainAspectRatio:false,
animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
plugins:{tooltip:{callbacks:{label:i=>'$'+i.raw+'K MRR'}}},
scales:{
x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
y:{display:false,suggestedMax:540},
},
},
plugins:[{
id:'topValues',
afterDatasetsDraw(chart){
const {ctx}=chart;
chart.getDatasetMeta(0).data.forEach((bar,i)=>{
ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';
ctx.fillText('$'+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();
});
},
}],
}));
})();
// ════ mono-redesign · donut → waffle of dots ════
(()=>{
const NS='http://www.w3.org/2000/svg';
const mkEl=(svg,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);svg.appendChild(n);return n};
const D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];
obsReveal('waffle',s=>{
s.innerHTML='';
const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;
let idx=0;
D.forEach(([name,v],g)=>{
for(let k=0;k<v;k++){
const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;
mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:LAD[g],
class:'pop',style:`animation-delay:${c*.008+g*.05}s`});
}
idx+=v;
});
D.forEach(([name,v],g)=>{
const y=26+g*40;
mkEl(s,'circle',{cx:246,cy:y,r:6,fill:LAD[g]});
const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});
n.textContent=name;
const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g<2?INK:'#8F8E88'});
val.textContent=v+'%';
});
});
})();
// ════ mono-fancy · 1: pictorial bar forest ════
(()=>{
const TREE='path://M20,0 L38,28 L29,28 L40,46 L26,46 L26,58 L14,58 L14,46 L0,46 L11,28 L2,28 Z';
const years=['2022','2023','2024','2025','2026'];
const vals=[26,41,63,88,117];
eReveal('forest',{
animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,
tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},
grid:{left:44,right:56,top:10,bottom:8},
yAxis:{type:'category',data:years,inverse:true,
axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},
xAxis:{show:false,max:13},
series:[{
type:'pictorialBar',symbol:TREE,
symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,
data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:L[Math.max(0,4-i)]}})),
label:{show:true,position:'right',offset:[8,0],
color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,
formatter:p=>vals[p.dataIndex]+'k'},
z:10,
},{
type:'pictorialBar',symbol:TREE,
symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,
itemStyle:{color:'#DEDDD6'},data:years.map(()=>13),silent:true,z:5,
}],
});
})();
// ════ mono-fancy · 2: circular graph on dark ════
(()=>{
const teams=[
['Product',52],['Design',38],['Frontend',46],['Backend',44],
['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],
];
const links=[
[0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],
[5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],
];
eReveal('circular',{
animationDuration:1200,animationEasing:'quarticOut',
tooltip:tipDark,
series:[{
type:'graph',layout:'circular',circular:{rotateLabel:false},
left:52,right:52,top:34,bottom:34,
data:teams.map(([n,v],i)=>({
name:n,value:v,symbolSize:v*.62,
itemStyle:{color:i<4?PAPER:i<8?'#8F8E88':'#4A4944',borderWidth:0},
label:{show:true,position:'right',distance:7,color:'#B3B0A4',fontFamily:'Inter',fontSize:9.5,fontWeight:600},
})),
links:links.map(([s,t,w])=>({
source:s,target:t,
lineStyle:{width:w*.7,color:'#4A4840',opacity:.55,curveness:.28},
})),
emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},
}],
});
})();
// ════ mono-fancy · 3: LR tree ════
(()=>{
const b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},
children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});
eReveal('tree',{
animationDuration:1100,animationEasing:'quarticOut',
tooltip:tipLight,
series:[{
type:'tree',layout:'orthogonal',orient:'LR',
left:64,right:96,top:8,bottom:8,
symbol:'circle',symbolSize:7,
initialTreeDepth:2,expandAndCollapse:false,roam:false,
itemStyle:{borderWidth:0},
lineStyle:{width:1.4,curveness:.5},
label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:'#6A6963',position:'left'},
leaves:{label:{position:'right',color:MUTED,fontWeight:500}},
data:[{
name:'Platform',itemStyle:{color:INK},lineStyle:{color:'#C6C5BF'},
label:{fontSize:11.5,color:INK},
children:[
b('Editor',['Blocks','Tables','Comments','History'],L[0]),
b('Automate',['Workflows','Triggers','Webhooks'],L[2]),
b('Collaborate',['Spaces','Guests','Mentions'],L[3]),
b('Integrate',['API','Slack','GitHub'],L[4]),
],
}],
}],
});
})();
// ════ mono-fancy · 4: rainfall dual area ════
(()=>{
const days=Array.from({length:30},(_,i)=>i+1);
const flow=[42,45,44,48,52,55,53,58,64,62,66,71,69,75,82,79,84,80,86,92,88,95,91,97,104,101,108,105,112,118];
const rain=[2,0,4,8,3,0,6,12,7,3,9,15,6,2,11,5,8,14,4,9,6,13,3,7,10,5,12,8,15,6];
eReveal('rainfall',{
animationDuration:1200,animationEasing:'quarticOut',
tooltip:{...tipLight,trigger:'axis',
formatter:p=>`Day ${p[0].axisValue} — spend $${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},
axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:'#B0AFA9'}},
grid:[
{left:44,right:14,top:8,height:'26%'},
{left:44,right:14,top:'42%',bottom:26},
],
xAxis:[
{gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},
{gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},
],
yAxis:[
{gridIndex:0,inverse:true,max:18,splitLine:{show:false},
axisLabel:{color:'#B0AFA9',fontFamily:'Inter',fontSize:9,formatter:'${value}K'},
axisLine:{show:false},axisTick:{show:false}},
{gridIndex:1,splitLine:{lineStyle:{color:'#DEDDD6'}},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},
axisLine:{show:false},axisTick:{show:false}},
],
series:[
{name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,data:rain,
itemStyle:{color:'#B0AFA9',borderRadius:[0,0,4,4]},barWidth:'55%'},
{name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,
smooth:.4,symbol:'none',lineStyle:{color:INK,width:2.2},
areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
colorStops:[{offset:0,color:'rgba(28,28,26,.22)'},{offset:1,color:'rgba(28,28,26,0)'}]}}},
],
});
})();
// ════ mono-fancy · 5: universal transition morph ════
(()=>{
const P12=[
['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],
['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],
['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],
['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],
];
const shade=i=>L[Math.min(5,Math.floor(i/2))];
const axis={splitLine:{lineStyle:{color:'#DEDDD6'}},axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};
const VIEWS=[
{opt:{
grid:{left:38,right:16,top:30,bottom:30},
xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE $',nameTextStyle:{color:'#C6C5BF',fontSize:8.5}},
yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:'#C6C5BF',fontSize:8.5}},
series:[{
id:'p',type:'scatter',universalTransition:true,
symbolSize:d=>Math.sqrt(d[3])*1.35,
data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],
groupId:d[0],itemStyle:{color:shade(i)}})),
label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,
formatter:p=>p.name},
}],
}},
{opt:{
grid:{left:38,right:16,top:30,bottom:44},
xAxis:{...axis,type:'category',
data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),
axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},
yAxis:{...axis,type:'value',max:520},
series:[{
id:'p',type:'bar',universalTransition:true,
data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
groupId:d[0],itemStyle:{color:shade(i),borderRadius:[6,6,0,0]}})),
barCategoryGap:'32%',
}],
}},
{opt:{
grid:{left:0,right:0,top:0,bottom:0},
xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},
series:[{
id:'p',type:'pie',universalTransition:true,
radius:['26%','72%'],center:['50%','50%'],
itemStyle:{borderColor:'#F0EFEB',borderWidth:2,borderRadius:6},
label:{color:'#6A6963',fontFamily:'Inter',fontSize:9,formatter:'{b}'},
labelLine:{lineStyle:{color:'#C6C5BF'}},
data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
groupId:d[0],itemStyle:{color:shade(i)}})),
}],
}},
];
const base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};
obsReveal('morph',el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();
let vi=0;
g.setOption({...base,...VIEWS[0].opt});
keep('morph',setInterval(()=>{
vi=(vi+1)%VIEWS.length;
g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});
},3000));
});
})();
// ════ mono-fancy2 · 2: diverging bar ════
(()=>{
const D=[
['Enterprise',86],['Team',54],['Pro',31],['Starter',12],
['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],
];
eReveal('negbar',{
animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,
tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},
grid:{left:96,right:52,top:8,bottom:8},
xAxis:{type:'value',
splitLine:{lineStyle:{color:'#DEDDD6'}},
axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,
axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:'#6A6963',fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
series:[{
type:'bar',barWidth:16,
data:D.map(([n,v])=>({name:n,value:v,
itemStyle:{color:v>0?INK:'#B0AFA9',
borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),
label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,
position:'outside',
formatter:p=>(p.value>0?'+':'')+p.value,color:INK},
markLine:{symbol:'none',silent:true,label:{show:false},
lineStyle:{color:'#8F8E88',width:1.5},
data:[{xAxis:0}]},
}],
});
})();
// ════ force graph on dark: the network shakes itself into place ════
// (replaced the axis-break bar — bars must never lie about length; house rule)
(()=>{
const hubs=[['Core API',52,0]];
const apps=[
['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],
['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],
['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],
];
const nodes=[...hubs,...apps];
const links=[
...apps.map((a,i)=>({source:a[0],target:'Core API',
lineStyle:{width:Math.max(.8,a[1]*.09),color:'#4A4840',opacity:.6}})),
// a few app-to-app side roads
{source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:'#3A3934',opacity:.5}},
{source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:'#3A3934',opacity:.5}},
{source:'Figma',target:'Notion',lineStyle:{width:1,color:'#3A3934',opacity:.5}},
{source:'Stripe',target:'Sheets',lineStyle:{width:1,color:'#3A3934',opacity:.5}},
];
eReveal('force',{
animationDuration:1200,animationEasing:'quarticOut',
tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},
series:[{
type:'graph',layout:'force',
force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},
roam:false,draggable:true,
left:14,right:14,top:14,bottom:14,
data:nodes.map(([n,v,tier])=>({
name:n,value:v,symbolSize:8+v*.75,
itemStyle:{color:tier===0?PAPER:tier===1?'#B3B0A4':'#6A6963'},
label:{show:true,position:'right',distance:6,
color:tier===0?PAPER:'#8F8E88',fontFamily:'Inter',
fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},
})),
links,
emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},
}],
});
})();
// ════ mono-fancy2 · 4: stagger wave ════
(()=>{
const n=50;
const vals=Array.from({length:n},(_,i)=>
Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));
eReveal('wave',{
tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},
grid:{left:36,right:10,top:14,bottom:24},
xAxis:{type:'category',data:vals.map((_,i)=>i+1),
axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},
yAxis:{type:'value',
splitLine:{lineStyle:{color:'#DEDDD6'}},
axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},
series:[{
type:'bar',barCategoryGap:'26%',
data:vals.map((v,i)=>({value:v,
itemStyle:{color:L[Math.min(5,Math.floor((120-v)/16))],borderRadius:[4,4,0,0]}})),
animationDelay:i=>i*36,
animationDuration:640,animationEasing:'elasticOut',
}],
animationDurationUpdate:640,
animationDelayUpdate:i=>i*36,
});
})();
// ════ mono-fancy3 · 1: custom double-encoded pie ════
(()=>{
const D=[
['Editor',34,42],['Boards',22,31],['Docs',18,38],
['Chat',12,11],['Automations',8,24],['Other',6,7],
];
const SCALE=45;
const TAU=Math.PI*2;
const byShare=[...D].sort((a,b)=>b[1]-a[1]);
const minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);
let acc=0;
const slices=byShare.map(([n,share,mins])=>{
const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;
acc+=share;
return {n,share,mins,p0,p1,color:L[minsDesc.indexOf(mins)]};
});
eReveal('custompie',{
animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,
tooltip:{...tipLight,formatter:p=>{
const s=slices[p.dataIndex];
return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';
}},
xAxis:{show:false,min:-1,max:1},
yAxis:{show:false,min:-1,max:1},
grid:{left:0,right:0,top:0,bottom:0},
series:[{
type:'custom',
renderItem:(params,api)=>{
const s=slices[params.dataIndex];
const cx=api.getWidth()/2, cy=api.getHeight()/2+6;
const R=Math.min(api.getWidth(),api.getHeight())*.37;
const r0=R*.24;
const rOf=m=>r0+(R-r0)*m/SCALE;
const pm=(s.p0+s.p1)/2, lr=R+14;
const cos=Math.cos(pm);
const kids=[];
if(params.dataIndex===0){
[15,30,45].forEach(m=>kids.push({type:'circle',
shape:{cx,cy,r:rOf(m)},
style:{fill:'none',stroke:'#D8D6CE',lineWidth:1,lineDash:[3,4]}}));
}
kids.push({type:'sector',
shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},
style:{fill:s.color,stroke:PAPER,lineWidth:3},
enterFrom:{shape:{r:r0}}});
kids.push({type:'text',style:{
x:cx+cos*lr, y:cy+Math.sin(pm)*lr,
text:s.n+' '+s.share+'% · '+s.mins+'m',
font:'600 10px Inter',fill:'#55544E',
align:Math.abs(cos)<.25?'center':(cos>0?'left':'right'),
verticalAlign:'middle'},
enterFrom:{style:{opacity:0}}});
return {type:'group',children:kids};
},
data:slices.map(s=>s.share),
}],
});
})();
// ════ mono-fancy3 · 3: single-axis small multiples ════
(()=>{
const days=['MON','TUE','WED','THU','FRI','SAT','SUN'];
const hours=Array.from({length:24},(_,i)=>i);
const mkRow=di=>hours.map(h=>{
const workday=di<5, core=h>=9&&h<=17;
let v=Math.round((workday&&core?6+5*Math.sin((h-9)/8*Math.PI):0)+((h+di)%5===0?2:0)+(h>=22||h<=5?0:1));
return [h,v];
}).filter(d=>d[1]>0);
eReveal('singleaxis',{
animationDuration:500,animationEasing:'backOut',
tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},
title:days.map((d,i)=>({
text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,
textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},
})),
singleAxis:days.map((d,i)=>({
left:52,right:14,type:'category',boundaryGap:false,
data:hours.map(h=>String(h).padStart(2,'0')),
top:i*100/7+6+'%',height:100/7-8+'%',
axisLine:{show:false},axisTick:{show:false},
axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},
splitLine:{show:false},
})),
series:days.map((d,i)=>({
singleAxisIndex:i,coordinateSystem:'singleAxis',
type:'scatter',
data:mkRow(i),
symbolSize:dd=>dd[1]*2.6,
itemStyle:{color:i<5?L[Math.min(4,i)]:'#C6C5BF'},
animationDelay:di=>i*160+di*14,
})),
});
})();
// ════ mono-fancy3 · 4: jitter strip ════
(()=>{
const cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];
const mkPts=(ci,n,median,spread,outliers)=>{
const pts=[];
for(let i=0;i<n;i++){
const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);
let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);
if(i<outliers) x=median+spread*2.2+u*spread*3;
const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;
pts.push([Math.max(.2,x),jy]);
}
return pts;
};
const groups=[
mkPts(0,38,.8,.5,2),
mkPts(1,64,2.4,1.1,3),
mkPts(2,110,6.5,2.4,4),
mkPts(3,72,14,4.5,3),
];
const flat=groups.flat();
eReveal('jitter',{
animationDuration:450,animationEasing:'cubicOut',
animationDelay:i=>i<flat.length?Math.round(flat[i][1])*260+(i%37)*9:0,
tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},
grid:{left:86,right:16,top:14,bottom:30},
xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:'#C6C5BF',fontSize:8.5},
splitLine:{lineStyle:{color:'#DEDDD6'}},
axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},
yAxis:{type:'value',min:-.6,max:3.6,inverse:true,
splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},
axisLabel:{show:false}},
series:[{
type:'scatter',
data:flat,
symbolSize:7,
itemStyle:{color:p=>L[Math.round(p.value[1])],opacity:.62},
},{
type:'scatter',silent:true,symbolSize:0,
data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],
color:'#6A6963',fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),
}],
});
})();
// ════ mono-fancy4 · 1: bar race ════
(()=>{
const P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];
const YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];
const vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];
for(let y=1;y<YEARS.length;y++)
vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));
obsReveal('race',el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();
let step=0;
const frame=()=>{
const row=vals[step];
const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);
g.setOption({
animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',
tooltip:{...tipLight,valueFormatter:v=>'$'+Math.round(v)+'K'},
grid:{left:64,right:64,top:8,bottom:8},
xAxis:{type:'value',max:'dataMax',
splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
yAxis:{type:'category',data:P,inverse:true,max:7,
animationDuration:300,animationDurationUpdate:300,
axisLine:{show:false},axisTick:{show:false},
axisLabel:{color:'#6A6963',fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
series:[{
type:'bar',realtimeSort:true,barCategoryGap:'32%',
data:row.map((v,i)=>({value:v,
itemStyle:{color:L[Math.min(5,rank.indexOf(i))],borderRadius:99}})),
label:{show:true,position:'right',valueAnimation:true,
fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,
formatter:p=>'$'+Math.round(p.value)+'K'},
}],
graphic:[{id:'yr',type:'text',right:18,bottom:14,
style:{text:YEARS[step],font:'800 44px Inter',fill:'#DEDDD6'}}],
});
step=(step+1)%YEARS.length;
};
frame();
keep('race',setInterval(frame,1150));
});
})();
// ════ mono-fancy4 · 2: dynamic data stream ════
(()=>{
const WIN=50;
obsReveal('stream',el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();
let t=0,v=64;
const data=[];
for(;t<WIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}
const draw=()=>g.setOption({
animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',
tooltip:{...tipLight,valueFormatter:x=>x+'k users'},
grid:{left:14,right:58,top:44,bottom:20},
xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,
axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},
yAxis:{show:false,min:20,max:130},
series:[{
type:'line',data:[...data],smooth:.45,symbol:'none',
lineStyle:{color:INK,width:2.4},
endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:INK,
formatter:p=>p.value+'k'},
areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
colorStops:[{offset:0,color:'rgba(28,28,26,.16)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
}],
graphic:[{type:'group',left:16,top:10,children:[
{type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:'#4A4944'}},
{type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:INK}},
]}],
});
draw();
keep('stream',setInterval(()=>{
v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));
data.push(Math.round(v));data.shift();t++;
draw();
},300));
});
})();
// ════ mono-fancy4 · 3: draw-in + counter ════
(()=>{
const days=Array.from({length:180},(_,i)=>i);
const daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);
const cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);
const TOTAL=cum[cum.length-1];
const DUR=2600;
let gen=0; // replay guard: only the latest counter loop may write
obsReveal('stroke',el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();
gen++;const my=gen;
g.setOption({
animationDuration:DUR,animationEasing:'cubicOut',
tooltip:{show:false},
grid:{left:14,right:20,top:64,bottom:24},
xAxis:{type:'category',data:days,boundaryGap:false,
axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
axisLabel:{color:'#C6C5BF',fontFamily:'Inter',fontSize:9,fontWeight:600,
interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},
yAxis:{show:false,max:TOTAL*1.06},
series:[{
type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',
lineStyle:{color:INK,width:2.6},
areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
colorStops:[{offset:0,color:'rgba(28,28,26,.14)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
}],
});
const t0=performance.now();
const tick=()=>{
if(my!==gen)return;
const p=Math.min(1,(performance.now()-t0)/DUR);
const eased=1-Math.pow(1-p,3);
const val=(TOTAL*eased/1000).toFixed(2);
g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[
{type:'text',style:{text:'$'+val+'M',font:'800 32px Inter',fill:INK}},
{type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:'#8F8E88'}},
]}]});
if(p<1)requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
})();
addEventListener('resize',()=>{
['rose','forest','circular','tree','rainfall','morph','negbar','force','wave',
'custompie','singleaxis','jitter','race','stream','stroke']
.forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.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;
// ════ G19 · violin ════
// 小提琴:提前聚合的密度剪影,Glance 世界观(beeswarm 的姊妹)。
// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),
// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。
(()=>{
const G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];
const M=64;
const samples=g=>Array.from({length:M},(_,i)=>{
const [,c,s]=G[g];
const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);
return Math.max(.2,v);
});
obsReveal('violin',s=>{
const TOP=40,BASE=262,HMAX=17;
const mapY=h=>BASE-h/HMAX*(BASE-TOP);
const colX=g=>76+g*84;
// hour grid
[0,4,8,12,16].forEach(h=>{
el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'#E3E2DB','stroke-width':.8,
class:'fade',style:`animation-delay:${h*.01}s`});
txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
class:'fade',style:`animation-delay:${h*.01}s`},h+'h');
});
const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9'];
G.forEach(([name],g)=>{
const vs=samples(g).sort((a,b)=>a-b);
const med=vs[Math.floor(M/2)];
// fuller bandwidth = the classic rounded violin belly, not a spindle
const bw=Math.max(.9,G[g][2]*.72);
// pad past the data by 1.6 bandwidths so the density decays to ~0 and the
// ends taper naturally — clipping tighter gives flat-cut lantern ends
const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);
const NPTS=44;
const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));
const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));
const dmax=Math.max(...dens);
const W=30;
const x=colX(g);
// smooth closed outline through the mirrored density — quadratic
// midpoint smoothing, same trick as almanac's blob
const pts=[];
grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));
for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);
let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
for(let t=0;t<pts.length;t++){
const p=pts[t],q=pts[(t+1)%pts.length];
d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
}
const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});
tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);
// median: paper tick across the body, value parked right
const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));
const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;
el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),
stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});
txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'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*.14}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.1s'},
'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');
});
})();
// ════ G20 · matrix heat ════
// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出
// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。
(()=>{
const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];
const VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];
const BASEV=[88,74,61,42,35,27];
const v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));
const shade=t=>t>64?INK:t>46?'#4A4944':t>30?'#8F8E88':t>15?'#B0AFA9':'#D8D7D1';
obsReveal('gheat',s=>{
const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;
FEAT.forEach((f,c)=>{
txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);
});
VER.forEach((ver,r)=>{
txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
class:'fade',style:`animation-delay:${r*.05}s`},ver);
FEAT.forEach((f,c)=>{
const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;
const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),
class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});
tip(cell,`${f} on ${ver} — ${t}% of accounts`);
txt(s,{x:x+W/2,y:y+H/2+4,'font-size':10.5,'font-weight':800,fill:dark?PAPER:INK,'text-anchor':'middle',
class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);
});
});
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:1s'},
'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');
});
})();
// ════ G21 · rank strip ════
// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,
// 和 slope 同一种丑(catalog 实测三连败那条),废弃。
// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。
// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。
(()=>{
const RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],
['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];
const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9','#D8D7D1']; // rank 1 → 5
obsReveal('rankstrip',s=>{
// rows sorted by final rank, the climber lands on top
const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);
const X0=110,PX=38,Y0=64,PY=42,C=32;
['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>
txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));
rows.forEach(([p,rk],i)=>{
const y=Y0+i*PY,hero=p==='Flows';
txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'#6A6963','text-anchor':'end',
class:'fade',style:`animation-delay:${i*.07}s`},p);
rk.forEach((r,q)=>{
const dark=r<=2;
const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],
stroke:'#CFCEC7','stroke-width':.5,
class:'pop',style:`animation-delay:${i*.07+q*.035}s`});
tip(cell,`${p} — #${r} in Q${q+1}`);
txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,
fill:dark?PAPER:'#55544E','text-anchor':'middle',
class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);
});
// arrival flag: climbed / fell / held, told at the row's end
const d=rk[0]-rk[5];
txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,
fill:d>0?INK:d<0?'#B0AFA9':'#C6C5BF',
class:'fade',style:`animation-delay:${.5+i*.07}s`},
d>0?'▲'+d:d<0?'▼'+(-d):'—');
});
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'},
'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');
});
})();
// ════ G22 · aggregate sankey ════
// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——
// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。
// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。
(()=>{
const SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];
const DST=['FREE','PRO','TEAM'];
const SH=[INK,'#4A4944','#8F8E88','#B0AFA9','#C6C5BF'];
obsReveal('sankey',s=>{
const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;
const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));
const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));
const sy=[],dy=[];
let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});
y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});
const syc=[...sy],dyc=[...dy];
SRC.forEach((d,i)=>{
d[1].forEach((v,j)=>{
if(!v)return;
const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;
syc[i]=sb;dyc[j]=db;
const rib=el(s,'path',{d:`M${XL} ${sa} C${MID} ${sa} ${MID} ${da} ${XR} ${da} L${XR} ${db} C${MID} ${db} ${MID} ${sb} ${XL} ${sb} Z`,
fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});
tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);
});
});
SRC.forEach((d,i)=>{
el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],
class:'fade',style:`animation-delay:${i*.06}s`});
txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#4A4944','text-anchor':'end',
'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);
});
DST.forEach((n,j)=>{
el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#3A3934',
class:'fade',style:`animation-delay:${.1+j*.06}s`});
txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#4A4944',
'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);
txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);
});
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.1s'},
'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL');
});
})();
})();
</script>
</body>
</html>