Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v));\n }\n let hiD=0,loD=0;\n DAYS.forEach(([o,c,h,l],d)=>{\n if(h>DAYS[hiD][2])hiD=d;\n if(l\u003cDAYS[loD][3])loD=d;\n });\n DAYS.forEach(([o,c,h,l],d)=>{\n const cx=x(d),up=c>=o;\n const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));\n // wick: one hairline, full reach\n el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'#6A6963','stroke-width':.7,\n class:'fade',style:`animation-delay:${d*.03}s`});\n // body: capsule; hollow = up, ink = down\n const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,\n ...(up?{fill:PAPER,stroke:INK,'stroke-width':1.1}:{fill:INK}),\n class:'fade',style:`animation-delay:${.05+d*.03}s`});\n tip(body,`Day ${d+1} — open ${o.toFixed(1)} · close ${c.toFixed(1)} · high ${h.toFixed(1)} · low ${l.toFixed(1)}`);\n // extremes, labeled with hairline flags\n if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(h));\n if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'#6A6963','text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(l));\n });\n // week ticks on the floor\n el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':.8,class:'fade'});\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'#CFCEC7','stroke-width':.6,\n class:'fade',style:`animation-delay:${d*.008}s`});\n ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>\n txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade'},w));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":49417,"content_sha256":"1d01c8695b6b8e921de142cef0de66c10802a7a3ef38112b049c0c2f1eb35fd6"},{"filename":"templates/big-circular.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Mono — Circular Graph, Dense\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#1C1C1A}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:#F0EFEB;border-radius:24px;padding:34px 34px 24px;\n width:min(1100px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#8F8E88;margin-bottom:10px}\n .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{height:78vh;min-height:620px;cursor:pointer}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>Six hundred collaborations, one ring\u003c/h2>\n \u003cdiv class=\"sub\">60 repos · line = shared contributors · hover a repo to light up its world · click to replay\u003c/div>\n \u003cdiv id=\"ch\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG\u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#1C1C1A',PAPER='#F0EFEB';\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── 60 repos in 5 org clusters; cluster = shade family ──\nconst ORGS=[\n ['platform',16,'#F0EFEB'], // paper — the core\n ['product',14,'#C9C7BD'],\n ['data',12,'#A5A39A'],\n ['infra',10,'#8F8E88'],\n ['labs',8,'#6A6963'],\n];\nconst nodes=[];\nORGS.forEach(([org,count],oi)=>{\n for(let i=0;i\u003ccount;i++){\n // activity: a few giants per cluster, long tail of small repos\n const heavy=i\u003c2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;\n nodes.push({\n name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),\n org:oi,value:Math.round(heavy),\n });\n }\n});\n// links: dense inside a cluster, sparser across; weight = shared contributors\nconst links=[];\nconst idx={};nodes.forEach((n,i)=>idx[n.name]=i);\nnodes.forEach((a,i)=>{\n nodes.forEach((b,j)=>{\n if(j\u003c=i)return;\n const same=a.org===b.org;\n const p=same?.16:.045;\n if(rnd(i+1,j+2)\u003cp){\n links.push({source:i,target:j,\n w:1+Math.round(rnd(i+2,j+3)*6),same});\n }\n });\n});\n\nconst g=echarts.init(document.getElementById('ch'));\nconst opt={\n animationDuration:1800,animationEasing:'quarticOut',\n tooltip:{backgroundColor:PAPER,borderWidth:0,\n textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14],\n formatter:p=>p.dataType==='node'\n ?p.name+' — '+p.value+' contributors'\n :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},\n series:[{\n type:'graph',layout:'circular',\n circular:{rotateLabel:true},\n left:24,right:24,top:24,bottom:24,\n data:nodes.map((n,i)=>({\n name:n.name,value:n.value,\n symbolSize:3.5+Math.sqrt(n.value)*1.7,\n itemStyle:{color:ORGS[n.org][2],borderWidth:0},\n // only the giants get a label — 60 names would be soup\n label:{show:n.value>=26,position:'right',distance:4,\n color:'#B3B0A4',fontFamily:'Inter',fontSize:8.5,fontWeight:600},\n })),\n links:links.map(l=>({\n source:l.source,target:l.target,\n rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,\n lineStyle:{\n width:.4+l.w*.28,\n color:l.same?'#55534B':'#3A3934',\n opacity:l.same?.5:.32,\n curveness:.3,\n },\n })),\n emphasis:{\n focus:'adjacency',\n lineStyle:{color:PAPER,opacity:.9,width:1.6},\n label:{show:true,color:PAPER},\n },\n blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},\n }],\n};\nconst play=()=>{g.clear();g.setOption(opt)};\nplay();\ndocument.getElementById('ch').addEventListener('click',play);\naddEventListener('resize',()=>g.resize());\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":4035,"content_sha256":"38af6e7022b58ddf28872c606d58ba99176fbc092acbb70e6bb53124cf776176"},{"filename":"templates/big-force.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Mono — Force Graph, Dense\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#1C1C1A}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:#F0EFEB;border-radius:24px;padding:34px 34px 24px;\n width:min(1100px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#8F8E88;margin-bottom:10px}\n .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{height:78vh;min-height:620px;cursor:grab}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>A galaxy of one hundred eighty services\u003c/h2>\n \u003cdiv class=\"sub\">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay\u003c/div>\n \u003cdiv id=\"ch\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH\u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#1C1C1A',PAPER='#F0EFEB';\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── 8 domain hubs, each with a swarm of services; hubs interconnect ──\nconst DOMAINS=[\n ['auth',26,'#F0EFEB'],['billing',24,'#DCDAD2'],['content',28,'#C9C7BD'],\n ['search',20,'#B3B0A4'],['notify',22,'#9C9A91'],['media',20,'#8F8E88'],\n ['analytics',18,'#7C7A72'],['edge',14,'#6A6963'],\n];\nconst nodes=[],links=[];\nDOMAINS.forEach(([dom,count,shade],di)=>{\n // the hub\n nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});\n // satellites: long-tail sizes, a couple of mid-weights per domain\n for(let i=0;i\u003ccount;i++){\n const v=i\u003c3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;\n const name=dom+'-'+String(i+1).padStart(2,'0');\n nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});\n // satellite -> its hub\n links.push({source:name,target:dom,\n lineStyle:{width:.5+v*.05,color:'#3F3E38',opacity:.5}});\n // occasional satellite-to-satellite shortcut inside the domain\n if(i>1&&rnd(di+3,i+11)\u003c.22){\n links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),\n lineStyle:{width:.5,color:'#33322D',opacity:.4}});\n }\n }\n});\n// hub-to-hub backbone: every domain talks to 2-3 others\nconst H=DOMAINS.length;\nfor(let a=0;a\u003cH;a++)for(let b=a+1;b\u003cH;b++){\n if(rnd(a+7,b+9)\u003c.42){\n links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],\n lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#55534B',opacity:.65,curveness:.08}});\n }\n}\n// a few cross-domain satellite shortcuts — the messy real-world wires\nfor(let k=0;k\u003c26;k++){\n const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];\n const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];\n if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){\n links.push({source:a.name,target:b.name,\n lineStyle:{width:.5,color:'#2E2D29',opacity:.38,curveness:.15}});\n }\n}\n\nconst g=echarts.init(document.getElementById('ch'));\nconst opt={\n animationDuration:300,\n tooltip:{backgroundColor:PAPER,borderWidth:0,\n textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14],\n formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},\n roam:true,draggable:true,\n left:6,right:6,top:6,bottom:6,\n data:nodes.map(n=>({\n name:n.name,value:n.value,\n symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,\n itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},\n label:{show:n.hub,position:'inside',\n color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},\n })),\n links,\n emphasis:{\n focus:'adjacency',\n lineStyle:{color:PAPER,opacity:.9,width:1.4},\n label:{show:true,color:PAPER,position:'right'},\n },\n blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},\n }],\n};\nconst play=()=>{g.clear();g.setOption(opt)};\nplay();\n// click on empty canvas replays; clicks on nodes are for dragging\ng.getZr().on('click',e=>{if(!e.target)play()});\naddEventListener('resize',()=>g.resize());\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":4651,"content_sha256":"e331808fbb83814c7557bfc9ca821c84e6f2dfb8a7425b2ae16a1842e669ac37"},{"filename":"templates/big-threads.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Mono — Thread Triptych, Interactive\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#1C1C1A;--paper:#F0EFEB}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:var(--paper);border-radius:24px;padding:34px 34px 24px;\n width:min(1200px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#8F8E88;margin-bottom:6px}\n .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{width:100%;height:74vh;min-height:560px;display:block}\n #status{height:34px;display:flex;align-items:center;gap:10px;\n font-size:12px;color:#8F8E88;border-top:1px solid #2E2D29;margin-top:6px;padding-top:8px}\n #status b{color:var(--paper);font-weight:700}\n #status .pin{font-size:9px;letter-spacing:.1em;color:#55554F;font-weight:700}\n svg text{font-family:'Inter',sans-serif;user-select:none}\n\n /* thread states */\n .thread{transition:opacity .18s ease,stroke-width .18s ease}\n .thread.hot{opacity:.95 !important}\n svg.focused .thread:not(.hot){opacity:.035 !important}\n .nodelab{cursor:pointer;transition:opacity .18s ease}\n svg.focused .nodelab:not(.hot){opacity:.25}\n .hit{cursor:pointer}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>Where the data actually goes — now answerable\u003c/h2>\n \u003cdiv class=\"sub\">source → processor → destination · one thread per data path · hover to trace a route · hover any label to pull its whole bundle · click to pin, click dark space to release\u003c/div>\n \u003csvg id=\"ch\" viewBox=\"0 0 1140 640\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv id=\"status\">\u003cspan id=\"stext\">hover a thread or a label\u003c/span>\u003cspan class=\"pin\" id=\"spin\">\u003c/span>\u003c/div>\n \u003cdiv class=\"src\">THREAD TRIPTYCH · INTERACTIVE · DATA MAP\u003c/div>\n\u003c/div>\n\u003cscript>\nconst PAPER='#F0EFEB',NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ── the pipeline ──\nconst SRC=['web-app','ios-app','android','desktop','cli','public-api','webhooks','forms','email-in','chat-widget',\n 'billing-ev','auth-ev','search-log','audit-ev','crash-rep','perf-beacon','cdn-log','edge-log','job-ev','sync-ev',\n 'import','export-req','admin-ui','partner-api','iot-ping','backfill'];\nconst MIDV=['parse','validate','dedupe','enrich','geoip','sessionize','pii-mask','score','sample','route',\n 'batch','window','join','flatten','filter','hash','encrypt','compact','index','tag'];\nconst MID=MIDV.flatMap(v=>[v+'-a',v+'-b']); // 40 processors\nconst DST=['WAREHOUSE','ANALYTICS','CRM','ADS','BILLING','SEARCH','ML PIPE','ARCHIVE','AUDIT','EXPORTS'];\n\n// routes: each source emits 2-3 full paths; volume is long-tailed\nconst routes=[];\nSRC.forEach((s,i)=>{\n const n=2+Math.floor(rnd(i+1,5)*2);\n for(let k=0;k\u003cn;k++){\n const mi=Math.floor(rnd(i*3+k+2,13)*MID.length);\n const di=Math.min(9,Math.floor(rnd(i*5+k+4,19)**2*11)); // skew: low dsts hoard\n const v=Math.round(1+rnd(i*7+k+6,29)**2*30); // 1..31, long tail\n routes.push({s:i,m:mi,d:di,v});\n }\n});\n\n// ── geometry ──\nconst X1=180,X2=560,X3=930;\nconst sy=i=>36+i*22.6, my=i=>30+i*14.6, dy=i=>60+i*57;\nconst path=r=>`M${X1+6} ${sy(r.s)} C${(X1+X2)/2} ${sy(r.s)} ${(X1+X2)/2} ${my(r.m)} ${X2} ${my(r.m)}`\n +` C${(X2+X3)/2} ${my(r.m)} ${(X2+X3)/2} ${dy(r.d)} ${X3-6} ${dy(r.d)}`;\n\nconst svg=document.getElementById('ch');\nconst stext=document.getElementById('stext'),spin=document.getElementById('spin');\nconst fmt=v=>v+'k ev/day';\nlet pinned=null;\n\n// column headers\n[[X1,'① SOURCE'],[X2,'② PROCESSOR'],[X3,'③ DESTINATION']].forEach(([x,t])=>\n txt(svg,{x,y:14,'font-size':9,'font-weight':800,fill:PAPER,'letter-spacing':'.14em','text-anchor':'middle'},t));\n\n// threads (drawn first, under labels)\nconst gThreads=el(svg,'g',{});\nroutes.forEach((r,ri)=>{\n r.el=el(gThreads,'path',{d:path(r),fill:'none',stroke:PAPER,\n 'stroke-width':Math.max(.6,r.v*.14),class:'thread',\n opacity:.06+Math.min(.2,r.v*.012),'stroke-linecap':'round'});\n // invisible fat twin: the hover/click target\n r.hit=el(gThreads,'path',{d:path(r),fill:'none',stroke:'#000','stroke-opacity':0,\n 'stroke-width':9,class:'hit'});\n r.hit.dataset.route=ri;\n});\n\n// node labels + ticks; each carries hit metadata\nconst mkNode=(x,y,label,anchor,cls,meta,size,fill)=>{\n const g=el(svg,'g',{class:'nodelab'});\n g.dataset.kind=meta.kind;g.dataset.idx=meta.idx;\n el(g,'rect',{x:x-3,y:y-1.6,width:6,height:3.2,fill:PAPER,opacity:.85});\n txt(g,{x:anchor==='end'?x-9:x+9,y:y+2.6,'font-size':size,'font-weight':600,\n fill,'text-anchor':anchor},label);\n // generous invisible hit zone\n el(g,'rect',{x:anchor==='end'?x-92:x-6,y:y-6.5,width:98,height:13,fill:'#000','fill-opacity':0,class:'hit'});\n return g;\n};\nSRC.forEach((s,i)=>mkNode(X1,sy(i),s,'end','',{kind:'s',idx:i},7.5,'#B3B0A4'));\nMID.forEach((m,i)=>mkNode(X2,my(i),m,'start','',{kind:'m',idx:i},5.4,'#6A6963'));\nDST.forEach((d,i)=>{\n const g=el(svg,'g',{class:'nodelab'});\n g.dataset.kind='d';g.dataset.idx=i;\n el(g,'circle',{cx:X3,cy:dy(i),r:3.4,fill:PAPER});\n txt(g,{x:X3+12,y:dy(i)+3,'font-size':8.5,'font-weight':700,fill:'#B3B0A4','letter-spacing':'.08em'},d);\n el(g,'rect',{x:X3-8,y:dy(i)-10,width:120,height:20,fill:'#000','fill-opacity':0,class:'hit'});\n});\n\n// ── highlight machinery ──\nconst nodeLabs=[...svg.querySelectorAll('.nodelab')];\nconst clear=()=>{\n svg.classList.remove('focused');\n routes.forEach(r=>r.el.classList.remove('hot'));\n nodeLabs.forEach(n=>n.classList.remove('hot'));\n stext.textContent='hover a thread or a label';spin.textContent='';\n};\nconst focusRoutes=(rs,label)=>{\n svg.classList.add('focused');\n routes.forEach(r=>r.el.classList.toggle('hot',rs.includes(r)));\n nodeLabs.forEach(n=>{\n const k=n.dataset.kind,ix=+n.dataset.idx;\n n.classList.toggle('hot',rs.some(r=>(k==='s'&&r.s===ix)||(k==='m'&&r.m===ix)||(k==='d'&&r.d===ix)));\n });\n stext.innerHTML=label;\n};\nconst routeLabel=r=>`\u003cb>${SRC[r.s]}\u003c/b> → ${MID[r.m]} → \u003cb>${DST[r.d]}\u003c/b> · ${fmt(r.v)}`;\nconst nodeSel=(kind,idx)=>{\n const rs=routes.filter(r=>(kind==='s'&&r.s===idx)||(kind==='m'&&r.m===idx)||(kind==='d'&&r.d===idx));\n const total=rs.reduce((a,r)=>a+r.v,0);\n const name=kind==='s'?SRC[idx]:kind==='m'?MID[idx]:DST[idx];\n return [rs,`\u003cb>${name}\u003c/b> · ${rs.length} path${rs.length>1?'s':''} · ${fmt(total)} total`];\n};\n\nconst apply=t=>{\n const hit=t.closest?.('.hit');\n if(!hit)return null;\n const rg=hit.dataset.route,ng=hit.closest('.nodelab');\n if(rg!=null){const r=routes[+rg];return()=>focusRoutes([r],routeLabel(r))}\n if(ng){const [rs,lab]=nodeSel(ng.dataset.kind,+ng.dataset.idx);return()=>focusRoutes(rs,lab)}\n return null;\n};\nsvg.addEventListener('mousemove',e=>{\n if(pinned)return;\n const f=apply(e.target);\n f?f():clear();\n});\nsvg.addEventListener('mouseleave',()=>{if(!pinned)clear()});\nsvg.addEventListener('click',e=>{\n const f=apply(e.target);\n if(f){pinned=f;f();spin.textContent='PINNED · CLICK DARK SPACE TO RELEASE'}\n else{pinned=null;clear()}\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":7711,"content_sha256":"093d08d506722d61edcbb1f5b33fb5a35cb9edc904c7cdc3d92b3377cdcfd39b"},{"filename":"templates/color/basics-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Basics F1–F17 × PALM\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap\" rel=\"stylesheet\">\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003cstyle>\n :root{--bg:#F0EFEB;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.32);--grid:rgba(88,64,46,.16)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed var(--faint);color:var(--muted)}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n .chartbox{width:100%;height:360px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>基础型组 F1–F17 × PALM\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#43593B\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#F2D17E\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#ACAD79\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#58402E\">\u003c/span>  椰林 · 深绿主墨 + 浓咖文字,琥珀只干强调的活(峰值/最优/表针)。序数梯纯走绿→黄:深绿 → 中绿 → 苔绿 → 浅苔 → 橄榄 → 麦黄。几何、动画、数据与 basics-gallery 一致;彩色版统一加墨(线宽 ×1.8 / 透明度地板 .85,dot heat 除外——它的墨本来就够)。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 柱状\u003c/span>\n \u003ch2>Revenue by plan, rung by rung\u003c/h2>\n \u003cdiv class=\"sub\">one rung = one $k of MRR · the bar is a ladder you can count\u003c/div>\n \u003csvg id=\"rungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG BARS · PALM · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 折线\u003c/span>\n \u003ch2>Thirty days of sign-ups\u003c/h2>\n \u003cdiv class=\"sub\">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest\u003c/div>\n \u003csvg id=\"dayline\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HAIRLINE LINE · PALM · GROWTH\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 面积\u003c/span>\n \u003ch2>Concurrent users, filled with days\u003c/h2>\n \u003cdiv class=\"sub\">one hairline = one day, floor to peak · the area is made of days, not paint\u003c/div>\n \u003csvg id=\"hairarea\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HAIRLINE AREA · PALM · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 环形\u003c/span>\n \u003ch2>Where the traffic comes from\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one percent · four sources shade the dial · read it like a clock\u003c/div>\n \u003csvg id=\"tickdonut\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK DONUT · PALM · ACQUISITION\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 横向条形\u003c/span>\n \u003ch2>Six teams, shipped and counted\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one release this quarter · the row is a queue, not a bar\u003c/div>\n \u003csvg id=\"tickrows\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK ROWS · PALM · RELEASE LOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 分组柱\u003c/span>\n \u003ch2>This year against last, plan by plan\u003c/h2>\n \u003cdiv class=\"sub\">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k\u003c/div>\n \u003csvg id=\"pairrungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PAIRED RUNGS · PALM · BILLING YoY\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 堆叠柱\u003c/span>\n \u003ch2>Where each region's revenue sits\u003c/h2>\n \u003cdiv class=\"sub\">one rung = $1k · three products stack up the ladder, strongest carries the most\u003c/div>\n \u003csvg id=\"stackrungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">STACKED RUNGS · PALM · REVENUE MIX\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 散点\u003c/span>\n \u003ch2>Price against satisfaction, twelve products\u003c/h2>\n \u003cdiv class=\"sub\">every dot hangs a plumb line to the floor · read x by the line, y by the height\u003c/div>\n \u003csvg id=\"plumb\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PLUMB SCATTER · PALM · PRODUCT PANEL\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 瀑布\u003c/span>\n \u003ch2>From gross to net, step by step\u003c/h2>\n \u003cdiv class=\"sub\">one rung = $1k · solid rungs add, dashed rungs take away · the ladder walks down\u003c/div>\n \u003csvg id=\"fall\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG WATERFALL · PALM · P&L\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 热力\u003c/span>\n \u003ch2>When support gets loud\u003c/h2>\n \u003cdiv class=\"sub\">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible\u003c/div>\n \u003csvg id=\"dotheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DOT HEAT · PALM · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 进度\u003c/span>\n \u003ch2>How far to the quarter's goal\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one percent of target · inked = earned · the dial reads left to right\u003c/div>\n \u003csvg id=\"gauge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK GAUGE · PALM · QUARTERLY TARGET\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 哑铃对比\u003c/span>\n \u003ch2>Onboarding, before and after the redesign\u003c/h2>\n \u003cdiv class=\"sub\">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable\u003c/div>\n \u003csvg id=\"dumbbell\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DUMBBELL QUEUE · PALM · UX TIMING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 基础型 · 矩形树图\u003c/span>\n \u003ch2>Where the work went\u003c/h2>\n \u003cdiv class=\"sub\">1,280 shipped hours · area = effort · hue = top-level product family\u003c/div>\n \u003cdiv id=\"treemap\" class=\"chartbox\" role=\"img\" aria-label=\"Palm treemap of shipped hours nested by product family\">\u003c/div>\n \u003cdiv class=\"src\">NESTED TREEMAP · PALM · WORK PORTFOLIO\u003c/div>\n \u003c/div>\n\n\n \u003c!-- F14 · rung histogram -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 直方图\u003c/span>\n \u003ch2>Most tickets resolve within six hours\u003c/h2>\n \u003cdiv class=\"sub\">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile\u003c/div>\n \u003csvg id=\"histo\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG HISTOGRAM · PALM · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- F15 · tick box -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 箱线图\u003c/span>\n \u003ch2>Reply times, boxed by plan\u003c/h2>\n \u003cdiv class=\"sub\">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed\u003c/div>\n \u003csvg id=\"boxplot\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK BOX · PALM · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- F16 · stream ribbon -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 基础型 · STREAMGRAPH\u003c/span>\n \u003ch2>Three products trade the same river\u003c/h2>\n \u003cdiv class=\"sub\">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows\u003c/div>\n \u003csvg id=\"stream\" viewBox=\"0 0 840 300\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:300px\">\u003c/svg>\n \u003cdiv class=\"src\">STREAM RIBBON · PALM · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- F17 · candlestick -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · K 线\u003c/span>\n \u003ch2>Six weeks of the token, candle by candle\u003c/h2>\n \u003cdiv class=\"sub\">body = open to close · wick = the day's full reach · green = closed down · wheat = closed up\u003c/div>\n \u003csvg id=\"candle\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">CANDLESTICK · PALM · MARKET DATA\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── PALM 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.32)';\nconst FLOOR='rgba(88,64,46,.24)',QUIET='rgba(88,64,46,.15)',TRACK='rgba(88,64,46,.12)',GRID='rgba(88,64,46,.16)';\nconst DATA='#43593B',DATA2='#58402E',HERO='#D4A017';\nconst FAINTDATA='#ACAD79',BEAD='#77835A';\n// 序数梯(明度∝数值,大→小 = 深→浅):给排序过的类目用\nconst RAMP6=['#43593B','#5A7049','#77835A','#929960','#ACAD79','#F2D17E'];\n// 分段色:月份 / 进度区段等图内既有维度\nconst SEG3=['#43593B','#77835A','#ACAD79'], ZONE3=['#ACAD79','#929960','#43593B'];\nconst CAT4=[\"#43593B\", \"#77835A\", \"#ACAD79\", \"#F2D17E\"],CAT3=[\"#43593B\", \"#929960\", \"#F2D17E\"],CAT3L=[\"#43593B\", \"#77835A\", \"#ACAD79\"],HEAT=[\"#43593B\", \"#929960\", \"#F2D17E\"];\nconst HEROMODE=false; // 黑体红睛模式:每张图一个主角元素整体走 HERO\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\nconst D2R=Math.PI/180;\nconst pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>{n.innerHTML='';fn(n)};\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ B1 · rung bars ════\n(()=>{\nconst D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];\nobsReveal('rungs',s=>{\n const x0=i=>56+i*56,base=266,step=5.6,HW=14;\n D.forEach(([name,v],i)=>{\n const x=x0(i);\n const colInk=(HEROMODE&&i===0)?HERO:RAMP6[i];\n for(let k=0;k\u003cv;k++){\n const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n }\n const topY=base-(v-1)*step;\n const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.4+i*.08}s`},v);\n tip(num,`${name} — ${v}k MRR`);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n });\n el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},\n 'ONE RUNG = $1K · SHADE = RANK · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ B2 · hairline line ════\n(()=>{\nconst N=30;\nconst val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;\nobsReveal('dayline',s=>{\n const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,\n class:'fade',style:`animation-delay:${d*.008}s`});\n el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n const vs=Array.from({length:N},(_,d)=>val(d));\n const top=[];\n for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){\n if(top.every(t=>Math.abs(t-d)>=5))top.push(d);\n if(top.length===2)break;\n }\n const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');\n el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,\n class:'draw',style:'animation-duration:1.2s'});\n vs.forEach((v,d)=>{\n const weekend=d%7===5||d%7===6,big=top.includes(d);\n const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?5.2:3.1,\n fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,\n class:'pop',style:`animation-delay:${.2+d*.03}s`});\n tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);\n if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,\n class:'fade'},Math.round(v));\n });\n [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>\n txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'ONE DOT = ONE DAY · HOLLOW = WEEKEND');\n});\n})();\n\n// ════ B3 · hairline area ════\n(()=>{\nconst N=45;\nconst val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;\nobsReveal('hairarea',s=>{\n const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;\n el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n const vs=Array.from({length:N},(_,d)=>val(d));\n const peak=vs.indexOf(Math.max(...vs));\n vs.forEach((v,d)=>{\n el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),\n stroke:d===peak?HERO:SEG3[Math.min(2,Math.floor(d/15))],'stroke-width':d===peak?1.6:1,\n opacity:d===peak?1:.8+rnd(d+1,7)*.2,\n class:'fade',style:`animation-delay:${d*.014}s`});\n });\n const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');\n el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2,pathLength:1,\n class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});\n const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:HERO,\n class:'pop',style:'animation-delay:1.2s'});\n tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);\n txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1.3s`,\n class:'fade'},Math.round(vs[peak])+'k');\n [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>\n txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},\n 'ONE HAIRLINE = ONE DAY · SHADE = MONTH · FLOOR TO PEAK');\n});\n})();\n\n// ════ B4 · tick donut ════\n(()=>{\nconst D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];\nobsReveal('tickdonut',s=>{\n const cx=200,cy=148,R0=64;\n let k0=0;\n D.forEach(([name,v,shade],si)=>{\n for(let k=0;k\u003cv;k++){\n const idx=k0+k,a=idx*3.6-90;\n const len=12+rnd(idx+1,si+2)*6;\n const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);\n el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,\n class:'fade',style:`animation-delay:${idx*.012}s`});\n if(idx%10===0){\n const [dx,dy]=pol(cx,cy,R0-5,a);\n el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${idx*.012}s`});\n }\n }\n const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);\n el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,\n 'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});\n const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)\u003c-0.3?'end':'middle';\n const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,\n 'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,\n class:'fade'},`${name} · ${v}`);\n tip(lab,`${name} — ${v}% of traffic`);\n k0+=v;\n });\n txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.9s'},'100');\n txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');\n txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');\n});\n})();\n\n// ════ C1 · tick rows ════\n(()=>{\nconst D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];\nobsReveal('tickrows',s=>{\n const y0=i=>52+i*44,X0=104,PX=6.9;\n D.forEach(([name,v],i)=>{\n const y=y0(i);\n txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,\n class:'fade',style:`animation-delay:${i*.08}s`});\n const rowInk=(HEROMODE&&i===0)?HERO:RAMP6[i];\n for(let k=0;k\u003cv;k++){\n const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;\n el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:rowInk,'stroke-width':1.7,\n opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n }\n const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:TXT,\n class:'fade',style:`animation-delay:${.4+i*.08}s`},v);\n tip(lab,`${name} — ${v} releases`);\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},\n 'ONE TICK = ONE RELEASE · SHADE = RANK · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ C2 · paired rungs ════\n(()=>{\nconst D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];\nobsReveal('pairrungs',s=>{\n const x0=i=>64+i*66,base=258,step=5.4,HW=10;\n D.forEach(([name,was,now],i)=>{\n const xa=x0(i)-13,xb=x0(i)+13;\n for(let k=0;k\u003cwas;k++){\n const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;\n el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.8,\n opacity:.75+rnd(k+2,i+3)*.25,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});\n }\n for(let k=0;k\u003cnow;k++){\n const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;\n el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:DATA,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+8)*.15,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});\n }\n const topB=base-(now-1)*step;\n const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:HEROMODE?HERO:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.5+i*.08}s`},now);\n tip(num,`${name} — ${was}k → ${now}k`);\n txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:FAINTDATA,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.5+i*.08}s`},was);\n txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n });\n el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'FAINT = 2025 · SOLID = 2026 · ONE RUNG = $1K');\n});\n})();\n\n// ════ C3 · stacked rungs ════\n(()=>{\nconst D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];\nconst SEG=['CORE','ADD-ONS','SERVICES'];\nobsReveal('stackrungs',s=>{\n const x0=i=>72+i*76,base=262,step=5.2,HW=13;\n D.forEach(([name,segs],i)=>{\n const x=x0(i);let k0=0;\n segs.forEach((v,si)=>{\n for(let k=0;k\u003cv;k++){\n const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:CAT3[si],'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+si+4)*.15,class:'fade',\n style:`animation-delay:${i*.09+(k0+k)*.012}s`});\n }\n const midY=base-(k0+v/2+si)*step;\n const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:CAT3L[si],\n class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);\n tip(lab,`${name} ${SEG[si]} — ${v}k`);\n k0+=v;\n });\n const total=segs[0]+segs[1]+segs[2];\n txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.6+i*.09}s`},total);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);\n });\n el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'STRONGEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');\n});\n})();\n\n// ════ C4 · plumb scatter ════\n(()=>{\nconst P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],\n ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],\n ['Hub',76,32],['Forms',34,38]];\nobsReveal('plumb',s=>{\n const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;\n for(let g=0;g\u003c=20;g++){\n const x=X0+g/20*(X1-X0);\n el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:FLOOR,'stroke-width':.6,\n class:'fade',style:`animation-delay:${g*.01}s`});\n }\n el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'CHEAP');\n txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',\n class:'fade'},'PREMIUM');\n const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]\u003ca[2]?b:a);\n P.forEach(([name,px,sat],i)=>{\n const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];\n el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1,\n opacity:.75,class:'fade',style:`animation-delay:${.2+i*.05}s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:hero?5.4:3.4,fill:hero?HERO:DATA,\n class:'pop',style:`animation-delay:${.25+i*.05}s`});\n tip(dot,`${name} — price ${px} · satisfaction ${sat}`);\n if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.8}s`,\n class:'fade'},`${name} · ${sat}`);\n });\n txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:FAINT,\n transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');\n});\n})();\n\n// ════ C5 · rung waterfall ════\n(()=>{\nconst D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];\nobsReveal('fall',s=>{\n const x0=i=>58+i*72,step=5.2,HW=11;\n let lv=0;const rows=[];\n D.forEach(([name,v],i)=>{\n if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}\n else if(name==='NET'){rows.push([name,lv,0,lv])}\n else{rows.push([name,v,lv+v,lv]);lv=lv+v}\n });\n const base=252,yOf=k=>base-k*step;\n rows.forEach(([name,v,lo,hi],i)=>{\n const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v\u003c0&&!isTotal;\n const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);\n const colInk=name==='NET'?HERO:DATA; // NET 是这张图的结论,常驻强调色\n for(let k=0;k\u003cn;k++){\n const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;\n if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA2,'stroke-width':1.8,\n 'stroke-dasharray':'2.5 2.5',opacity:.85,class:'fade',\n style:`animation-delay:${i*.12+k*.014}s`});\n else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});\n }\n if(i\u003crows.length-1){\n const nx=x0(i+1);\n const lvl=isTotal?v:(neg?lo:hi);\n el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:FAINT,\n 'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',\n style:`animation-delay:${.3+i*.12}s`});\n }\n const topY=yOf(Math.max(from,to));\n const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,\n fill:neg?DATA2:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));\n tip(num,`${name} — ${neg?'−':''}${Math.abs(v)}k`);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);\n });\n el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'SOLID ADD · DASHED TAKE AWAY · NET IN ACCENT · ONE RUNG = $1K');\n});\n})();\n\n// ════ C6 · dot heat ════\n(()=>{\nconst DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nobsReveal('dotheat',s=>{\n const x0=j=>64+j*27,y0=i=>58+i*30;\n let max=0,mi=0,mj=0;\n const v=(i,j)=>{\n const day=i\u003c5?1:.32;\n const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5);\n const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);\n return Math.round(raw);\n };\n for(let i=0;i\u003c7;i++)for(let j=0;j\u003c12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}\n for(let i=0;i\u003c7;i++){\n txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);\n for(let j=0;j\u003c12;j++){\n const t=v(i,j),x=x0(j),y=y0(i);\n if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,\n class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}\n const hero=i===mi&&j===mj;\n const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,\n fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],\n class:'pop',style:`animation-delay:${i*.05+j*.015}s`});\n tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);\n if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',\n stroke:HERO,'stroke-width':1,'stroke-dasharray':'2 3',\n class:'fade',style:'animation-delay:1s'});\n }\n }\n for(let j=0;j\u003c12;j+=2)\n txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);\n});\n})();\n\n// ════ C7 · tick gauge ════\n(()=>{\nconst GOAL=73;\nobsReveal('gauge',s=>{\n const cx=200,cy=190,R0=104,A0=-195,SW=210;\n for(let k=0;k\u003c100;k++){\n const a=A0+k/100*SW,inked=k\u003cGOAL;\n const len=inked?15+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;\n const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);\n el(s,'line',{x1,y1,x2,y2,stroke:inked?ZONE3[Math.min(2,Math.floor(k/25))]:FLOOR,'stroke-width':inked?1.9:.6,\n class:'fade',style:`animation-delay:${k*.012}s`});\n }\n [25,50,75,100].forEach(m=>{\n const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);\n el(s,'circle',{cx:dx,cy:dy,r:1,fill:FAINT,class:'fade',style:'animation-delay:.8s'});\n txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.85s'},m);\n });\n const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);\n el(s,'circle',{cx:ex,cy:ey,r:3.2,fill:HERO,class:'pop',style:'animation-delay:1.1s'});\n const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},GOAL+'%');\n tip(num,`$730k of the $1M quarter target`);\n txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE TICK = 1% OF TARGET · SHADE = QUARTER · INKED = EARNED');\n});\n})();\n\n// ════ C8 · dumbbell queue ════\n(()=>{\nconst D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];\nobsReveal('dumbbell',s=>{\n const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);\n D.forEach(([name,was,now],i)=>{\n const y=y0(i),xa=mapX(was),xb=mapX(now);\n txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:TRACK,'stroke-width':.7,\n class:'fade',style:`animation-delay:${i*.08}s`});\n const n=was-now;\n for(let k=0;k\u003cn;k++){\n const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;\n el(s,'circle',{cx:x,cy:yy,r:2.1+rnd(k+2,i+4)*.9,fill:BEAD,opacity:.95,\n class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});\n }\n el(s,'circle',{cx:xa,cy:y,r:4.8,fill:BG,stroke:DATA,'stroke-width':1.7,\n class:'pop',style:`animation-delay:${.2+i*.08}s`});\n const after=el(s,'circle',{cx:xb,cy:y,r:5.2,fill:HERO,\n class:'pop',style:`animation-delay:${.6+i*.08}s`});\n tip(after,`${name} — ${was} min → ${now} min`);\n txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:FAINTDATA,\n class:'fade',style:`animation-delay:${.3+i*.08}s`},was);\n txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:TXT,'text-anchor':'end',\n class:'fade',style:`animation-delay:${.7+i*.08}s`},now);\n });\n txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'FASTER ←');\n txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',\n class:'fade'},'MINUTES');\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · SOLID = AFTER');\n});\n})();\n\n// ════ C9 · nested treemap ════\n(()=>{\nconst TOTAL=1280;\nconst leaf=(name,value,color,labelColor=BG)=>({name,value,itemStyle:{color},label:{color:labelColor}});\nconst TREE_DATA=[\n {name:'PRODUCT · 50%',children:[leaf('Core app',260,DATA),leaf('Collaboration',190,DATA),leaf('Search',110,DATA),leaf('Mobile',80,DATA)]},\n {name:'PLATFORM · 31%',children:[leaf('AI systems',170,BEAD),leaf('Data infra',120,BEAD),leaf('APIs',105,BEAD)]},\n {name:'GROWTH · 19%',children:[leaf('Acquisition',90,HERO,TXT),leaf('Retention',80,HERO,TXT),leaf('Onboarding',75,HERO,TXT)]},\n];\nconst host=document.getElementById('treemap');let chart;\nconst draw=()=>{chart=chart||echarts.init(host,null,{renderer:'svg'});chart.clear();chart.setOption({\n animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},\n tooltip:{backgroundColor:BG,borderColor:TXT,borderWidth:1,textStyle:{color:TXT,fontSize:12},\n formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}\u003cbr>\u003cb>${i.value} hours\u003c/b> · ${(i.value/TOTAL*100).toFixed(1)}%`},\n series:[{type:'treemap',data:TREE_DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,\n label:{show:true,position:'insideTopLeft',padding:[10,8],color:TXT,fontSize:12,fontWeight:700,lineHeight:18,\n formatter:p=>p.treePathInfo.length\u003c=1?'':p.treePathInfo.length===2?p.name:`${p.name}\\n${p.value} h`},\n upperLabel:{show:true,height:32,padding:[0,10],color:TXT,fontSize:10,fontWeight:700,backgroundColor:BG},\n itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2},emphasis:{focus:'ancestor',itemStyle:{borderColor:TXT,borderWidth:1.5}},\n levels:[{itemStyle:{borderWidth:0,gapWidth:5}},{upperLabel:{show:true,height:32},itemStyle:{borderColor:BG,borderWidth:3,gapWidth:3}},{upperLabel:{show:false},itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2}}]}]});};\nconst io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});\nio.observe(host);host.style.cursor='pointer';host.addEventListener('click',draw);addEventListener('resize',()=>chart?.resize());\n})();\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ F14 · rung histogram ════\n// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单\n// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,\n// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。\n(()=>{\nconst BIN=[6,14,22,19,13,9,6,4,3,2,1,1]; // 2h bins, sums to 100\n// 序数梯:箱越高绿越深(票堆的大小是序数维度,同 glance-palm 的 rampAt 逻辑)\nconst BMAX=Math.max(...BIN);\nconst bcol=v=>v>BMAX*.75?INK:v>BMAX*.45?'#77835A':v>BMAX*.2?'#929960':'#ACAD79';\nobsReveal('histo',s=>{\n const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;\n BIN.forEach((v,i)=>{\n const x=X0+i*PW+PW/2;\n for(let k=0;k\u003cv;k++){\n const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:bcol(v),'stroke-width':1.8,\n opacity:.68+rnd(k+2,i+4)*.32,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'rgba(88,64,46,.32)',\n class:'fade',style:`animation-delay:${i*.06+k*.012}s`});\n }\n });\n // peak label\n const pk=BIN.indexOf(Math.max(...BIN));\n const px=X0+pk*PW+PW/2;\n const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,\n 'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);\n tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);\n // bin edges: the continuous axis is the histogram's identity\n el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let e=0;e\u003c=12;e++){\n const x=X0+e*PW;\n el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${e*.02}s`});\n if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');\n }\n // median flag: cumulative crosses 50 inside bin 2\n let acc=0,mb=0;for(let i=0;i\u003c12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}\n const mx=X0+mb*PW+PW*.7;\n el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#929960','stroke-width':1.62,\n 'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});\n txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'rgba(88,64,46,.72)',\n class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE RUNG = ONE TICKET IN A HUNDRED · DEEPER GREEN = TALLER PILE · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ F15 · tick box ════\n// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,\n// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。\n(()=>{\nconst G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],\n ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],\n ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],\n ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];\nconst SHADE=[INK,'#77835A','#929960','#ACAD79'];\nobsReveal('boxplot',s=>{\n const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);\n const colX=g=>78+g*84,BW=24;\n [0,6,12,18,24].forEach(h=>{\n el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(88,64,46,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${h*.008}s`});\n txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.008}s`},h+'h');\n });\n G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{\n const x=colX(g);\n el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:SHADE[g],'stroke-width':1.44,\n pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});\n [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),\n stroke:SHADE[g],'stroke-width':1.8,class:'fade',style:`animation-delay:${.2+g*.12}s`}));\n const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,\n fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});\n tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);\n el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,\n class:'fade',style:`animation-delay:${.5+g*.12}s`});\n txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':9.5,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');\n outs.forEach((o,k)=>{\n const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,\n stroke:SHADE[g],'stroke-width':1.3,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});\n tip(d,`${name} outlier — ${o}h`);\n });\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · HOLLOW = OUTLIER · DARKEST = FASTEST');\n});\n})();\n\n// ════ F16 · stream ribbon ════\n// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴\n// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。\n// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。\n(()=>{\nconst N=48;\nconst mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>\n Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));\n// 三条带走绿→黄梯:现任霸主最深绿,退场的 LEGACY 落在麦黄(不再用灰褐透明档)\nconst SERIES=[\n ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'#F2D17E'],\n ['BOARDS',mk(26,.10,11,4.2,5),'#929960'],\n ['FLOWS',mk(12,.78,10,3.1,8),INK],\n];\nobsReveal('stream',s=>{\n const X0=36,X1=744,CY=138,SC=1.15;\n const x=t=>X0+t/(N-1)*(X1-X0);\n const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));\n // stacked offsets, silhouette baseline: top of stack starts at -total/2\n const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);\n const smooth=pts=>{\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let k=1;k\u003cpts.length-1;k++){\n const p=pts[k],q=pts[k+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)}`;\n }\n return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;\n };\n let run=y0.slice();\n SERIES.forEach(([name,vals,shade],si)=>{\n const top=run.map((v,t)=>[x(t),v]);\n const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);\n run=bot.map(p=>p[1]);\n const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';\n const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,\n class:'fade',style:`animation-delay:${si*.16}s`});\n tip(band,`${name} — weekly active accounts, 48 weeks`);\n // label at the band's widest week — clamped off both ends so the\n // paper-colored text never spills past the band onto paper (invisible)\n let wk=4;vals.forEach((v,t)=>{if(t>=4&&t\u003c=N-6&&v>vals[wk])wk=t});\n const midY=(top[wk][1]+bot[wk][1])/2;\n const dark=shade===INK;\n txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',\n fill:dark?PAPER:'#58402E','letter-spacing':'.06em',\n class:'fade',style:`animation-delay:${.5+si*.14}s`},name);\n });\n // month floor: barcode ticks + labels every 8 weeks\n const base=252;\n el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let t=0;t\u003cN;t++)\n el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${t*.006}s`});\n ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>\n txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'BAND WIDTH = WEEKLY ACTIVES · DARKEST = TODAY’S LEADER · THE RIVER IS THE TOTAL');\n});\n})();\n\n// ════ F17 · candlestick ════\n// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,\n// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。\n// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。\n(()=>{\nconst N=30;\n// deterministic random walk with a dip-and-recover arc\nconst DAYS=[];\nlet px=52;\nfor(let d=0;d\u003cN;d++){\n const drift=d\u003c10?.4:d\u003c18?-1.5:1.3;\n const open=px;\n const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);\n const hi=Math.max(open,close)+rnd(d+2,7)*2.6;\n const lo=Math.min(open,close)-rnd(d+3,11)*2.6;\n DAYS.push([open,close,hi,lo]);\n px=close;\n}\nobsReveal('candle',s=>{\n const X0=40,PW=11.2,BASE=258,TOP=44;\n const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));\n const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);\n const x=d=>X0+d*PW+PW/2;\n // price grid\n for(let g=0;g\u003c=4;g++){\n const v=vmin+g/4*(vmax-vmin);\n el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'rgba(88,64,46,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${g*.03}s`});\n txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${g*.03}s`},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v));\n }\n let hiD=0,loD=0;\n DAYS.forEach(([o,c,h,l],d)=>{\n if(h>DAYS[hiD][2])hiD=d;\n if(l\u003cDAYS[loD][3])loD=d;\n });\n DAYS.forEach(([o,c,h,l],d)=>{\n const cx=x(d),up=c>=o;\n const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));\n // wick: one hairline, full reach\n el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'rgba(88,64,46,.72)','stroke-width':1.26,\n class:'fade',style:`animation-delay:${d*.03}s`});\n // body: capsule; 涨跌是两个类目——深绿 = 收跌,麦黄 = 收涨(palm 的双分类位)\n const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,\n ...(up?{fill:'#F2D17E',stroke:'#77835A','stroke-width':1}:{fill:INK}),\n class:'fade',style:`animation-delay:${.05+d*.03}s`});\n tip(body,`Day ${d+1} — open ${o.toFixed(1)} · close ${c.toFixed(1)} · high ${h.toFixed(1)} · low ${l.toFixed(1)}`);\n // extremes, labeled with hairline flags\n if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(h));\n if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'rgba(88,64,46,.72)','text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(l));\n });\n // week ticks on the floor\n el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${d*.008}s`});\n ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>\n txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade'},w));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'GREEN BODY = CLOSED DOWN · WHEAT = CLOSED UP · WICK = THE DAY’S FULL REACH');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":48341,"content_sha256":"fddcd007a66b81f288be7bafc0d2dd81021f4d90414788992b75b3eccd592458"},{"filename":"templates/color/basics-porcelain.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Basics F1–F17 × PORCELAIN\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap\" rel=\"stylesheet\">\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003cstyle>\n :root{--bg:#F7F2EB;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.32);--grid:rgba(8,31,92,.16)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed var(--faint);color:var(--muted)}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n .chartbox{width:100%;height:360px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>基础型组 F1–F17 × PORCELAIN\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#334EAC\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#7096D1\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#BAD6EB\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#D0E3FF\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#081F5C\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#F7F2EB\">\u003c/span>  青瓷蓝家族 · Royal 当主墨,Midnight 当文字与最深档,九色号全原厂。几何、动画、数据与 basics-gallery 一致;彩色版统一加墨(线宽 ×1.8 / 透明度地板 .85,dot heat 除外——它的墨本来就够)。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 柱状\u003c/span>\n \u003ch2>Revenue by plan, rung by rung\u003c/h2>\n \u003cdiv class=\"sub\">one rung = one $k of MRR · the bar is a ladder you can count\u003c/div>\n \u003csvg id=\"rungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG BARS · PORCELAIN · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 折线\u003c/span>\n \u003ch2>Thirty days of sign-ups\u003c/h2>\n \u003cdiv class=\"sub\">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest\u003c/div>\n \u003csvg id=\"dayline\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HAIRLINE LINE · PORCELAIN · GROWTH\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 面积\u003c/span>\n \u003ch2>Concurrent users, filled with days\u003c/h2>\n \u003cdiv class=\"sub\">one hairline = one day, floor to peak · the area is made of days, not paint\u003c/div>\n \u003csvg id=\"hairarea\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HAIRLINE AREA · PORCELAIN · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 环形\u003c/span>\n \u003ch2>Where the traffic comes from\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one percent · four sources shade the dial · read it like a clock\u003c/div>\n \u003csvg id=\"tickdonut\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK DONUT · PORCELAIN · ACQUISITION\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 横向条形\u003c/span>\n \u003ch2>Six teams, shipped and counted\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one release this quarter · the row is a queue, not a bar\u003c/div>\n \u003csvg id=\"tickrows\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK ROWS · PORCELAIN · RELEASE LOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 分组柱\u003c/span>\n \u003ch2>This year against last, plan by plan\u003c/h2>\n \u003cdiv class=\"sub\">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k\u003c/div>\n \u003csvg id=\"pairrungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PAIRED RUNGS · PORCELAIN · BILLING YoY\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 堆叠柱\u003c/span>\n \u003ch2>Where each region's revenue sits\u003c/h2>\n \u003cdiv class=\"sub\">one rung = $1k · three products stack up the ladder, strongest carries the most\u003c/div>\n \u003csvg id=\"stackrungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">STACKED RUNGS · PORCELAIN · REVENUE MIX\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 散点\u003c/span>\n \u003ch2>Price against satisfaction, twelve products\u003c/h2>\n \u003cdiv class=\"sub\">every dot hangs a plumb line to the floor · read x by the line, y by the height\u003c/div>\n \u003csvg id=\"plumb\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PLUMB SCATTER · PORCELAIN · PRODUCT PANEL\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 瀑布\u003c/span>\n \u003ch2>From gross to net, step by step\u003c/h2>\n \u003cdiv class=\"sub\">one rung = $1k · solid rungs add, dashed rungs take away · the ladder walks down\u003c/div>\n \u003csvg id=\"fall\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG WATERFALL · PORCELAIN · P&L\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 热力\u003c/span>\n \u003ch2>When support gets loud\u003c/h2>\n \u003cdiv class=\"sub\">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible\u003c/div>\n \u003csvg id=\"dotheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DOT HEAT · PORCELAIN · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 进度\u003c/span>\n \u003ch2>How far to the quarter's goal\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one percent of target · inked = earned · the dial reads left to right\u003c/div>\n \u003csvg id=\"gauge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK GAUGE · PORCELAIN · QUARTERLY TARGET\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 哑铃对比\u003c/span>\n \u003ch2>Onboarding, before and after the redesign\u003c/h2>\n \u003cdiv class=\"sub\">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable\u003c/div>\n \u003csvg id=\"dumbbell\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DUMBBELL QUEUE · PORCELAIN · UX TIMING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 基础型 · 矩形树图\u003c/span>\n \u003ch2>Where the work went\u003c/h2>\n \u003cdiv class=\"sub\">1,280 shipped hours · area = effort · blue lightness = parent portfolio share\u003c/div>\n \u003cdiv id=\"treemap\" class=\"chartbox\" role=\"img\" aria-label=\"Porcelain treemap of shipped hours nested by product family\">\u003c/div>\n \u003cdiv class=\"src\">NESTED TREEMAP · PORCELAIN · WORK PORTFOLIO\u003c/div>\n \u003c/div>\n\n\n \u003c!-- F14 · rung histogram -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 直方图\u003c/span>\n \u003ch2>Most tickets resolve within six hours\u003c/h2>\n \u003cdiv class=\"sub\">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile\u003c/div>\n \u003csvg id=\"histo\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG HISTOGRAM · PORCELAIN · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- F15 · tick box -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 箱线图\u003c/span>\n \u003ch2>Reply times, boxed by plan\u003c/h2>\n \u003cdiv class=\"sub\">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed\u003c/div>\n \u003csvg id=\"boxplot\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK BOX · PORCELAIN · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- F16 · stream ribbon -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 基础型 · STREAMGRAPH\u003c/span>\n \u003ch2>Three products trade the same river\u003c/h2>\n \u003cdiv class=\"sub\">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows\u003c/div>\n \u003csvg id=\"stream\" viewBox=\"0 0 840 300\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:300px\">\u003c/svg>\n \u003cdiv class=\"src\">STREAM RIBBON · PORCELAIN · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- F17 · candlestick -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · K 线\u003c/span>\n \u003ch2>Six weeks of the token, candle by candle\u003c/h2>\n \u003cdiv class=\"sub\">body = open to close · wick = the day's full reach · ink = closed down · hollow = closed up\u003c/div>\n \u003csvg id=\"candle\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">CANDLESTICK · PORCELAIN · MARKET DATA\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── PORCELAIN 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F7F2EB',TXT='#081F5C',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.32)';\nconst FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',TRACK='rgba(8,31,92,.12)',GRID='rgba(8,31,92,.16)';\nconst DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C';\nconst FAINTDATA='#BAD6EB',BEAD='#7096D1';\nconst CAT4=[\"#081F5C\", \"#334EAC\", \"#7096D1\", \"#BAD6EB\"],CAT3=[\"#334EAC\", \"#7096D1\", \"#BAD6EB\"],CAT3L=[\"#334EAC\", \"#7096D1\", \"rgba(8,31,92,.6)\"],HEAT=[\"#334EAC\", \"#7096D1\", \"#BAD6EB\"];\nconst HEROMODE=false; // 黑体红睛模式:每张图一个主角元素整体走 HERO\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\nconst D2R=Math.PI/180;\nconst pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>{n.innerHTML='';fn(n)};\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ B1 · rung bars ════\n(()=>{\nconst D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];\nobsReveal('rungs',s=>{\n const x0=i=>56+i*56,base=266,step=5.6,HW=14;\n D.forEach(([name,v],i)=>{\n const x=x0(i);\n const colInk=(HEROMODE&&i===0)?HERO:DATA;\n for(let k=0;k\u003cv;k++){\n const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n }\n const topY=base-(v-1)*step;\n const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.4+i*.08}s`},v);\n tip(num,`${name} — ${v}k MRR`);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n });\n el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},\n 'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ B2 · hairline line ════\n(()=>{\nconst N=30;\nconst val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;\nobsReveal('dayline',s=>{\n const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,\n class:'fade',style:`animation-delay:${d*.008}s`});\n el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n const vs=Array.from({length:N},(_,d)=>val(d));\n const top=[];\n for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){\n if(top.every(t=>Math.abs(t-d)>=5))top.push(d);\n if(top.length===2)break;\n }\n const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');\n el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,\n class:'draw',style:'animation-duration:1.2s'});\n vs.forEach((v,d)=>{\n const weekend=d%7===5||d%7===6,big=top.includes(d);\n const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?5.2:3.1,\n fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,\n class:'pop',style:`animation-delay:${.2+d*.03}s`});\n tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);\n if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,\n class:'fade'},Math.round(v));\n });\n [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>\n txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'ONE DOT = ONE DAY · HOLLOW = WEEKEND');\n});\n})();\n\n// ════ B3 · hairline area ════\n(()=>{\nconst N=45;\nconst val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;\nobsReveal('hairarea',s=>{\n const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;\n el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n const vs=Array.from({length:N},(_,d)=>val(d));\n const peak=vs.indexOf(Math.max(...vs));\n vs.forEach((v,d)=>{\n el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),\n stroke:d===peak?HERO:DATA,'stroke-width':d===peak?1.6:1,\n opacity:d===peak?1:.8+rnd(d+1,7)*.2,\n class:'fade',style:`animation-delay:${d*.014}s`});\n });\n const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');\n el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2,pathLength:1,\n class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});\n const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:HERO,\n class:'pop',style:'animation-delay:1.2s'});\n tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);\n txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1.3s`,\n class:'fade'},Math.round(vs[peak])+'k');\n [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>\n txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},\n 'ONE HAIRLINE = ONE DAY, FLOOR TO PEAK');\n});\n})();\n\n// ════ B4 · tick donut ════\n(()=>{\nconst D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];\nobsReveal('tickdonut',s=>{\n const cx=200,cy=148,R0=64;\n let k0=0;\n D.forEach(([name,v,shade],si)=>{\n for(let k=0;k\u003cv;k++){\n const idx=k0+k,a=idx*3.6-90;\n const len=12+rnd(idx+1,si+2)*6;\n const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);\n el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,\n class:'fade',style:`animation-delay:${idx*.012}s`});\n if(idx%10===0){\n const [dx,dy]=pol(cx,cy,R0-5,a);\n el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${idx*.012}s`});\n }\n }\n const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);\n el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,\n 'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});\n const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)\u003c-0.3?'end':'middle';\n const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,\n 'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,\n class:'fade'},`${name} · ${v}`);\n tip(lab,`${name} — ${v}% of traffic`);\n k0+=v;\n });\n txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.9s'},'100');\n txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');\n txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');\n});\n})();\n\n// ════ C1 · tick rows ════\n(()=>{\nconst D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];\nobsReveal('tickrows',s=>{\n const y0=i=>52+i*44,X0=104,PX=6.9;\n D.forEach(([name,v],i)=>{\n const y=y0(i);\n txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,\n class:'fade',style:`animation-delay:${i*.08}s`});\n const rowInk=(HEROMODE&&i===0)?HERO:DATA;\n for(let k=0;k\u003cv;k++){\n const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;\n el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:rowInk,'stroke-width':1.7,\n opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n }\n const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:TXT,\n class:'fade',style:`animation-delay:${.4+i*.08}s`},v);\n tip(lab,`${name} — ${v} releases`);\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},\n 'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ C2 · paired rungs ════\n(()=>{\nconst D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];\nobsReveal('pairrungs',s=>{\n const x0=i=>64+i*66,base=258,step=5.4,HW=10;\n D.forEach(([name,was,now],i)=>{\n const xa=x0(i)-13,xb=x0(i)+13;\n for(let k=0;k\u003cwas;k++){\n const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;\n el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.8,\n opacity:.75+rnd(k+2,i+3)*.25,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});\n }\n for(let k=0;k\u003cnow;k++){\n const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;\n el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:DATA,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+8)*.15,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});\n }\n const topB=base-(now-1)*step;\n const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:HEROMODE?HERO:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.5+i*.08}s`},now);\n tip(num,`${name} — ${was}k → ${now}k`);\n txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:FAINTDATA,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.5+i*.08}s`},was);\n txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n });\n el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'FAINT = 2025 · SOLID = 2026 · ONE RUNG = $1K');\n});\n})();\n\n// ════ C3 · stacked rungs ════\n(()=>{\nconst D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];\nconst SEG=['CORE','ADD-ONS','SERVICES'];\nobsReveal('stackrungs',s=>{\n const x0=i=>72+i*76,base=262,step=5.2,HW=13;\n D.forEach(([name,segs],i)=>{\n const x=x0(i);let k0=0;\n segs.forEach((v,si)=>{\n for(let k=0;k\u003cv;k++){\n const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:CAT3[si],'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+si+4)*.15,class:'fade',\n style:`animation-delay:${i*.09+(k0+k)*.012}s`});\n }\n const midY=base-(k0+v/2+si)*step;\n const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:CAT3L[si],\n class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);\n tip(lab,`${name} ${SEG[si]} — ${v}k`);\n k0+=v;\n });\n const total=segs[0]+segs[1]+segs[2];\n txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.6+i*.09}s`},total);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);\n });\n el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'STRONGEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');\n});\n})();\n\n// ════ C4 · plumb scatter ════\n(()=>{\nconst P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],\n ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],\n ['Hub',76,32],['Forms',34,38]];\nobsReveal('plumb',s=>{\n const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;\n for(let g=0;g\u003c=20;g++){\n const x=X0+g/20*(X1-X0);\n el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:FLOOR,'stroke-width':.6,\n class:'fade',style:`animation-delay:${g*.01}s`});\n }\n el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'CHEAP');\n txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',\n class:'fade'},'PREMIUM');\n const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]\u003ca[2]?b:a);\n P.forEach(([name,px,sat],i)=>{\n const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];\n el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1,\n opacity:.75,class:'fade',style:`animation-delay:${.2+i*.05}s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:hero?5.4:3.4,fill:hero?HERO:DATA,\n class:'pop',style:`animation-delay:${.25+i*.05}s`});\n tip(dot,`${name} — price ${px} · satisfaction ${sat}`);\n if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.8}s`,\n class:'fade'},`${name} · ${sat}`);\n });\n txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:FAINT,\n transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');\n});\n})();\n\n// ════ C5 · rung waterfall ════\n(()=>{\nconst D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];\nobsReveal('fall',s=>{\n const x0=i=>58+i*72,step=5.2,HW=11;\n let lv=0;const rows=[];\n D.forEach(([name,v],i)=>{\n if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}\n else if(name==='NET'){rows.push([name,lv,0,lv])}\n else{rows.push([name,v,lv+v,lv]);lv=lv+v}\n });\n const base=252,yOf=k=>base-k*step;\n rows.forEach(([name,v,lo,hi],i)=>{\n const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v\u003c0&&!isTotal;\n const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);\n const colInk=(HEROMODE&&name==='NET')?HERO:DATA;\n for(let k=0;k\u003cn;k++){\n const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;\n if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA2,'stroke-width':1.8,\n 'stroke-dasharray':'2.5 2.5',opacity:.85,class:'fade',\n style:`animation-delay:${i*.12+k*.014}s`});\n else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});\n }\n if(i\u003crows.length-1){\n const nx=x0(i+1);\n const lvl=isTotal?v:(neg?lo:hi);\n el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:FAINT,\n 'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',\n style:`animation-delay:${.3+i*.12}s`});\n }\n const topY=yOf(Math.max(from,to));\n const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,\n fill:neg?DATA2:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));\n tip(num,`${name} — ${neg?'−':''}${Math.abs(v)}k`);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);\n });\n el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'SOLID RUNGS ADD · DASHED RUNGS TAKE AWAY · ONE RUNG = $1K');\n});\n})();\n\n// ════ C6 · dot heat ════\n(()=>{\nconst DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nobsReveal('dotheat',s=>{\n const x0=j=>64+j*27,y0=i=>58+i*30;\n let max=0,mi=0,mj=0;\n const v=(i,j)=>{\n const day=i\u003c5?1:.32;\n const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5);\n const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);\n return Math.round(raw);\n };\n for(let i=0;i\u003c7;i++)for(let j=0;j\u003c12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}\n for(let i=0;i\u003c7;i++){\n txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);\n for(let j=0;j\u003c12;j++){\n const t=v(i,j),x=x0(j),y=y0(i);\n if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,\n class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}\n const hero=i===mi&&j===mj;\n const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,\n fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],\n class:'pop',style:`animation-delay:${i*.05+j*.015}s`});\n tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);\n if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',\n stroke:HERO,'stroke-width':1,'stroke-dasharray':'2 3',\n class:'fade',style:'animation-delay:1s'});\n }\n }\n for(let j=0;j\u003c12;j+=2)\n txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);\n});\n})();\n\n// ════ C7 · tick gauge ════\n(()=>{\nconst GOAL=73;\nobsReveal('gauge',s=>{\n const cx=200,cy=190,R0=104,A0=-195,SW=210;\n for(let k=0;k\u003c100;k++){\n const a=A0+k/100*SW,inked=k\u003cGOAL;\n const len=inked?15+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;\n const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);\n el(s,'line',{x1,y1,x2,y2,stroke:inked?DATA:FLOOR,'stroke-width':inked?1.9:.6,\n class:'fade',style:`animation-delay:${k*.012}s`});\n }\n [25,50,75,100].forEach(m=>{\n const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);\n el(s,'circle',{cx:dx,cy:dy,r:1,fill:FAINT,class:'fade',style:'animation-delay:.8s'});\n txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.85s'},m);\n });\n const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);\n el(s,'circle',{cx:ex,cy:ey,r:3.2,fill:HERO,class:'pop',style:'animation-delay:1.1s'});\n const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},GOAL+'%');\n tip(num,`$730k of the $1M quarter target`);\n txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE TICK = 1% OF TARGET · INKED = EARNED');\n});\n})();\n\n// ════ C8 · dumbbell queue ════\n(()=>{\nconst D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];\nobsReveal('dumbbell',s=>{\n const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);\n D.forEach(([name,was,now],i)=>{\n const y=y0(i),xa=mapX(was),xb=mapX(now);\n txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:TRACK,'stroke-width':.7,\n class:'fade',style:`animation-delay:${i*.08}s`});\n const n=was-now;\n for(let k=0;k\u003cn;k++){\n const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;\n el(s,'circle',{cx:x,cy:yy,r:2.1+rnd(k+2,i+4)*.9,fill:BEAD,opacity:.95,\n class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});\n }\n el(s,'circle',{cx:xa,cy:y,r:4.8,fill:BG,stroke:DATA,'stroke-width':1.7,\n class:'pop',style:`animation-delay:${.2+i*.08}s`});\n const after=el(s,'circle',{cx:xb,cy:y,r:5.2,fill:HERO,\n class:'pop',style:`animation-delay:${.6+i*.08}s`});\n tip(after,`${name} — ${was} min → ${now} min`);\n txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:FAINTDATA,\n class:'fade',style:`animation-delay:${.3+i*.08}s`},was);\n txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:TXT,'text-anchor':'end',\n class:'fade',style:`animation-delay:${.7+i*.08}s`},now);\n });\n txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'FASTER ←');\n txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',\n class:'fade'},'MINUTES');\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · SOLID = AFTER');\n});\n})();\n\n// ════ C9 · nested treemap ════\n(()=>{\nconst TOTAL=1280;\nconst leaf=(name,value,color,labelColor=BG)=>({name,value,itemStyle:{color},label:{color:labelColor}});\nconst TREE_DATA=[\n {name:'PRODUCT · 50%',children:[leaf('Core app',260,HERO),leaf('Collaboration',190,HERO),leaf('Search',110,HERO),leaf('Mobile',80,HERO)]},\n {name:'PLATFORM · 31%',children:[leaf('AI systems',170,DATA),leaf('Data infra',120,DATA),leaf('APIs',105,DATA)]},\n {name:'GROWTH · 19%',children:[leaf('Acquisition',90,FAINTDATA,TXT),leaf('Retention',80,FAINTDATA,TXT),leaf('Onboarding',75,FAINTDATA,TXT)]},\n];\nconst host=document.getElementById('treemap');let chart;\nconst draw=()=>{chart=chart||echarts.init(host,null,{renderer:'svg'});chart.clear();chart.setOption({\n animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},\n tooltip:{backgroundColor:BG,borderColor:TXT,borderWidth:1,textStyle:{color:TXT,fontSize:12},\n formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}\u003cbr>\u003cb>${i.value} hours\u003c/b> · ${(i.value/TOTAL*100).toFixed(1)}%`},\n series:[{type:'treemap',data:TREE_DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,\n label:{show:true,position:'insideTopLeft',padding:[10,8],color:TXT,fontSize:12,fontWeight:700,lineHeight:18,\n formatter:p=>p.treePathInfo.length\u003c=1?'':p.treePathInfo.length===2?p.name:`${p.name}\\n${p.value} h`},\n upperLabel:{show:true,height:32,padding:[0,10],color:TXT,fontSize:10,fontWeight:700,backgroundColor:BG},\n itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2},emphasis:{focus:'ancestor',itemStyle:{borderColor:TXT,borderWidth:1.5}},\n levels:[{itemStyle:{borderWidth:0,gapWidth:5}},{upperLabel:{show:true,height:32},itemStyle:{borderColor:BG,borderWidth:3,gapWidth:3}},{upperLabel:{show:false},itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2}}]}]});};\nconst io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});\nio.observe(host);host.style.cursor='pointer';host.addEventListener('click',draw);addEventListener('resize',()=>chart?.resize());\n})();\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ F14 · rung histogram ════\n// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单\n// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,\n// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。\n(()=>{\nconst BIN=[6,14,22,19,13,9,6,4,3,2,1,1]; // 2h bins, sums to 100\nobsReveal('histo',s=>{\n const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;\n BIN.forEach((v,i)=>{\n const x=X0+i*PW+PW/2;\n for(let k=0;k\u003cv;k++){\n const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1.8,\n opacity:.55+rnd(k+2,i+4)*.45,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'rgba(8,31,92,.32)',\n class:'fade',style:`animation-delay:${i*.06+k*.012}s`});\n }\n });\n // peak label\n const pk=BIN.indexOf(Math.max(...BIN));\n const px=X0+pk*PW+PW/2;\n const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,\n 'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);\n tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);\n // bin edges: the continuous axis is the histogram's identity\n el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let e=0;e\u003c=12;e++){\n const x=X0+e*PW;\n el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${e*.02}s`});\n if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');\n }\n // median flag: cumulative crosses 50 inside bin 2\n let acc=0,mb=0;for(let i=0;i\u003c12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}\n const mx=X0+mb*PW+PW*.7;\n el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#7096D1','stroke-width':1.62,\n 'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});\n txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'rgba(8,31,92,.72)',\n class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE RUNG = ONE TICKET IN A HUNDRED · BINS OF TWO HOURS · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ F15 · tick box ════\n// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,\n// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。\n(()=>{\nconst G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],\n ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],\n ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],\n ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];\nconst SHADE=[INK,'#334EAC','#7096D1','#BAD6EB'];\nobsReveal('boxplot',s=>{\n const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);\n const colX=g=>78+g*84,BW=24;\n [0,6,12,18,24].forEach(h=>{\n el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(8,31,92,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${h*.008}s`});\n txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.008}s`},h+'h');\n });\n G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{\n const x=colX(g);\n el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:'#7096D1','stroke-width':1.44,\n pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});\n [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),\n stroke:'#7096D1','stroke-width':1.8,class:'fade',style:`animation-delay:${.2+g*.12}s`}));\n const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,\n fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});\n tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);\n el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,\n class:'fade',style:`animation-delay:${.5+g*.12}s`});\n txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':9.5,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');\n outs.forEach((o,k)=>{\n const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,\n stroke:'rgba(8,31,92,.72)','stroke-width':1.98,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});\n tip(d,`${name} outlier — ${o}h`);\n });\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · HOLLOW = OUTLIER · DARKEST = FASTEST');\n});\n})();\n\n// ════ F16 · stream ribbon ════\n// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴\n// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。\n// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。\n(()=>{\nconst N=48;\nconst mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>\n Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));\nconst SERIES=[\n ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'rgba(8,31,92,.32)'],\n ['BOARDS',mk(26,.10,11,4.2,5),'#7096D1'],\n ['FLOWS',mk(12,.78,10,3.1,8),INK],\n];\nobsReveal('stream',s=>{\n const X0=36,X1=744,CY=138,SC=1.15;\n const x=t=>X0+t/(N-1)*(X1-X0);\n const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));\n // stacked offsets, silhouette baseline: top of stack starts at -total/2\n const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);\n const smooth=pts=>{\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let k=1;k\u003cpts.length-1;k++){\n const p=pts[k],q=pts[k+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)}`;\n }\n return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;\n };\n let run=y0.slice();\n SERIES.forEach(([name,vals,shade],si)=>{\n const top=run.map((v,t)=>[x(t),v]);\n const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);\n run=bot.map(p=>p[1]);\n const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';\n const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,\n class:'fade',style:`animation-delay:${si*.16}s`});\n tip(band,`${name} — weekly active accounts, 48 weeks`);\n // label at the band's widest week — clamped off both ends so the\n // paper-colored text never spills past the band onto paper (invisible)\n let wk=4;vals.forEach((v,t)=>{if(t>=4&&t\u003c=N-6&&v>vals[wk])wk=t});\n const midY=(top[wk][1]+bot[wk][1])/2;\n const dark=shade===INK||shade==='#334EAC';\n txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',\n fill:dark?PAPER:'#334EAC','letter-spacing':'.06em',\n class:'fade',style:`animation-delay:${.5+si*.14}s`},name);\n });\n // month floor: barcode ticks + labels every 8 weeks\n const base=252;\n el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let t=0;t\u003cN;t++)\n el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${t*.006}s`});\n ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>\n txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'BAND WIDTH = WEEKLY ACTIVES · DARKEST = TODAY’S LEADER · THE RIVER IS THE TOTAL');\n});\n})();\n\n// ════ F17 · candlestick ════\n// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,\n// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。\n// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。\n(()=>{\nconst N=30;\n// deterministic random walk with a dip-and-recover arc\nconst DAYS=[];\nlet px=52;\nfor(let d=0;d\u003cN;d++){\n const drift=d\u003c10?.4:d\u003c18?-1.5:1.3;\n const open=px;\n const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);\n const hi=Math.max(open,close)+rnd(d+2,7)*2.6;\n const lo=Math.min(open,close)-rnd(d+3,11)*2.6;\n DAYS.push([open,close,hi,lo]);\n px=close;\n}\nobsReveal('candle',s=>{\n const X0=40,PW=11.2,BASE=258,TOP=44;\n const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));\n const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);\n const x=d=>X0+d*PW+PW/2;\n // price grid\n for(let g=0;g\u003c=4;g++){\n const v=vmin+g/4*(vmax-vmin);\n el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'rgba(8,31,92,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${g*.03}s`});\n txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${g*.03}s`},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v));\n }\n let hiD=0,loD=0;\n DAYS.forEach(([o,c,h,l],d)=>{\n if(h>DAYS[hiD][2])hiD=d;\n if(l\u003cDAYS[loD][3])loD=d;\n });\n DAYS.forEach(([o,c,h,l],d)=>{\n const cx=x(d),up=c>=o;\n const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));\n // wick: one hairline, full reach\n el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'rgba(8,31,92,.72)','stroke-width':1.26,\n class:'fade',style:`animation-delay:${d*.03}s`});\n // body: capsule; hollow = up, ink = down\n const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,\n ...(up?{fill:PAPER,stroke:INK,'stroke-width':1.98}:{fill:INK}),\n class:'fade',style:`animation-delay:${.05+d*.03}s`});\n tip(body,`Day ${d+1} — open ${o.toFixed(1)} · close ${c.toFixed(1)} · high ${h.toFixed(1)} · low ${l.toFixed(1)}`);\n // extremes, labeled with hairline flags\n if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(h));\n if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'rgba(8,31,92,.72)','text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(l));\n });\n // week ticks on the floor\n el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${d*.008}s`});\n ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>\n txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade'},w));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":47617,"content_sha256":"335586ea95e87f3a6fdd8f023325ea72c91b3d5bc69cd0be36eb1e38b158f8ef"},{"filename":"templates/color/basics-wire.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Basics F1–F17 × WIRE\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap\" rel=\"stylesheet\">\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003cstyle>\n :root{--bg:#F0F0EE;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.32);--grid:rgba(31,30,28,.16)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed var(--faint);color:var(--muted)}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n .chartbox{width:100%;height:360px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>基础型组 F1–F17 × WIRE\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#F5572F\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#22211F\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#8F8E86\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#C0BFB7\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#DBDAD3\">\u003c/span>  编辑部红 · 黑灰为体,每张图一个主角元素上红(最大柱/峰值/NET/改版后)。几何、动画、数据与 basics-gallery 一致;彩色版统一加墨(线宽 ×1.8 / 透明度地板 .85,dot heat 除外——它的墨本来就够)。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 柱状\u003c/span>\n \u003ch2>Revenue by plan, rung by rung\u003c/h2>\n \u003cdiv class=\"sub\">one rung = one $k of MRR · the bar is a ladder you can count\u003c/div>\n \u003csvg id=\"rungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG BARS · WIRE · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 折线\u003c/span>\n \u003ch2>Thirty days of sign-ups\u003c/h2>\n \u003cdiv class=\"sub\">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest\u003c/div>\n \u003csvg id=\"dayline\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HAIRLINE LINE · WIRE · GROWTH\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 面积\u003c/span>\n \u003ch2>Concurrent users, filled with days\u003c/h2>\n \u003cdiv class=\"sub\">one hairline = one day, floor to peak · the area is made of days, not paint\u003c/div>\n \u003csvg id=\"hairarea\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HAIRLINE AREA · WIRE · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 环形\u003c/span>\n \u003ch2>Where the traffic comes from\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one percent · four sources shade the dial · read it like a clock\u003c/div>\n \u003csvg id=\"tickdonut\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK DONUT · WIRE · ACQUISITION\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 横向条形\u003c/span>\n \u003ch2>Six teams, shipped and counted\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one release this quarter · the row is a queue, not a bar\u003c/div>\n \u003csvg id=\"tickrows\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK ROWS · WIRE · RELEASE LOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 分组柱\u003c/span>\n \u003ch2>This year against last, plan by plan\u003c/h2>\n \u003cdiv class=\"sub\">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k\u003c/div>\n \u003csvg id=\"pairrungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PAIRED RUNGS · WIRE · BILLING YoY\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 堆叠柱\u003c/span>\n \u003ch2>Where each region's revenue sits\u003c/h2>\n \u003cdiv class=\"sub\">one rung = $1k · three products stack up the ladder, strongest carries the most\u003c/div>\n \u003csvg id=\"stackrungs\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">STACKED RUNGS · WIRE · REVENUE MIX\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 散点\u003c/span>\n \u003ch2>Price against satisfaction, twelve products\u003c/h2>\n \u003cdiv class=\"sub\">every dot hangs a plumb line to the floor · read x by the line, y by the height\u003c/div>\n \u003csvg id=\"plumb\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PLUMB SCATTER · WIRE · PRODUCT PANEL\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 瀑布\u003c/span>\n \u003ch2>From gross to net, step by step\u003c/h2>\n \u003cdiv class=\"sub\">one rung = $1k · solid rungs add, dashed rungs take away · the ladder walks down\u003c/div>\n \u003csvg id=\"fall\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG WATERFALL · WIRE · P&L\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 热力\u003c/span>\n \u003ch2>When support gets loud\u003c/h2>\n \u003cdiv class=\"sub\">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible\u003c/div>\n \u003csvg id=\"dotheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DOT HEAT · WIRE · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 进度\u003c/span>\n \u003ch2>How far to the quarter's goal\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one percent of target · inked = earned · the dial reads left to right\u003c/div>\n \u003csvg id=\"gauge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK GAUGE · WIRE · QUARTERLY TARGET\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 哑铃对比\u003c/span>\n \u003ch2>Onboarding, before and after the redesign\u003c/h2>\n \u003cdiv class=\"sub\">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable\u003c/div>\n \u003csvg id=\"dumbbell\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DUMBBELL QUEUE · WIRE · UX TIMING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 基础型 · 矩形树图\u003c/span>\n \u003ch2>Where the work went\u003c/h2>\n \u003cdiv class=\"sub\">1,280 shipped hours · area = effort · fluorescent orange = the single focal workstream\u003c/div>\n \u003cdiv id=\"treemap\" class=\"chartbox\" role=\"img\" aria-label=\"Wire treemap of shipped hours nested by product family\">\u003c/div>\n \u003cdiv class=\"src\">NESTED TREEMAP · WIRE · WORK PORTFOLIO\u003c/div>\n \u003c/div>\n\n\n \u003c!-- F14 · rung histogram -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 直方图\u003c/span>\n \u003ch2>Most tickets resolve within six hours\u003c/h2>\n \u003cdiv class=\"sub\">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile\u003c/div>\n \u003csvg id=\"histo\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RUNG HISTOGRAM · WIRE · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- F15 · tick box -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · 箱线图\u003c/span>\n \u003ch2>Reply times, boxed by plan\u003c/h2>\n \u003cdiv class=\"sub\">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed\u003c/div>\n \u003csvg id=\"boxplot\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TICK BOX · WIRE · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- F16 · stream ribbon -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 基础型 · STREAMGRAPH\u003c/span>\n \u003ch2>Three products trade the same river\u003c/h2>\n \u003cdiv class=\"sub\">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows\u003c/div>\n \u003csvg id=\"stream\" viewBox=\"0 0 840 300\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:300px\">\u003c/svg>\n \u003cdiv class=\"src\">STREAM RIBBON · WIRE · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- F17 · candlestick -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 基础型 · K 线\u003c/span>\n \u003ch2>Six weeks of the token, candle by candle\u003c/h2>\n \u003cdiv class=\"sub\">body = open to close · wick = the day's full reach · ink = closed down · hollow = closed up\u003c/div>\n \u003csvg id=\"candle\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">CANDLESTICK · WIRE · MARKET DATA\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── WIRE 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F0F0EE',TXT='#1F1E1C',MUT='rgba(31,30,28,.60)',LAB='rgba(31,30,28,.72)',FAINT='rgba(31,30,28,.32)';\nconst FLOOR='rgba(31,30,28,.24)',QUIET='rgba(31,30,28,.15)',TRACK='rgba(31,30,28,.12)',GRID='rgba(31,30,28,.16)';\nconst DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F';\nconst FAINTDATA='#C0BFB7',BEAD='#8F8E86';\nconst CAT4=[\"#F5572F\", \"#22211F\", \"#8F8E86\", \"#C0BFB7\"],CAT3=[\"#F5572F\", \"#22211F\", \"#8F8E86\"],CAT3L=[\"#F5572F\", \"#22211F\", \"#6E6D66\"],HEAT=[\"#F5572F\", \"#6E6D66\", \"#C0BFB7\"];\nconst HEROMODE=true; // 黑体红睛模式:每张图一个主角元素整体走 HERO\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\nconst D2R=Math.PI/180;\nconst pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>{n.innerHTML='';fn(n)};\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ B1 · rung bars ════\n(()=>{\nconst D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];\nobsReveal('rungs',s=>{\n const x0=i=>56+i*56,base=266,step=5.6,HW=14;\n D.forEach(([name,v],i)=>{\n const x=x0(i);\n const colInk=(HEROMODE&&i===0)?HERO:DATA;\n for(let k=0;k\u003cv;k++){\n const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n }\n const topY=base-(v-1)*step;\n const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.4+i*.08}s`},v);\n tip(num,`${name} — ${v}k MRR`);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n });\n el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},\n 'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ B2 · hairline line ════\n(()=>{\nconst N=30;\nconst val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;\nobsReveal('dayline',s=>{\n const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,\n class:'fade',style:`animation-delay:${d*.008}s`});\n el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n const vs=Array.from({length:N},(_,d)=>val(d));\n const top=[];\n for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){\n if(top.every(t=>Math.abs(t-d)>=5))top.push(d);\n if(top.length===2)break;\n }\n const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');\n el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,\n class:'draw',style:'animation-duration:1.2s'});\n vs.forEach((v,d)=>{\n const weekend=d%7===5||d%7===6,big=top.includes(d);\n const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?5.2:3.1,\n fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,\n class:'pop',style:`animation-delay:${.2+d*.03}s`});\n tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);\n if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,\n class:'fade'},Math.round(v));\n });\n [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>\n txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'ONE DOT = ONE DAY · HOLLOW = WEEKEND');\n});\n})();\n\n// ════ B3 · hairline area ════\n(()=>{\nconst N=45;\nconst val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;\nobsReveal('hairarea',s=>{\n const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;\n el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n const vs=Array.from({length:N},(_,d)=>val(d));\n const peak=vs.indexOf(Math.max(...vs));\n vs.forEach((v,d)=>{\n el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),\n stroke:d===peak?HERO:DATA,'stroke-width':d===peak?1.6:1,\n opacity:d===peak?1:.8+rnd(d+1,7)*.2,\n class:'fade',style:`animation-delay:${d*.014}s`});\n });\n const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');\n el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2,pathLength:1,\n class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});\n const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:HERO,\n class:'pop',style:'animation-delay:1.2s'});\n tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);\n txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1.3s`,\n class:'fade'},Math.round(vs[peak])+'k');\n [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>\n txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},\n 'ONE HAIRLINE = ONE DAY, FLOOR TO PEAK');\n});\n})();\n\n// ════ B4 · tick donut ════\n(()=>{\nconst D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];\nobsReveal('tickdonut',s=>{\n const cx=200,cy=148,R0=64;\n let k0=0;\n D.forEach(([name,v,shade],si)=>{\n for(let k=0;k\u003cv;k++){\n const idx=k0+k,a=idx*3.6-90;\n const len=12+rnd(idx+1,si+2)*6;\n const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);\n el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,\n class:'fade',style:`animation-delay:${idx*.012}s`});\n if(idx%10===0){\n const [dx,dy]=pol(cx,cy,R0-5,a);\n el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${idx*.012}s`});\n }\n }\n const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);\n el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,\n 'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});\n const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)\u003c-0.3?'end':'middle';\n const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,\n 'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,\n class:'fade'},`${name} · ${v}`);\n tip(lab,`${name} — ${v}% of traffic`);\n k0+=v;\n });\n txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.9s'},'100');\n txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');\n txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');\n});\n})();\n\n// ════ C1 · tick rows ════\n(()=>{\nconst D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];\nobsReveal('tickrows',s=>{\n const y0=i=>52+i*44,X0=104,PX=6.9;\n D.forEach(([name,v],i)=>{\n const y=y0(i);\n txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,\n class:'fade',style:`animation-delay:${i*.08}s`});\n const rowInk=(HEROMODE&&i===0)?HERO:DATA;\n for(let k=0;k\u003cv;k++){\n const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;\n el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:rowInk,'stroke-width':1.7,\n opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.08+k*.012}s`});\n }\n const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:TXT,\n class:'fade',style:`animation-delay:${.4+i*.08}s`},v);\n tip(lab,`${name} — ${v} releases`);\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},\n 'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ C2 · paired rungs ════\n(()=>{\nconst D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];\nobsReveal('pairrungs',s=>{\n const x0=i=>64+i*66,base=258,step=5.4,HW=10;\n D.forEach(([name,was,now],i)=>{\n const xa=x0(i)-13,xb=x0(i)+13;\n for(let k=0;k\u003cwas;k++){\n const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;\n el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.8,\n opacity:.75+rnd(k+2,i+3)*.25,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});\n }\n for(let k=0;k\u003cnow;k++){\n const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;\n el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:DATA,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+8)*.15,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});\n }\n const topB=base-(now-1)*step;\n const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:HEROMODE?HERO:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.5+i*.08}s`},now);\n tip(num,`${name} — ${was}k → ${now}k`);\n txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:FAINTDATA,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.5+i*.08}s`},was);\n txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n });\n el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'FAINT = 2025 · SOLID = 2026 · ONE RUNG = $1K');\n});\n})();\n\n// ════ C3 · stacked rungs ════\n(()=>{\nconst D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];\nconst SEG=['CORE','ADD-ONS','SERVICES'];\nobsReveal('stackrungs',s=>{\n const x0=i=>72+i*76,base=262,step=5.2,HW=13;\n D.forEach(([name,segs],i)=>{\n const x=x0(i);let k0=0;\n segs.forEach((v,si)=>{\n for(let k=0;k\u003cv;k++){\n const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:CAT3[si],'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+si+4)*.15,class:'fade',\n style:`animation-delay:${i*.09+(k0+k)*.012}s`});\n }\n const midY=base-(k0+v/2+si)*step;\n const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:CAT3L[si],\n class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);\n tip(lab,`${name} ${SEG[si]} — ${v}k`);\n k0+=v;\n });\n const total=segs[0]+segs[1]+segs[2];\n txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.6+i*.09}s`},total);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);\n });\n el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'STRONGEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');\n});\n})();\n\n// ════ C4 · plumb scatter ════\n(()=>{\nconst P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],\n ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],\n ['Hub',76,32],['Forms',34,38]];\nobsReveal('plumb',s=>{\n const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;\n for(let g=0;g\u003c=20;g++){\n const x=X0+g/20*(X1-X0);\n el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:FLOOR,'stroke-width':.6,\n class:'fade',style:`animation-delay:${g*.01}s`});\n }\n el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'CHEAP');\n txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',\n class:'fade'},'PREMIUM');\n const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]\u003ca[2]?b:a);\n P.forEach(([name,px,sat],i)=>{\n const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];\n el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1,\n opacity:.75,class:'fade',style:`animation-delay:${.2+i*.05}s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:hero?5.4:3.4,fill:hero?HERO:DATA,\n class:'pop',style:`animation-delay:${.25+i*.05}s`});\n tip(dot,`${name} — price ${px} · satisfaction ${sat}`);\n if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.8}s`,\n class:'fade'},`${name} · ${sat}`);\n });\n txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:FAINT,\n transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');\n});\n})();\n\n// ════ C5 · rung waterfall ════\n(()=>{\nconst D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];\nobsReveal('fall',s=>{\n const x0=i=>58+i*72,step=5.2,HW=11;\n let lv=0;const rows=[];\n D.forEach(([name,v],i)=>{\n if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}\n else if(name==='NET'){rows.push([name,lv,0,lv])}\n else{rows.push([name,v,lv+v,lv]);lv=lv+v}\n });\n const base=252,yOf=k=>base-k*step;\n rows.forEach(([name,v,lo,hi],i)=>{\n const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v\u003c0&&!isTotal;\n const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);\n const colInk=(HEROMODE&&name==='NET')?HERO:DATA;\n for(let k=0;k\u003cn;k++){\n const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;\n if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA2,'stroke-width':1.8,\n 'stroke-dasharray':'2.5 2.5',opacity:.85,class:'fade',\n style:`animation-delay:${i*.12+k*.014}s`});\n else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,\n opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});\n }\n if(i\u003crows.length-1){\n const nx=x0(i+1);\n const lvl=isTotal?v:(neg?lo:hi);\n el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:FAINT,\n 'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',\n style:`animation-delay:${.3+i*.12}s`});\n }\n const topY=yOf(Math.max(from,to));\n const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,\n fill:neg?DATA2:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));\n tip(num,`${name} — ${neg?'−':''}${Math.abs(v)}k`);\n txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);\n });\n el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'SOLID RUNGS ADD · DASHED RUNGS TAKE AWAY · ONE RUNG = $1K');\n});\n})();\n\n// ════ C6 · dot heat ════\n(()=>{\nconst DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nobsReveal('dotheat',s=>{\n const x0=j=>64+j*27,y0=i=>58+i*30;\n let max=0,mi=0,mj=0;\n const v=(i,j)=>{\n const day=i\u003c5?1:.32;\n const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5);\n const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);\n return Math.round(raw);\n };\n for(let i=0;i\u003c7;i++)for(let j=0;j\u003c12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}\n for(let i=0;i\u003c7;i++){\n txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);\n for(let j=0;j\u003c12;j++){\n const t=v(i,j),x=x0(j),y=y0(i);\n if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,\n class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}\n const hero=i===mi&&j===mj;\n const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,\n fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],\n class:'pop',style:`animation-delay:${i*.05+j*.015}s`});\n tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);\n if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',\n stroke:HERO,'stroke-width':1,'stroke-dasharray':'2 3',\n class:'fade',style:'animation-delay:1s'});\n }\n }\n for(let j=0;j\u003c12;j+=2)\n txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);\n});\n})();\n\n// ════ C7 · tick gauge ════\n(()=>{\nconst GOAL=73;\nobsReveal('gauge',s=>{\n const cx=200,cy=190,R0=104,A0=-195,SW=210;\n for(let k=0;k\u003c100;k++){\n const a=A0+k/100*SW,inked=k\u003cGOAL;\n const len=inked?15+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;\n const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);\n el(s,'line',{x1,y1,x2,y2,stroke:inked?DATA:FLOOR,'stroke-width':inked?1.9:.6,\n class:'fade',style:`animation-delay:${k*.012}s`});\n }\n [25,50,75,100].forEach(m=>{\n const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);\n el(s,'circle',{cx:dx,cy:dy,r:1,fill:FAINT,class:'fade',style:'animation-delay:.8s'});\n txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.85s'},m);\n });\n const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);\n el(s,'circle',{cx:ex,cy:ey,r:3.2,fill:HERO,class:'pop',style:'animation-delay:1.1s'});\n const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},GOAL+'%');\n tip(num,`$730k of the $1M quarter target`);\n txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE TICK = 1% OF TARGET · INKED = EARNED');\n});\n})();\n\n// ════ C8 · dumbbell queue ════\n(()=>{\nconst D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];\nobsReveal('dumbbell',s=>{\n const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);\n D.forEach(([name,was,now],i)=>{\n const y=y0(i),xa=mapX(was),xb=mapX(now);\n txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);\n el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:TRACK,'stroke-width':.7,\n class:'fade',style:`animation-delay:${i*.08}s`});\n const n=was-now;\n for(let k=0;k\u003cn;k++){\n const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;\n el(s,'circle',{cx:x,cy:yy,r:2.1+rnd(k+2,i+4)*.9,fill:BEAD,opacity:.95,\n class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});\n }\n el(s,'circle',{cx:xa,cy:y,r:4.8,fill:BG,stroke:DATA,'stroke-width':1.7,\n class:'pop',style:`animation-delay:${.2+i*.08}s`});\n const after=el(s,'circle',{cx:xb,cy:y,r:5.2,fill:HERO,\n class:'pop',style:`animation-delay:${.6+i*.08}s`});\n tip(after,`${name} — ${was} min → ${now} min`);\n txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:FAINTDATA,\n class:'fade',style:`animation-delay:${.3+i*.08}s`},was);\n txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:TXT,'text-anchor':'end',\n class:'fade',style:`animation-delay:${.7+i*.08}s`},now);\n });\n txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'FASTER ←');\n txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',\n class:'fade'},'MINUTES');\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · SOLID = AFTER');\n});\n})();\n\n// ════ C9 · nested treemap ════\n(()=>{\nconst TOTAL=1280;\nconst leaf=(name,value,color,labelColor=BG)=>({name,value,itemStyle:{color},label:{color:labelColor}});\nconst TREE_DATA=[\n {name:'PRODUCT · 50%',children:[leaf('Core app',260,HERO),leaf('Collaboration',190,DATA),leaf('Search',110,DATA),leaf('Mobile',80,DATA)]},\n {name:'PLATFORM · 31%',children:[leaf('AI systems',170,DATA2,TXT),leaf('Data infra',120,DATA2,TXT),leaf('APIs',105,DATA2,TXT)]},\n {name:'GROWTH · 19%',children:[leaf('Acquisition',90,FAINTDATA,TXT),leaf('Retention',80,FAINTDATA,TXT),leaf('Onboarding',75,FAINTDATA,TXT)]},\n];\nconst host=document.getElementById('treemap');let chart;\nconst draw=()=>{chart=chart||echarts.init(host,null,{renderer:'svg'});chart.clear();chart.setOption({\n animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},\n tooltip:{backgroundColor:BG,borderColor:TXT,borderWidth:1,textStyle:{color:TXT,fontSize:12},\n formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}\u003cbr>\u003cb>${i.value} hours\u003c/b> · ${(i.value/TOTAL*100).toFixed(1)}%`},\n series:[{type:'treemap',data:TREE_DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,\n label:{show:true,position:'insideTopLeft',padding:[10,8],color:TXT,fontSize:12,fontWeight:700,lineHeight:18,\n formatter:p=>p.treePathInfo.length\u003c=1?'':p.treePathInfo.length===2?p.name:`${p.name}\\n${p.value} h`},\n upperLabel:{show:true,height:32,padding:[0,10],color:TXT,fontSize:10,fontWeight:700,backgroundColor:BG},\n itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2},emphasis:{focus:'ancestor',itemStyle:{borderColor:TXT,borderWidth:1.5}},\n levels:[{itemStyle:{borderWidth:0,gapWidth:5}},{upperLabel:{show:true,height:32},itemStyle:{borderColor:BG,borderWidth:3,gapWidth:3}},{upperLabel:{show:false},itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2}}]}]});};\nconst io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});\nio.observe(host);host.style.cursor='pointer';host.addEventListener('click',draw);addEventListener('resize',()=>chart?.resize());\n})();\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ F14 · rung histogram ════\n// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单\n// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,\n// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。\n(()=>{\nconst BIN=[6,14,22,19,13,9,6,4,3,2,1,1]; // 2h bins, sums to 100\nobsReveal('histo',s=>{\n const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;\n BIN.forEach((v,i)=>{\n const x=X0+i*PW+PW/2;\n for(let k=0;k\u003cv;k++){\n const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;\n el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1.8,\n opacity:.55+rnd(k+2,i+4)*.45,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});\n if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'rgba(31,30,28,.32)',\n class:'fade',style:`animation-delay:${i*.06+k*.012}s`});\n }\n });\n // peak label\n const pk=BIN.indexOf(Math.max(...BIN));\n const px=X0+pk*PW+PW/2;\n // 一图一红:红给中位旗(half resolved 才是结论),峰值数走墨\n const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,\n 'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);\n tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);\n // bin edges: the continuous axis is the histogram's identity\n el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let e=0;e\u003c=12;e++){\n const x=X0+e*PW;\n el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${e*.02}s`});\n if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');\n }\n // median flag: cumulative crosses 50 inside bin 2\n let acc=0,mb=0;for(let i=0;i\u003c12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}\n const mx=X0+mb*PW+PW*.7;\n el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#F5572F','stroke-width':1.62,\n 'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});\n txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'#F5572F',\n class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE RUNG = ONE TICKET IN A HUNDRED · BINS OF TWO HOURS · DOT MARKS EVERY FIFTH');\n});\n})();\n\n// ════ F15 · tick box ════\n// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,\n// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。\n(()=>{\nconst G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],\n ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],\n ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],\n ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];\nconst SHADE=[INK,'#6E6D66','#8F8E86','#C0BFB7'];\nobsReveal('boxplot',s=>{\n const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);\n const colX=g=>78+g*84,BW=24;\n [0,6,12,18,24].forEach(h=>{\n el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(31,30,28,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${h*.008}s`});\n txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.008}s`},h+'h');\n });\n G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{\n const x=colX(g);\n el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:'#8F8E86','stroke-width':1.44,\n pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});\n [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),\n stroke:'#8F8E86','stroke-width':1.8,class:'fade',style:`animation-delay:${.2+g*.12}s`}));\n const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,\n fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});\n tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);\n el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,\n class:'fade',style:`animation-delay:${.5+g*.12}s`});\n // 红只给一个主角:最慢的 FREE 是这张图的故事,其余中位数走墨色\n const slow=g===G.length-1;\n txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':slow?11:9.5,'font-weight':800,\n fill:slow?'#F5572F':INK,\n class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');\n outs.forEach((o,k)=>{\n const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,\n stroke:'rgba(31,30,28,.72)','stroke-width':1.98,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});\n tip(d,`${name} outlier — ${o}h`);\n });\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · RED = THE SLOWEST PLAN · DARKEST = FASTEST');\n});\n})();\n\n// ════ F16 · stream ribbon ════\n// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴\n// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。\n// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。\n(()=>{\nconst N=48;\nconst mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>\n Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));\n// 现任霸主是主角:FLOWS 上红,其余灰阶退后\nconst SERIES=[\n ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'rgba(31,30,28,.32)'],\n ['BOARDS',mk(26,.10,11,4.2,5),'#8F8E86'],\n ['FLOWS',mk(12,.78,10,3.1,8),'#F5572F'],\n];\nobsReveal('stream',s=>{\n const X0=36,X1=744,CY=138,SC=1.15;\n const x=t=>X0+t/(N-1)*(X1-X0);\n const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));\n // stacked offsets, silhouette baseline: top of stack starts at -total/2\n const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);\n const smooth=pts=>{\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let k=1;k\u003cpts.length-1;k++){\n const p=pts[k],q=pts[k+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)}`;\n }\n return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;\n };\n let run=y0.slice();\n SERIES.forEach(([name,vals,shade],si)=>{\n const top=run.map((v,t)=>[x(t),v]);\n const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);\n run=bot.map(p=>p[1]);\n const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';\n const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,\n class:'fade',style:`animation-delay:${si*.16}s`});\n tip(band,`${name} — weekly active accounts, 48 weeks`);\n // label at the band's widest week — clamped off both ends so the\n // paper-colored text never spills past the band onto paper (invisible)\n let wk=4;vals.forEach((v,t)=>{if(t>=4&&t\u003c=N-6&&v>vals[wk])wk=t});\n const midY=(top[wk][1]+bot[wk][1])/2;\n const dark=shade===INK||shade==='#6E6D66'||shade==='#F5572F';\n txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',\n fill:dark?PAPER:'#6E6D66','letter-spacing':'.06em',\n class:'fade',style:`animation-delay:${.5+si*.14}s`},name);\n });\n // month floor: barcode ticks + labels every 8 weeks\n const base=252;\n el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let t=0;t\u003cN;t++)\n el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${t*.006}s`});\n ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>\n txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.1em',class:'fade'},m));\n txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'BAND WIDTH = WEEKLY ACTIVES · RED = TODAY’S LEADER · THE RIVER IS THE TOTAL');\n});\n})();\n\n// ════ F17 · candlestick ════\n// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,\n// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。\n// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。\n(()=>{\nconst N=30;\n// deterministic random walk with a dip-and-recover arc\nconst DAYS=[];\nlet px=52;\nfor(let d=0;d\u003cN;d++){\n const drift=d\u003c10?.4:d\u003c18?-1.5:1.3;\n const open=px;\n const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);\n const hi=Math.max(open,close)+rnd(d+2,7)*2.6;\n const lo=Math.min(open,close)-rnd(d+3,11)*2.6;\n DAYS.push([open,close,hi,lo]);\n px=close;\n}\nobsReveal('candle',s=>{\n const X0=40,PW=11.2,BASE=258,TOP=44;\n const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));\n const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);\n const x=d=>X0+d*PW+PW/2;\n // price grid\n for(let g=0;g\u003c=4;g++){\n const v=vmin+g/4*(vmax-vmin);\n el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'rgba(31,30,28,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${g*.03}s`});\n txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${g*.03}s`},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v));\n }\n let hiD=0,loD=0;\n DAYS.forEach(([o,c,h,l],d)=>{\n if(h>DAYS[hiD][2])hiD=d;\n if(l\u003cDAYS[loD][3])loD=d;\n });\n DAYS.forEach(([o,c,h,l],d)=>{\n const cx=x(d),up=c>=o;\n const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));\n // wick: one hairline, full reach\n el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'rgba(31,30,28,.72)','stroke-width':1.26,\n class:'fade',style:`animation-delay:${d*.03}s`});\n // body: capsule; hollow = up, ink = down\n const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,\n ...(up?{fill:PAPER,stroke:INK,'stroke-width':1.98}:{fill:INK}),\n class:'fade',style:`animation-delay:${.05+d*.03}s`});\n tip(body,`Day ${d+1} — open ${o.toFixed(1)} · close ${c.toFixed(1)} · high ${h.toFixed(1)} · low ${l.toFixed(1)}`);\n // extremes, labeled with hairline flags\n // 一图一红:这段行情的故事是探底,红只给最低价,最高价走墨\n if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(h));\n if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':9.5,'font-weight':800,fill:'#F5572F','text-anchor':'middle',\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(l));\n });\n // week ticks on the floor\n el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':1.44,class:'fade'});\n for(let d=0;d\u003cN;d++)\n el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,\n class:'fade',style:`animation-delay:${d*.008}s`});\n ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>\n txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade'},w));\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":47910,"content_sha256":"15aa27fc3d9cd325031c689c1bf7eaf89c1368cb6337c33faee981c05245e911"},{"filename":"templates/color/big-circular-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>椰林绿 — Circular Graph, Dense\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#28311F}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:#F0EFE9;border-radius:24px;padding:34px 34px 24px;\n width:min(1100px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#92A763;margin-bottom:10px}\n .src{font-size:9.5px;color:#5A6B47;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{height:78vh;min-height:620px;cursor:pointer}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>Six hundred collaborations, one ring\u003c/h2>\n \u003cdiv class=\"sub\">60 repos · line = shared contributors · hover a repo to light up its world · click to replay · 绿黄阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走麦黄。\u003c/div>\n \u003cdiv id=\"ch\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG · PALM\u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#28311F',PAPER='#F0EFE9';\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── 60 repos in 5 org clusters; cluster = shade family ──\nconst ORGS=[\n ['platform',16,'#F0EFE9'], // paper — the core\n ['product',14,'#C8CAA8'],\n ['data',12,'#AFB885'],\n ['infra',10,'#7EA056'],\n ['labs',8,'#4F7D3E'],\n];\nconst nodes=[];\nORGS.forEach(([org,count],oi)=>{\n for(let i=0;i\u003ccount;i++){\n // activity: a few giants per cluster, long tail of small repos\n const heavy=i\u003c2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;\n nodes.push({\n name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),\n org:oi,value:Math.round(heavy),\n });\n }\n});\n// links: dense inside a cluster, sparser across; weight = shared contributors\nconst links=[];\nconst idx={};nodes.forEach((n,i)=>idx[n.name]=i);\nnodes.forEach((a,i)=>{\n nodes.forEach((b,j)=>{\n if(j\u003c=i)return;\n const same=a.org===b.org;\n const p=same?.16:.045;\n if(rnd(i+1,j+2)\u003cp){\n links.push({source:i,target:j,\n w:1+Math.round(rnd(i+2,j+3)*6),same});\n }\n });\n});\n\nconst g=echarts.init(document.getElementById('ch'));\nconst opt={\n animationDuration:1800,animationEasing:'quarticOut',\n tooltip:{backgroundColor:'#F0EFEB',borderWidth:0,\n textStyle:{color:'#28311F',fontFamily:'Inter',fontSize:12},padding:[10,14],\n formatter:p=>p.dataType==='node'\n ?p.name+' — '+p.value+' contributors'\n :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},\n series:[{\n type:'graph',layout:'circular',\n circular:{rotateLabel:true},\n left:24,right:24,top:24,bottom:24,\n data:nodes.map((n,i)=>({\n name:n.name,value:n.value,\n symbolSize:3.5+Math.sqrt(n.value)*1.7,\n itemStyle:{color:ORGS[n.org][2],borderWidth:0},\n // only the giants get a label — 60 names would be soup\n label:{show:n.value>=26,position:'right',distance:4,\n color:'#ACAD79',fontFamily:'Inter',fontSize:8.5,fontWeight:600},\n })),\n links:links.map(l=>({\n source:l.source,target:l.target,\n rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,\n lineStyle:{\n width:.4+l.w*.28,\n color:l.same?'#405333':'#334329',\n opacity:l.same?.5:.32,\n curveness:.3,\n },\n })),\n emphasis:{\n focus:'adjacency',\n lineStyle:{color:'#F2D17E',opacity:.95,width:1.6},\n label:{show:true,color:'#F2D17E'},\n },\n blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},\n }],\n};\nconst play=()=>{g.clear();g.setOption(opt)};\nplay();\ndocument.getElementById('ch').addEventListener('click',play);\naddEventListener('resize',()=>g.resize());\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":4161,"content_sha256":"e198cae1a9d1c04c1290b6527572052ea17fea90bc420ac4ab7ab1d08d985265"},{"filename":"templates/color/big-circular-porcelain.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>青瓷蓝 — Circular Graph, Dense\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F7F2EB;--dark:#081F5C}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:#EDEFF1;border-radius:24px;padding:34px 34px 24px;\n width:min(1100px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#809EC6;margin-bottom:10px}\n .src{font-size:9.5px;color:#3E5A8F;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{height:78vh;min-height:620px;cursor:pointer}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>Six hundred collaborations, one ring\u003c/h2>\n \u003cdiv class=\"sub\">60 repos · line = shared contributors · hover a repo to light up its world · click to replay · 蓝阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走纸白微蓝。\u003c/div>\n \u003cdiv id=\"ch\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG · PORCELAIN\u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#081F5C',PAPER='#EDEFF1';\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── 60 repos in 5 org clusters; cluster = shade family ──\nconst ORGS=[\n ['platform',16,'#EDEFF1'], // paper — the core\n ['product',14,'#BCC7D7'],\n ['data',12,'#9EB3CD'],\n ['infra',10,'#6C93C7'],\n ['labs',8,'#3472C2'],\n];\nconst nodes=[];\nORGS.forEach(([org,count],oi)=>{\n for(let i=0;i\u003ccount;i++){\n // activity: a few giants per cluster, long tail of small repos\n const heavy=i\u003c2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;\n nodes.push({\n name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),\n org:oi,value:Math.round(heavy),\n });\n }\n});\n// links: dense inside a cluster, sparser across; weight = shared contributors\nconst links=[];\nconst idx={};nodes.forEach((n,i)=>idx[n.name]=i);\nnodes.forEach((a,i)=>{\n nodes.forEach((b,j)=>{\n if(j\u003c=i)return;\n const same=a.org===b.org;\n const p=same?.16:.045;\n if(rnd(i+1,j+2)\u003cp){\n links.push({source:i,target:j,\n w:1+Math.round(rnd(i+2,j+3)*6),same});\n }\n });\n});\n\nconst g=echarts.init(document.getElementById('ch'));\nconst opt={\n animationDuration:1800,animationEasing:'quarticOut',\n tooltip:{backgroundColor:'#F7F2EB',borderWidth:0,\n textStyle:{color:'#081F5C',fontFamily:'Inter',fontSize:12},padding:[10,14],\n formatter:p=>p.dataType==='node'\n ?p.name+' — '+p.value+' contributors'\n :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},\n series:[{\n type:'graph',layout:'circular',\n circular:{rotateLabel:true},\n left:24,right:24,top:24,bottom:24,\n data:nodes.map((n,i)=>({\n name:n.name,value:n.value,\n symbolSize:3.5+Math.sqrt(n.value)*1.7,\n itemStyle:{color:ORGS[n.org][2],borderWidth:0},\n // only the giants get a label — 60 names would be soup\n label:{show:n.value>=26,position:'right',distance:4,\n color:'#9EB3CD',fontFamily:'Inter',fontSize:8.5,fontWeight:600},\n })),\n links:links.map(l=>({\n source:l.source,target:l.target,\n rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,\n lineStyle:{\n width:.4+l.w*.28,\n color:l.same?'#32517B':'#263E5E',\n opacity:l.same?.5:.32,\n curveness:.3,\n },\n })),\n emphasis:{\n focus:'adjacency',\n lineStyle:{color:'#EDEFF1',opacity:.95,width:1.6},\n label:{show:true,color:'#EDEFF1'},\n },\n blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},\n }],\n};\nconst play=()=>{g.clear();g.setOption(opt)};\nplay();\ndocument.getElementById('ch').addEventListener('click',play);\naddEventListener('resize',()=>g.resize());\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":4169,"content_sha256":"f652468e513df6f32aa038490063a1cd3033ea637f48e173760304b2709a76eb"},{"filename":"templates/color/big-force-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>椰林绿 — Force Graph, Dense\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#28311F}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:#F0EFE9;border-radius:24px;padding:34px 34px 24px;\n width:min(1100px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#92A763;margin-bottom:10px}\n .src{font-size:9.5px;color:#5A6B47;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{height:78vh;min-height:620px;cursor:grab}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>A galaxy of one hundred eighty services\u003c/h2>\n \u003cdiv class=\"sub\">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay · 绿黄阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走麦黄。\u003c/div>\n \u003cdiv id=\"ch\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH · PALM\u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#28311F',PAPER='#F0EFE9';\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── 8 domain hubs, each with a swarm of services; hubs interconnect ──\nconst DOMAINS=[\n ['auth',26,'#F0EFE9'],['billing',24,'#DDDBC9'],['content',28,'#C8CAA8'],\n ['search',20,'#AFB885'],['notify',22,'#92A763'],['media',20,'#7EA056'],\n ['analytics',18,'#658D49'],['edge',14,'#4F7D3E'],\n];\nconst nodes=[],links=[];\nDOMAINS.forEach(([dom,count,shade],di)=>{\n // the hub\n nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});\n // satellites: long-tail sizes, a couple of mid-weights per domain\n for(let i=0;i\u003ccount;i++){\n const v=i\u003c3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;\n const name=dom+'-'+String(i+1).padStart(2,'0');\n nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});\n // satellite -> its hub\n links.push({source:name,target:dom,\n lineStyle:{width:.5+v*.05,color:'#405333',opacity:.5}});\n // occasional satellite-to-satellite shortcut inside the domain\n if(i>1&&rnd(di+3,i+11)\u003c.22){\n links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),\n lineStyle:{width:.5,color:'#334329',opacity:.4}});\n }\n }\n});\n// hub-to-hub backbone: every domain talks to 2-3 others\nconst H=DOMAINS.length;\nfor(let a=0;a\u003cH;a++)for(let b=a+1;b\u003cH;b++){\n if(rnd(a+7,b+9)\u003c.42){\n links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],\n lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#405333',opacity:.65,curveness:.08}});\n }\n}\n// a few cross-domain satellite shortcuts — the messy real-world wires\nfor(let k=0;k\u003c26;k++){\n const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];\n const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];\n if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){\n links.push({source:a.name,target:b.name,\n lineStyle:{width:.5,color:'#2C3823',opacity:.38,curveness:.15}});\n }\n}\n\nconst g=echarts.init(document.getElementById('ch'));\nconst opt={\n animationDuration:300,\n tooltip:{backgroundColor:'#F0EFEB',borderWidth:0,\n textStyle:{color:'#28311F',fontFamily:'Inter',fontSize:12},padding:[10,14],\n formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},\n roam:true,draggable:true,\n left:6,right:6,top:6,bottom:6,\n data:nodes.map(n=>({\n name:n.name,value:n.value,\n symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,\n itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},\n label:{show:n.hub,position:'inside',\n color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},\n })),\n links,\n emphasis:{\n focus:'adjacency',\n lineStyle:{color:'#F2D17E',opacity:.95,width:1.4},\n label:{show:true,color:PAPER,position:'right'},\n },\n blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},\n }],\n};\nconst play=()=>{g.clear();g.setOption(opt)};\nplay();\n// click on empty canvas replays; clicks on nodes are for dragging\ng.getZr().on('click',e=>{if(!e.target)play()});\naddEventListener('resize',()=>g.resize());\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":4773,"content_sha256":"371b427f601a714632b913349667a6841a554dbbed879fc257e0294d21482cf2"},{"filename":"templates/color/big-force-porcelain.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>青瓷蓝 — Force Graph, Dense\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F7F2EB;--dark:#081F5C}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:#EDEFF1;border-radius:24px;padding:34px 34px 24px;\n width:min(1100px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#809EC6;margin-bottom:10px}\n .src{font-size:9.5px;color:#3E5A8F;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{height:78vh;min-height:620px;cursor:grab}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>A galaxy of one hundred eighty services\u003c/h2>\n \u003cdiv class=\"sub\">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay · 蓝阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走纸白微蓝。\u003c/div>\n \u003cdiv id=\"ch\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH · PORCELAIN\u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#081F5C',PAPER='#EDEFF1';\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── 8 domain hubs, each with a swarm of services; hubs interconnect ──\nconst DOMAINS=[\n ['auth',26,'#EDEFF1'],['billing',24,'#D5DBE2'],['content',28,'#BCC7D7'],\n ['search',20,'#9EB3CD'],['notify',22,'#809EC6'],['media',20,'#6C93C7'],\n ['analytics',18,'#4D82C6'],['edge',14,'#3472C2'],\n];\nconst nodes=[],links=[];\nDOMAINS.forEach(([dom,count,shade],di)=>{\n // the hub\n nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});\n // satellites: long-tail sizes, a couple of mid-weights per domain\n for(let i=0;i\u003ccount;i++){\n const v=i\u003c3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;\n const name=dom+'-'+String(i+1).padStart(2,'0');\n nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});\n // satellite -> its hub\n links.push({source:name,target:dom,\n lineStyle:{width:.5+v*.05,color:'#32517B',opacity:.5}});\n // occasional satellite-to-satellite shortcut inside the domain\n if(i>1&&rnd(di+3,i+11)\u003c.22){\n links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),\n lineStyle:{width:.5,color:'#263E5E',opacity:.4}});\n }\n }\n});\n// hub-to-hub backbone: every domain talks to 2-3 others\nconst H=DOMAINS.length;\nfor(let a=0;a\u003cH;a++)for(let b=a+1;b\u003cH;b++){\n if(rnd(a+7,b+9)\u003c.42){\n links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],\n lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#32517B',opacity:.65,curveness:.08}});\n }\n}\n// a few cross-domain satellite shortcuts — the messy real-world wires\nfor(let k=0;k\u003c26;k++){\n const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];\n const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];\n if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){\n links.push({source:a.name,target:b.name,\n lineStyle:{width:.5,color:'#213650',opacity:.38,curveness:.15}});\n }\n}\n\nconst g=echarts.init(document.getElementById('ch'));\nconst opt={\n animationDuration:300,\n tooltip:{backgroundColor:'#F7F2EB',borderWidth:0,\n textStyle:{color:'#081F5C',fontFamily:'Inter',fontSize:12},padding:[10,14],\n formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},\n roam:true,draggable:true,\n left:6,right:6,top:6,bottom:6,\n data:nodes.map(n=>({\n name:n.name,value:n.value,\n symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,\n itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},\n label:{show:n.hub,position:'inside',\n color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},\n })),\n links,\n emphasis:{\n focus:'adjacency',\n lineStyle:{color:'#EDEFF1',opacity:.95,width:1.4},\n label:{show:true,color:PAPER,position:'right'},\n },\n blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},\n }],\n};\nconst play=()=>{g.clear();g.setOption(opt)};\nplay();\n// click on empty canvas replays; clicks on nodes are for dragging\ng.getZr().on('click',e=>{if(!e.target)play()});\naddEventListener('resize',()=>g.resize());\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":4781,"content_sha256":"d8d483ff8b254d5bd4d4fa029c8f984119938e353520178bbb6032602ac7346b"},{"filename":"templates/color/big-threads-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>椰林绿 — Thread Triptych, Interactive\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#28311F;--paper:#F0EFEB}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:var(--paper);border-radius:24px;padding:34px 34px 24px;\n width:min(1200px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#92A763;margin-bottom:6px}\n .src{font-size:9.5px;color:#5A6B47;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{width:100%;height:74vh;min-height:560px;display:block}\n #status{height:34px;display:flex;align-items:center;gap:10px;\n font-size:12px;color:#92A763;border-top:1px solid #38452C;margin-top:6px;padding-top:8px}\n #status b{color:var(--paper);font-weight:700}\n #status .pin{font-size:9px;letter-spacing:.1em;color:#5A6B47;font-weight:700}\n svg text{font-family:'Inter',sans-serif;user-select:none}\n\n /* thread states */\n .thread{transition:opacity .18s ease,stroke-width .18s ease}\n .thread.hot{opacity:.95 !important}\n svg.focused .thread:not(.hot){opacity:.035 !important}\n .nodelab{cursor:pointer;transition:opacity .18s ease}\n svg.focused .nodelab:not(.hot){opacity:.25}\n .hit{cursor:pointer}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>Where the data actually goes — now answerable\u003c/h2>\n \u003cdiv class=\"sub\">source → processor → destination · one thread per data path · hover to trace a route · hover any label to pull its whole bundle · click to pin, click dark space to release\u003c/div>\n \u003csvg id=\"ch\" viewBox=\"0 0 1140 640\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv id=\"status\">\u003cspan id=\"stext\">hover a thread or a label\u003c/span>\u003cspan class=\"pin\" id=\"spin\">\u003c/span>\u003c/div>\n \u003cdiv class=\"src\">THREAD TRIPTYCH · INTERACTIVE · DATA MAP · PALM\u003c/div>\n\u003c/div>\n\u003cscript>\nconst PAPER='#F0EFE9',NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ── the pipeline ──\nconst SRC=['web-app','ios-app','android','desktop','cli','public-api','webhooks','forms','email-in','chat-widget',\n 'billing-ev','auth-ev','search-log','audit-ev','crash-rep','perf-beacon','cdn-log','edge-log','job-ev','sync-ev',\n 'import','export-req','admin-ui','partner-api','iot-ping','backfill'];\nconst MIDV=['parse','validate','dedupe','enrich','geoip','sessionize','pii-mask','score','sample','route',\n 'batch','window','join','flatten','filter','hash','encrypt','compact','index','tag'];\nconst MID=MIDV.flatMap(v=>[v+'-a',v+'-b']); // 40 processors\nconst DST=['WAREHOUSE','ANALYTICS','CRM','ADS','BILLING','SEARCH','ML PIPE','ARCHIVE','AUDIT','EXPORTS'];\n\n// routes: each source emits 2-3 full paths; volume is long-tailed\nconst routes=[];\nSRC.forEach((s,i)=>{\n const n=2+Math.floor(rnd(i+1,5)*2);\n for(let k=0;k\u003cn;k++){\n const mi=Math.floor(rnd(i*3+k+2,13)*MID.length);\n const di=Math.min(9,Math.floor(rnd(i*5+k+4,19)**2*11)); // skew: low dsts hoard\n const v=Math.round(1+rnd(i*7+k+6,29)**2*30); // 1..31, long tail\n routes.push({s:i,m:mi,d:di,v});\n }\n});\n\n// ── geometry ──\nconst X1=180,X2=560,X3=930;\nconst sy=i=>36+i*22.6, my=i=>30+i*14.6, dy=i=>60+i*57;\nconst path=r=>`M${X1+6} ${sy(r.s)} C${(X1+X2)/2} ${sy(r.s)} ${(X1+X2)/2} ${my(r.m)} ${X2} ${my(r.m)}`\n +` C${(X2+X3)/2} ${my(r.m)} ${(X2+X3)/2} ${dy(r.d)} ${X3-6} ${dy(r.d)}`;\n\nconst svg=document.getElementById('ch');\nconst stext=document.getElementById('stext'),spin=document.getElementById('spin');\nconst fmt=v=>v+'k ev/day';\nlet pinned=null;\n\n// column headers\n[[X1,'① SOURCE'],[X2,'② PROCESSOR'],[X3,'③ DESTINATION']].forEach(([x,t])=>\n txt(svg,{x,y:14,'font-size':9,'font-weight':800,fill:PAPER,'letter-spacing':'.14em','text-anchor':'middle'},t));\n\n// threads (drawn first, under labels)\nconst gThreads=el(svg,'g',{});\nroutes.forEach((r,ri)=>{\n r.el=el(gThreads,'path',{d:path(r),fill:'none',stroke:PAPER,\n 'stroke-width':Math.max(.6,r.v*.14),class:'thread',\n opacity:.06+Math.min(.2,r.v*.012),'stroke-linecap':'round'});\n // invisible fat twin: the hover/click target\n r.hit=el(gThreads,'path',{d:path(r),fill:'none',stroke:'#000','stroke-opacity':0,\n 'stroke-width':9,class:'hit'});\n r.hit.dataset.route=ri;\n});\n\n// node labels + ticks; each carries hit metadata\nconst mkNode=(x,y,label,anchor,cls,meta,size,fill)=>{\n const g=el(svg,'g',{class:'nodelab'});\n g.dataset.kind=meta.kind;g.dataset.idx=meta.idx;\n el(g,'rect',{x:x-3,y:y-1.6,width:6,height:3.2,fill:PAPER,opacity:.85});\n txt(g,{x:anchor==='end'?x-9:x+9,y:y+2.6,'font-size':size,'font-weight':600,\n fill,'text-anchor':anchor},label);\n // generous invisible hit zone\n el(g,'rect',{x:anchor==='end'?x-92:x-6,y:y-6.5,width:98,height:13,fill:'#000','fill-opacity':0,class:'hit'});\n return g;\n};\nSRC.forEach((s,i)=>mkNode(X1,sy(i),s,'end','',{kind:'s',idx:i},7.5,'#ACAD79'));\nMID.forEach((m,i)=>mkNode(X2,my(i),m,'start','',{kind:'m',idx:i},5.4,'#4F7D3E'));\nDST.forEach((d,i)=>{\n const g=el(svg,'g',{class:'nodelab'});\n g.dataset.kind='d';g.dataset.idx=i;\n el(g,'circle',{cx:X3,cy:dy(i),r:3.4,fill:PAPER});\n txt(g,{x:X3+12,y:dy(i)+3,'font-size':8.5,'font-weight':700,fill:'#ACAD79','letter-spacing':'.08em'},d);\n el(g,'rect',{x:X3-8,y:dy(i)-10,width:120,height:20,fill:'#000','fill-opacity':0,class:'hit'});\n});\n\n// ── highlight machinery ──\nconst nodeLabs=[...svg.querySelectorAll('.nodelab')];\nconst clear=()=>{\n svg.classList.remove('focused');\n routes.forEach(r=>r.el.classList.remove('hot'));\n nodeLabs.forEach(n=>n.classList.remove('hot'));\n stext.textContent='hover a thread or a label';spin.textContent='';\n};\nconst focusRoutes=(rs,label)=>{\n svg.classList.add('focused');\n routes.forEach(r=>r.el.classList.toggle('hot',rs.includes(r)));\n nodeLabs.forEach(n=>{\n const k=n.dataset.kind,ix=+n.dataset.idx;\n n.classList.toggle('hot',rs.some(r=>(k==='s'&&r.s===ix)||(k==='m'&&r.m===ix)||(k==='d'&&r.d===ix)));\n });\n stext.innerHTML=label;\n};\nconst routeLabel=r=>`\u003cb>${SRC[r.s]}\u003c/b> → ${MID[r.m]} → \u003cb>${DST[r.d]}\u003c/b> · ${fmt(r.v)}`;\nconst nodeSel=(kind,idx)=>{\n const rs=routes.filter(r=>(kind==='s'&&r.s===idx)||(kind==='m'&&r.m===idx)||(kind==='d'&&r.d===idx));\n const total=rs.reduce((a,r)=>a+r.v,0);\n const name=kind==='s'?SRC[idx]:kind==='m'?MID[idx]:DST[idx];\n return [rs,`\u003cb>${name}\u003c/b> · ${rs.length} path${rs.length>1?'s':''} · ${fmt(total)} total`];\n};\n\nconst apply=t=>{\n const hit=t.closest?.('.hit');\n if(!hit)return null;\n const rg=hit.dataset.route,ng=hit.closest('.nodelab');\n if(rg!=null){const r=routes[+rg];return()=>focusRoutes([r],routeLabel(r))}\n if(ng){const [rs,lab]=nodeSel(ng.dataset.kind,+ng.dataset.idx);return()=>focusRoutes(rs,lab)}\n return null;\n};\nsvg.addEventListener('mousemove',e=>{\n if(pinned)return;\n const f=apply(e.target);\n f?f():clear();\n});\nsvg.addEventListener('mouseleave',()=>{if(!pinned)clear()});\nsvg.addEventListener('click',e=>{\n const f=apply(e.target);\n if(f){pinned=f;f();spin.textContent='PINNED · CLICK DARK SPACE TO RELEASE'}\n else{pinned=null;clear()}\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":7724,"content_sha256":"d57707118b1e8f498d3b908cde1bad0d75ec8a3ea503ec407a2b10bab6661920"},{"filename":"templates/color/big-threads-porcelain.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>青瓷蓝 — Thread Triptych, Interactive\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F7F2EB;--dark:#081F5C;--paper:#F7F2EB}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;\n display:flex;justify-content:center}\n .card{background:var(--dark);color:var(--paper);border-radius:24px;padding:34px 34px 24px;\n width:min(1200px,100%)}\n h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}\n .sub{font-size:12px;color:#809EC6;margin-bottom:6px}\n .src{font-size:9.5px;color:#3E5A8F;margin-top:12px;letter-spacing:.08em;font-weight:500}\n #ch{width:100%;height:74vh;min-height:560px;display:block}\n #status{height:34px;display:flex;align-items:center;gap:10px;\n font-size:12px;color:#809EC6;border-top:1px solid #1B3159;margin-top:6px;padding-top:8px}\n #status b{color:var(--paper);font-weight:700}\n #status .pin{font-size:9px;letter-spacing:.1em;color:#3E5A8F;font-weight:700}\n svg text{font-family:'Inter',sans-serif;user-select:none}\n\n /* thread states */\n .thread{transition:opacity .18s ease,stroke-width .18s ease}\n .thread.hot{opacity:.95 !important}\n svg.focused .thread:not(.hot){opacity:.035 !important}\n .nodelab{cursor:pointer;transition:opacity .18s ease}\n svg.focused .nodelab:not(.hot){opacity:.25}\n .hit{cursor:pointer}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003ch2>Where the data actually goes — now answerable\u003c/h2>\n \u003cdiv class=\"sub\">source → processor → destination · one thread per data path · hover to trace a route · hover any label to pull its whole bundle · click to pin, click dark space to release\u003c/div>\n \u003csvg id=\"ch\" viewBox=\"0 0 1140 640\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv id=\"status\">\u003cspan id=\"stext\">hover a thread or a label\u003c/span>\u003cspan class=\"pin\" id=\"spin\">\u003c/span>\u003c/div>\n \u003cdiv class=\"src\">THREAD TRIPTYCH · INTERACTIVE · DATA MAP · PORCELAIN\u003c/div>\n\u003c/div>\n\u003cscript>\nconst PAPER='#EDEFF1',NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ── the pipeline ──\nconst SRC=['web-app','ios-app','android','desktop','cli','public-api','webhooks','forms','email-in','chat-widget',\n 'billing-ev','auth-ev','search-log','audit-ev','crash-rep','perf-beacon','cdn-log','edge-log','job-ev','sync-ev',\n 'import','export-req','admin-ui','partner-api','iot-ping','backfill'];\nconst MIDV=['parse','validate','dedupe','enrich','geoip','sessionize','pii-mask','score','sample','route',\n 'batch','window','join','flatten','filter','hash','encrypt','compact','index','tag'];\nconst MID=MIDV.flatMap(v=>[v+'-a',v+'-b']); // 40 processors\nconst DST=['WAREHOUSE','ANALYTICS','CRM','ADS','BILLING','SEARCH','ML PIPE','ARCHIVE','AUDIT','EXPORTS'];\n\n// routes: each source emits 2-3 full paths; volume is long-tailed\nconst routes=[];\nSRC.forEach((s,i)=>{\n const n=2+Math.floor(rnd(i+1,5)*2);\n for(let k=0;k\u003cn;k++){\n const mi=Math.floor(rnd(i*3+k+2,13)*MID.length);\n const di=Math.min(9,Math.floor(rnd(i*5+k+4,19)**2*11)); // skew: low dsts hoard\n const v=Math.round(1+rnd(i*7+k+6,29)**2*30); // 1..31, long tail\n routes.push({s:i,m:mi,d:di,v});\n }\n});\n\n// ── geometry ──\nconst X1=180,X2=560,X3=930;\nconst sy=i=>36+i*22.6, my=i=>30+i*14.6, dy=i=>60+i*57;\nconst path=r=>`M${X1+6} ${sy(r.s)} C${(X1+X2)/2} ${sy(r.s)} ${(X1+X2)/2} ${my(r.m)} ${X2} ${my(r.m)}`\n +` C${(X2+X3)/2} ${my(r.m)} ${(X2+X3)/2} ${dy(r.d)} ${X3-6} ${dy(r.d)}`;\n\nconst svg=document.getElementById('ch');\nconst stext=document.getElementById('stext'),spin=document.getElementById('spin');\nconst fmt=v=>v+'k ev/day';\nlet pinned=null;\n\n// column headers\n[[X1,'① SOURCE'],[X2,'② PROCESSOR'],[X3,'③ DESTINATION']].forEach(([x,t])=>\n txt(svg,{x,y:14,'font-size':9,'font-weight':800,fill:PAPER,'letter-spacing':'.14em','text-anchor':'middle'},t));\n\n// threads (drawn first, under labels)\nconst gThreads=el(svg,'g',{});\nroutes.forEach((r,ri)=>{\n r.el=el(gThreads,'path',{d:path(r),fill:'none',stroke:PAPER,\n 'stroke-width':Math.max(.6,r.v*.14),class:'thread',\n opacity:.06+Math.min(.2,r.v*.012),'stroke-linecap':'round'});\n // invisible fat twin: the hover/click target\n r.hit=el(gThreads,'path',{d:path(r),fill:'none',stroke:'#000','stroke-opacity':0,\n 'stroke-width':9,class:'hit'});\n r.hit.dataset.route=ri;\n});\n\n// node labels + ticks; each carries hit metadata\nconst mkNode=(x,y,label,anchor,cls,meta,size,fill)=>{\n const g=el(svg,'g',{class:'nodelab'});\n g.dataset.kind=meta.kind;g.dataset.idx=meta.idx;\n el(g,'rect',{x:x-3,y:y-1.6,width:6,height:3.2,fill:PAPER,opacity:.85});\n txt(g,{x:anchor==='end'?x-9:x+9,y:y+2.6,'font-size':size,'font-weight':600,\n fill,'text-anchor':anchor},label);\n // generous invisible hit zone\n el(g,'rect',{x:anchor==='end'?x-92:x-6,y:y-6.5,width:98,height:13,fill:'#000','fill-opacity':0,class:'hit'});\n return g;\n};\nSRC.forEach((s,i)=>mkNode(X1,sy(i),s,'end','',{kind:'s',idx:i},7.5,'#9EB3CD'));\nMID.forEach((m,i)=>mkNode(X2,my(i),m,'start','',{kind:'m',idx:i},5.4,'#3472C2'));\nDST.forEach((d,i)=>{\n const g=el(svg,'g',{class:'nodelab'});\n g.dataset.kind='d';g.dataset.idx=i;\n el(g,'circle',{cx:X3,cy:dy(i),r:3.4,fill:PAPER});\n txt(g,{x:X3+12,y:dy(i)+3,'font-size':8.5,'font-weight':700,fill:'#9EB3CD','letter-spacing':'.08em'},d);\n el(g,'rect',{x:X3-8,y:dy(i)-10,width:120,height:20,fill:'#000','fill-opacity':0,class:'hit'});\n});\n\n// ── highlight machinery ──\nconst nodeLabs=[...svg.querySelectorAll('.nodelab')];\nconst clear=()=>{\n svg.classList.remove('focused');\n routes.forEach(r=>r.el.classList.remove('hot'));\n nodeLabs.forEach(n=>n.classList.remove('hot'));\n stext.textContent='hover a thread or a label';spin.textContent='';\n};\nconst focusRoutes=(rs,label)=>{\n svg.classList.add('focused');\n routes.forEach(r=>r.el.classList.toggle('hot',rs.includes(r)));\n nodeLabs.forEach(n=>{\n const k=n.dataset.kind,ix=+n.dataset.idx;\n n.classList.toggle('hot',rs.some(r=>(k==='s'&&r.s===ix)||(k==='m'&&r.m===ix)||(k==='d'&&r.d===ix)));\n });\n stext.innerHTML=label;\n};\nconst routeLabel=r=>`\u003cb>${SRC[r.s]}\u003c/b> → ${MID[r.m]} → \u003cb>${DST[r.d]}\u003c/b> · ${fmt(r.v)}`;\nconst nodeSel=(kind,idx)=>{\n const rs=routes.filter(r=>(kind==='s'&&r.s===idx)||(kind==='m'&&r.m===idx)||(kind==='d'&&r.d===idx));\n const total=rs.reduce((a,r)=>a+r.v,0);\n const name=kind==='s'?SRC[idx]:kind==='m'?MID[idx]:DST[idx];\n return [rs,`\u003cb>${name}\u003c/b> · ${rs.length} path${rs.length>1?'s':''} · ${fmt(total)} total`];\n};\n\nconst apply=t=>{\n const hit=t.closest?.('.hit');\n if(!hit)return null;\n const rg=hit.dataset.route,ng=hit.closest('.nodelab');\n if(rg!=null){const r=routes[+rg];return()=>focusRoutes([r],routeLabel(r))}\n if(ng){const [rs,lab]=nodeSel(ng.dataset.kind,+ng.dataset.idx);return()=>focusRoutes(rs,lab)}\n return null;\n};\nsvg.addEventListener('mousemove',e=>{\n if(pinned)return;\n const f=apply(e.target);\n f?f():clear();\n});\nsvg.addEventListener('mouseleave',()=>{if(!pinned)clear()});\nsvg.addEventListener('click',e=>{\n const f=apply(e.target);\n if(f){pinned=f;f();spin.textContent='PINNED · CLICK DARK SPACE TO RELEASE'}\n else{pinned=null;clear()}\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":7729,"content_sha256":"071e71236a0a04c24f1d72b5cda82343a98512214dc745d6db66943dc893257a"},{"filename":"templates/color/glance-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Glance 20 × PALM\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/chart.js@4\">\u003c/script>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#58402E;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.40);--grid:rgba(88,64,46,.16);--lab:rgba(88,64,46,.72);--dmid:rgba(240,239,235,.62);--dq:rgba(240,239,235,.24)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.dark{background:var(--dark);color:var(--bg)}\n .card.dark .sub{color:var(--dmid)}\n .card.dark .src{color:var(--dq)}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .ch{height:320px}\n .wrap{position:relative;height:320px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .pagehead .badge{margin:0 2px}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px}\n .badge.del{background:var(--ink);color:var(--bg)}\n .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}\n .badge.pair{border:1px dashed var(--faint);color:var(--muted)}\n .card.dark .badge.del{background:var(--bg);color:var(--ink)}\n .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}\n .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Glance 系(快读)· 20 张 × PALM\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#43593B\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#F2D17E\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#77835A\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#ACAD79\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#58402E\">\u003c/span>  椰林 · 绿黄六色板:麦黄 → 橄榄 → 浅苔 → 苔绿 → 中绿 → 深绿。深绿当主墨、琥珀当主角、浓咖当文字;暗卡上强调改麦黄(最宽波幅 / 直播灯 / 集成中枢)——深绿在浓咖底上读不出。\u003c/p>\n \u003cp style=\"margin-top:7px\">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 个来源 / 4 条产品线 / 5 个工作日 / 10 支小队 / 12 个集成)各走一个分类位,序数型(8 年营收排名 / 50 个市场 / 4 档优先级 / 树木年份 / 面板停留时长 / 情绪票数)走 5 档梯,深色卡换纸色梯、末档落在强调色。几何、动画、数据与 glance-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003c!-- ══ mono-demo ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Revenue by plan\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · MRR, $K\u003c/div>\n \u003cdiv class=\"wrap\">\u003ccanvas id=\"c3\">\u003c/canvas>\u003c/div>\n \u003cdiv class=\"src\">CHUNKY BARS · MONO-DEMO · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-redesign · dot waffle ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where sign-ups come from\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · every dot = 1% of new accounts\u003c/div>\n \u003csvg id=\"waffle\" viewBox=\"0 0 380 300\" width=\"100%\" style=\"max-height:320px\">\u003c/svg>\n \u003cdiv class=\"src\">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Trees planted, year by year\u003c/h2>\n \u003cdiv class=\"sub\">reforestation program · thousands of trees\u003c/div>\n \u003cdiv class=\"ch\" id=\"forest\">\u003c/div>\n \u003cdiv class=\"src\">PICTORIAL BAR · MONO-FANCY · GREEN OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Who works with whom\u003c/h2>\n \u003cdiv class=\"sub\">cross-team collaborations · Q2 2026 · line = shared project\u003c/div>\n \u003cdiv class=\"ch\" id=\"circular\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Everything the platform ships\u003c/h2>\n \u003cdiv class=\"sub\">product areas and capabilities · Jun 2026\u003c/div>\n \u003cdiv class=\"ch\" id=\"tree\">\u003c/div>\n \u003cdiv class=\"src\">TREE · MONO-FANCY · PRODUCT CATALOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Campaigns rain down, sign-ups flow\u003c/h2>\n \u003cdiv class=\"sub\">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days\u003c/div>\n \u003cdiv class=\"ch\" id=\"rainfall\">\u003c/div>\n \u003cdiv class=\"src\">DUAL AREA · MONO-FANCY · GROWTH + ADS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>One dataset, three views\u003c/h2>\n \u003cdiv class=\"sub\">watch the same 12 products fly between views · every 3s\u003c/div>\n \u003cdiv class=\"ch\" id=\"morph\">\u003c/div>\n \u003cdiv class=\"src\">SCATTER MORPH · MONO-FANCY · COHORTS\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy2 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where we gained, where we bled\u003c/h2>\n \u003cdiv class=\"sub\">net accounts per segment · Q2 2026 · churn left, growth right\u003c/div>\n \u003cdiv class=\"ch\" id=\"negbar\">\u003c/div>\n \u003cdiv class=\"src\">DIVERGING BAR · MONO-FANCY2 · CRM\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Integrations, pulled into orbit\u003c/h2>\n \u003cdiv class=\"sub\">force layout · node = monthly syncs · drag a node, the web fights back\u003c/div>\n \u003cdiv class=\"ch\" id=\"force\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · NEW · INTEGRATION HUB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Fifty markets, one wave\u003c/h2>\n \u003cdiv class=\"sub\">sign-ups per market · bars cascade in · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"wave\">\u003c/div>\n \u003cdiv class=\"src\">STAGGER DELAY · MONO-FANCY2 · GROWTH\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy3 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Big slice, deep engagement\u003c/h2>\n \u003cdiv class=\"sub\">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min\u003c/div>\n \u003cdiv class=\"ch\" id=\"custompie\">\u003c/div>\n \u003cdiv class=\"src\">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Support load, day by day\u003c/h2>\n \u003cdiv class=\"sub\">tickets per hour · each row is its own axis · dot = volume\u003c/div>\n \u003cdiv class=\"ch\" id=\"singleaxis\">\u003c/div>\n \u003cdiv class=\"src\">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Response times, spread out\u003c/h2>\n \u003cdiv class=\"sub\">every dot = one ticket · jitter reveals the pile-up · by priority\u003c/div>\n \u003cdiv class=\"ch\" id=\"jitter\">\u003c/div>\n \u003cdiv class=\"src\">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy4 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Eight products race for revenue\u003c/h2>\n \u003cdiv class=\"sub\">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"race\">\u003c/div>\n \u003cdiv class=\"src\">BAR RACE · MONO-FANCY4 · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Concurrent users, streaming\u003c/h2>\n \u003cdiv class=\"sub\">one new reading every 300ms · window of 50 · click to restart\u003c/div>\n \u003cdiv class=\"ch\" id=\"stream\">\u003c/div>\n \u003cdiv class=\"src\">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>H1 revenue, drawn in one stroke\u003c/h2>\n \u003cdiv class=\"sub\">cumulative ARR · the line draws, the number counts · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"stroke\">\u003c/div>\n \u003cdiv class=\"src\">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE\u003c/div>\n \u003c/div>\n\n\n \u003c!-- G19 · violin -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 分布型 · 小提琴\u003c/span>\n \u003ch2>How fast each plan gets an answer\u003c/h2>\n \u003cdiv class=\"sub\">hours to first reply · silhouette = where tickets pile up · paper tick = median\u003c/div>\n \u003csvg id=\"violin\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">VIOLIN · PALM · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- G20 · matrix heat -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 热力型 · 通用矩阵\u003c/span>\n \u003ch2>Adoption runs hot on the new versions\u003c/h2>\n \u003cdiv class=\"sub\">5 versions × 6 features · shade = % of accounts using it · every cell carries its number\u003c/div>\n \u003csvg id=\"gheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">MATRIX HEAT (GLANCE) · PALM · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- G21 · rank strip -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 排名型 · RANK STRIP\u003c/span>\n \u003ch2>Flows climbs to the top\u003c/h2>\n \u003cdiv class=\"sub\">one row per product, one cell per quarter · darker = higher rank · a bar race you can print\u003c/div>\n \u003csvg id=\"rankstrip\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RANK STRIP · PALM · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- G22 · aggregate sankey -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 流向型 · 聚合桑基\u003c/span>\n \u003ch2>Channels pour into plans\u003c/h2>\n \u003cdiv class=\"sub\">ribbon width = accounts · channels on the left, the plans they land in on the right\u003c/div>\n \u003csvg id=\"sankey\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">AGGREGATE SANKEY · PALM · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── PALM 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F0EFEB',DARK='#58402E',TXT='#58402E';\nconst MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';\nconst DATA='#43593B',DATA2='#58402E',HERO='#D4A017';\nconst HERODK='#F2D17E'; // 深色卡上的强调色(HERO 在深底上可能读不出)\nconst FAINTDATA='#ACAD79'; // 静音数据档:周末 / 非主序列\nconst DMID='rgba(240,239,235,.62)',DQ='rgba(240,239,235,.24)'; // 深色卡上的浅墨 / 静音档\nconst SER=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79','#58402E']; // 分类位:一图之内每个系列 / 每类 / 每行一个色\nconst RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B']; // 5 档序数梯(浅→重):排名 / 票量 / 峰值 / 停留时长\nconst DRAMP4=['#F2D17E','#ACAD79','rgba(240,239,235,.55)','rgba(240,239,235,.26)']; // 深色卡上的 4 档序数梯(浅→深)\nconst HEROMODE=false; // 主角模式:分类位的头号项也整体走 HERO\n\n// ── 别名与派生:深色卡的线全是卡底向纸色的插值,不引入新色相 ──\nconst INK=TXT,PAPER=BG,MUTED=MUT;\nconst rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));\nconst alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;\nconst mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');\nconst lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v\u003c=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));\n return .2126*r+.7152*g+.0722*b};\nconst ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};\nconst readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG); // 深色卡上落在色块里的字\nconst DLINE=mix(DARK,BG,.20),DLINE2=mix(DARK,BG,.13),DSHELL=mix(DARK,BG,.09);\nconst hi=c=>HEROMODE?HERO:c;\n// 名次 → 序数梯。末档(WIRE 的红)只发给第 1 名,其余摊在前四档上\nconst rampAt=(i,n)=>i===0?RAMP[4]\n :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];\nconst rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);\n return i=>r.indexOf(i)};\nconst tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── unified reveal: every chart waits to be scrolled into view; click replays.\n// fn charts manage their own timers via keep(); they're cleared on replay.\nconst timers={};\nconst obsReveal=(id,fn)=>{\n const el=document.getElementById(id);\n const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};\n const io=new IntersectionObserver(es=>{\n if(es[0].isIntersecting){go();io.disconnect()}\n },{threshold:.35});\n io.observe(el);\n el.style.cursor='pointer';\n el.addEventListener('click',go);\n};\nconst keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};\nconst eReveal=(id,opt)=>obsReveal(id,el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();g.setOption(opt);\n});\nconst cjs={};\nconst cReveal=(id,mkCfg)=>obsReveal(id,el=>{\n cjs[id]?.destroy();\n cjs[id]=new Chart(el,mkCfg());\n});\n\nChart.defaults.color=MUTED;\nChart.defaults.font.family='Inter';\nChart.defaults.font.size=10.5;\nChart.defaults.borderColor=GRID;\nChart.defaults.plugins.legend.display=false;\nChart.defaults.plugins.tooltip.backgroundColor=INK;\nChart.defaults.plugins.tooltip.titleColor=BG;\nChart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);\nChart.defaults.plugins.tooltip.padding=12;\nChart.defaults.plugins.tooltip.cornerRadius=12;\nChart.defaults.plugins.tooltip.displayColors=false;\n\n// ════ mono-demo · 3: chunky bars, values on top ════\n(()=>{\nconst V3=[182,486,391,274],rk3=rankOf(V3);\ncReveal('c3',()=>({\n type:'bar',\n data:{labels:['STARTER','PRO','TEAM','ENT'],\n datasets:[{data:V3,\n backgroundColor:V3.map((_,i)=>rampAt(rk3(i),4)),\n borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},\n options:{\n maintainAspectRatio:false,\n animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},\n plugins:{tooltip:{callbacks:{label:i=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+i.raw+'K MRR'}}},\n scales:{\n x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},\n y:{display:false,suggestedMax:540},\n },\n },\n plugins:[{\n id:'topValues',\n afterDatasetsDraw(chart){\n const {ctx}=chart;\n chart.getDatasetMeta(0).data.forEach((bar,i)=>{\n ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';\n ctx.fillText('

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();\n });\n },\n }],\n}));\n})();\n\n// ════ mono-redesign · donut → waffle of dots ════\n(()=>{\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];\nobsReveal('waffle',s=>{\n s.innerHTML='';\n const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;\n let idx=0;\n D.forEach(([name,v],g)=>{\n for(let k=0;k\u003cv;k++){\n const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;\n mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:g===0?hi(SER[0]):SER[g],\n class:'pop',style:`animation-delay:${c*.008+g*.05}s`});\n }\n idx+=v;\n });\n D.forEach(([name,v],g)=>{\n const y=26+g*40;\n mkEl(s,'circle',{cx:246,cy:y,r:6,fill:g===0?hi(SER[0]):SER[g]});\n const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});\n n.textContent=name;\n const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g\u003c2?TXT:MUT});\n val.textContent=v+'%';\n });\n});\n})();\n\n// ════ mono-fancy · 1: pictorial bar forest ════\n(()=>{\nconst 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';\nconst years=['2022','2023','2024','2025','2026'];\nconst vals=[26,41,63,88,117];\neReveal('forest',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},\n grid:{left:44,right:56,top:10,bottom:8},\n yAxis:{type:'category',data:years,inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},\n xAxis:{show:false,max:13},\n series:[{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:RAMP[i]}})),\n label:{show:true,position:'right',offset:[8,0],\n color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,\n formatter:p=>vals[p.dataIndex]+'k'},\n z:10,\n },{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n itemStyle:{color:QUIET},data:years.map(()=>13),silent:true,z:5,\n }],\n});\n})();\n\n// ════ mono-fancy · 2: circular graph on dark ════\n(()=>{\nconst teams=[\n ['Product',52],['Design',38],['Frontend',46],['Backend',44],\n ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],\n];\nconst links=[\n [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],\n [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],\n];\neReveal('circular',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:tipDark,\n series:[{\n type:'graph',layout:'circular',circular:{rotateLabel:false},\n left:52,right:52,top:34,bottom:34,\n data:teams.map(([n,v],i)=>({\n name:n,value:v,symbolSize:v*.62,\n itemStyle:{color:i===0?HERODK:i\u003c4?BG:i\u003c8?DMID:DQ,borderWidth:0},\n label:{show:true,position:'right',distance:7,color:DMID,fontFamily:'Inter',fontSize:9.5,fontWeight:600},\n })),\n links:links.map(([s,t,w])=>({\n source:s,target:t,\n lineStyle:{width:w*.7,color:DLINE,opacity:.55,curveness:.28},\n })),\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},\n }],\n});\n})();\n\n// ════ mono-fancy · 3: LR tree ════\n(()=>{\nconst b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},\n children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});\neReveal('tree',{\n animationDuration:1100,animationEasing:'quarticOut',\n tooltip:tipLight,\n series:[{\n type:'tree',layout:'orthogonal',orient:'LR',\n left:64,right:96,top:8,bottom:8,\n symbol:'circle',symbolSize:7,\n initialTreeDepth:2,expandAndCollapse:false,roam:false,\n itemStyle:{borderWidth:0},\n lineStyle:{width:1.4,curveness:.5},\n label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:LAB,position:'left'},\n leaves:{label:{position:'right',color:MUTED,fontWeight:500}},\n data:[{\n name:'Platform',itemStyle:{color:TXT},lineStyle:{color:FAINT},\n label:{fontSize:11.5,color:INK},\n children:[\n b('Editor',['Blocks','Tables','Comments','History'],hi(SER[0])),\n b('Automate',['Workflows','Triggers','Webhooks'],SER[1]),\n b('Collaborate',['Spaces','Guests','Mentions'],SER[2]),\n b('Integrate',['API','Slack','GitHub'],SER[3]),\n ],\n }],\n }],\n});\n})();\n\n// ════ mono-fancy · 4: rainfall dual area ════\n(()=>{\nconst days=Array.from({length:30},(_,i)=>i+1);\nconst 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];\nconst 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];\nconst RMAX=Math.max(...rain);\neReveal('rainfall',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipLight,trigger:'axis',\n formatter:p=>`Day ${p[0].axisValue} — spend ${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},\n axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:FAINT}},\n grid:[\n {left:44,right:14,top:8,height:'26%'},\n {left:44,right:14,top:'42%',bottom:26},\n ],\n xAxis:[\n {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},\n {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},\n ],\n yAxis:[\n {gridIndex:0,inverse:true,max:18,splitLine:{show:false},\n axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,formatter:'${value}K'},\n axisLine:{show:false},axisTick:{show:false}},\n {gridIndex:1,splitLine:{lineStyle:{color:GRID}},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},\n axisLine:{show:false},axisTick:{show:false}},\n ],\n series:[\n {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,\n data:rain.map(v=>({value:v,itemStyle:{color:v===RMAX?hi(DATA2):DATA2}})),\n itemStyle:{borderRadius:[0,0,4,4]},barWidth:'55%'},\n {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,\n smooth:.4,symbol:'none',lineStyle:{color:DATA,width:2.2},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.22)},{offset:1,color:alpha(DATA,0)}]}}},\n ],\n});\n})();\n\n// ════ mono-fancy · 5: universal transition morph ════\n(()=>{\nconst P12=[\n ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],\n ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],\n ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],\n ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],\n];\nconst RK12=[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]);\nconst shade=n=>rampAt(RK12.indexOf(n),12);\nconst axis={splitLine:{lineStyle:{color:GRID}},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};\nconst VIEWS=[\n {opt:{\n grid:{left:38,right:16,top:30,bottom:30},\n xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

,nameTextStyle:{color:FAINT,fontSize:8.5}},\n yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:FAINT,fontSize:8.5}},\n series:[{\n id:'p',type:'scatter',universalTransition:true,\n symbolSize:d=>Math.sqrt(d[3])*1.35,\n data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],\n groupId:d[0],itemStyle:{color:shade(d[0])}})),\n label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,\n formatter:p=>p.name},\n }],\n }},\n {opt:{\n grid:{left:38,right:16,top:30,bottom:44},\n xAxis:{...axis,type:'category',\n data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),\n axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},\n yAxis:{...axis,type:'value',max:520},\n series:[{\n id:'p',type:'bar',universalTransition:true,\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(d[0]),borderRadius:[6,6,0,0]}})),\n barCategoryGap:'32%',\n }],\n }},\n {opt:{\n grid:{left:0,right:0,top:0,bottom:0},\n xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},\n series:[{\n id:'p',type:'pie',universalTransition:true,\n radius:['26%','72%'],center:['50%','50%'],\n itemStyle:{borderColor:BG,borderWidth:2,borderRadius:6},\n label:{color:LAB,fontFamily:'Inter',fontSize:9,formatter:'{b}'},\n labelLine:{lineStyle:{color:FAINT}},\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(d[0])}})),\n }],\n }},\n];\nconst base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};\nobsReveal('morph',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let vi=0;\n g.setOption({...base,...VIEWS[0].opt});\n keep('morph',setInterval(()=>{\n vi=(vi+1)%VIEWS.length;\n g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});\n },3000));\n});\n})();\n\n// ════ mono-fancy2 · 2: diverging bar ════\n(()=>{\nconst D=[\n ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],\n ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],\n];\nconst MAXG=Math.max(...D.map(d=>d[1]));\neReveal('negbar',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},\n grid:{left:96,right:52,top:8,bottom:8},\n xAxis:{type:'value',\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',barWidth:16,\n data:D.map(([n,v])=>({name:n,value:v,\n itemStyle:{color:v>0?(v===MAXG?hi(DATA):DATA):DATA2,\n borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),\n label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,\n position:'outside',\n formatter:p=>(p.value>0?'+':'')+p.value,color:TXT},\n markLine:{symbol:'none',silent:true,label:{show:false},\n lineStyle:{color:MUT,width:1.5},\n data:[{xAxis:0}]},\n }],\n});\n})();\n\n// ════ force graph on dark: the network shakes itself into place ════\n// (replaced the axis-break bar — bars must never lie about length; house rule)\n(()=>{\nconst hubs=[['Core API',52,0]];\nconst apps=[\n ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],\n ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],\n ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],\n];\nconst nodes=[...hubs,...apps];\nconst links=[\n ...apps.map((a,i)=>({source:a[0],target:'Core API',\n lineStyle:{width:Math.max(.8,a[1]*.09),color:DLINE,opacity:.6}})),\n // a few app-to-app side roads\n {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},\n {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},\n {source:'Figma',target:'Notion',lineStyle:{width:1,color:DLINE2,opacity:.5}},\n {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:DLINE2,opacity:.5}},\n];\neReveal('force',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},\n roam:false,draggable:true,\n left:14,right:14,top:14,bottom:14,\n data:nodes.map(([n,v,tier])=>({\n name:n,value:v,symbolSize:8+v*.75,\n itemStyle:{color:tier===0?HERODK:tier===1?BG:DMID},\n label:{show:true,position:'right',distance:6,\n color:tier===0?BG:DMID,fontFamily:'Inter',\n fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},\n })),\n links,\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},\n }],\n});\n})();\n\n// ════ mono-fancy2 · 4: stagger wave ════\n(()=>{\nconst n=50;\nconst vals=Array.from({length:n},(_,i)=>\n Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));\nconst rkW=rankOf(vals);\neReveal('wave',{\n tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},\n grid:{left:36,right:10,top:14,bottom:24},\n xAxis:{type:'category',data:vals.map((_,i)=>i+1),\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},\n yAxis:{type:'value',\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},\n series:[{\n type:'bar',barCategoryGap:'26%',\n data:vals.map((v,i)=>({value:v,\n itemStyle:{color:rampAt(rkW(i),vals.length),borderRadius:[4,4,0,0]}})),\n animationDelay:i=>i*36,\n animationDuration:640,animationEasing:'elasticOut',\n }],\n animationDurationUpdate:640,\n animationDelayUpdate:i=>i*36,\n});\n})();\n\n// ════ mono-fancy3 · 1: custom double-encoded pie ════\n(()=>{\nconst D=[\n ['Editor',34,42],['Boards',22,31],['Docs',18,38],\n ['Chat',12,11],['Automations',8,24],['Other',6,7],\n];\nconst SCALE=45;\nconst TAU=Math.PI*2;\nconst byShare=[...D].sort((a,b)=>b[1]-a[1]);\nconst minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);\nlet acc=0;\nconst slices=byShare.map(([n,share,mins])=>{\n const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;\n acc+=share;\n return {n,share,mins,p0,p1,color:rampAt(minsDesc.indexOf(mins),6)};\n});\neReveal('custompie',{\n animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,\n tooltip:{...tipLight,formatter:p=>{\n const s=slices[p.dataIndex];\n return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';\n }},\n xAxis:{show:false,min:-1,max:1},\n yAxis:{show:false,min:-1,max:1},\n grid:{left:0,right:0,top:0,bottom:0},\n series:[{\n type:'custom',\n renderItem:(params,api)=>{\n const s=slices[params.dataIndex];\n const cx=api.getWidth()/2, cy=api.getHeight()/2+6;\n const R=Math.min(api.getWidth(),api.getHeight())*.37;\n const r0=R*.24;\n const rOf=m=>r0+(R-r0)*m/SCALE;\n const pm=(s.p0+s.p1)/2, lr=R+14;\n const cos=Math.cos(pm);\n const kids=[];\n if(params.dataIndex===0){\n [15,30,45].forEach(m=>kids.push({type:'circle',\n shape:{cx,cy,r:rOf(m)},\n style:{fill:'none',stroke:GRID,lineWidth:1,lineDash:[3,4]}}));\n }\n kids.push({type:'sector',\n shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},\n style:{fill:s.color,stroke:BG,lineWidth:3},\n enterFrom:{shape:{r:r0}}});\n kids.push({type:'text',style:{\n x:cx+cos*lr, y:cy+Math.sin(pm)*lr,\n text:s.n+' '+s.share+'% · '+s.mins+'m',\n font:'600 10px Inter',fill:LAB,\n align:Math.abs(cos)\u003c.25?'center':(cos>0?'left':'right'),\n verticalAlign:'middle'},\n enterFrom:{style:{opacity:0}}});\n return {type:'group',children:kids};\n },\n data:slices.map(s=>s.share),\n }],\n});\n})();\n\n// ════ mono-fancy3 · 3: single-axis small multiples ════\n(()=>{\nconst days=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nconst hours=Array.from({length:24},(_,i)=>i);\nconst mkRow=di=>hours.map(h=>{\n const workday=di\u003c5, core=h>=9&&h\u003c=17;\n 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\u003c=5?0:1));\n return [h,v];\n}).filter(d=>d[1]>0);\neReveal('singleaxis',{\n animationDuration:500,animationEasing:'backOut',\n tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},\n title:days.map((d,i)=>({\n text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,\n textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},\n })),\n singleAxis:days.map((d,i)=>({\n left:52,right:14,type:'category',boundaryGap:false,\n data:hours.map(h=>String(h).padStart(2,'0')),\n top:i*100/7+6+'%',height:100/7-8+'%',\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},\n splitLine:{show:false},\n })),\n series:days.map((d,i)=>({\n singleAxisIndex:i,coordinateSystem:'singleAxis',\n type:'scatter',\n data:mkRow(i),\n symbolSize:dd=>dd[1]*2.6,\n itemStyle:{color:i\u003c5?(i===0?hi(SER[0]):SER[i]):FAINTDATA},\n animationDelay:di=>i*160+di*14,\n })),\n});\n})();\n\n// ════ mono-fancy3 · 4: jitter strip ════\n(()=>{\nconst cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];\nconst mkPts=(ci,n,median,spread,outliers)=>{\n const pts=[];\n for(let i=0;i\u003cn;i++){\n const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);\n let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);\n if(i\u003coutliers) x=median+spread*2.2+u*spread*3;\n const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;\n pts.push([Math.max(.2,x),jy]);\n }\n return pts;\n};\nconst groups=[\n mkPts(0,38,.8,.5,2),\n mkPts(1,64,2.4,1.1,3),\n mkPts(2,110,6.5,2.4,4),\n mkPts(3,72,14,4.5,3),\n];\nconst flat=groups.flat();\neReveal('jitter',{\n animationDuration:450,animationEasing:'cubicOut',\n animationDelay:i=>i\u003cflat.length?Math.round(flat[i][1])*260+(i%37)*9:0,\n tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},\n grid:{left:86,right:16,top:14,bottom:30},\n xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:FAINT,fontSize:8.5},\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},\n yAxis:{type:'value',min:-.6,max:3.6,inverse:true,\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:false}},\n series:[{\n type:'scatter',\n data:flat,\n symbolSize:7,\n itemStyle:{color:p=>RAMP[4-Math.round(p.value[1])],opacity:.62},\n },{\n type:'scatter',silent:true,symbolSize:0,\n data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],\n color:LAB,fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),\n }],\n});\n})();\n\n// ════ mono-fancy4 · 1: bar race ════\n(()=>{\nconst P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];\nconst YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];\nconst vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];\nfor(let y=1;y\u003cYEARS.length;y++)\n vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));\nobsReveal('race',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let step=0;\n const frame=()=>{\n const row=vals[step];\n const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);\n g.setOption({\n animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:v=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v)+'K'},\n grid:{left:64,right:64,top:8,bottom:8},\n xAxis:{type:'value',max:'dataMax',\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:P,inverse:true,max:7,\n animationDuration:300,animationDurationUpdate:300,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',realtimeSort:true,barCategoryGap:'32%',\n data:row.map((v,i)=>({value:v,\n itemStyle:{color:rampAt(rank.indexOf(i),8),borderRadius:99}})),\n label:{show:true,position:'right',valueAnimation:true,\n fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,\n formatter:p=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(p.value)+'K'},\n }],\n graphic:[{id:'yr',type:'text',right:18,bottom:14,\n style:{text:YEARS[step],font:'800 44px Inter',fill:GRID}}],\n });\n step=(step+1)%YEARS.length;\n };\n frame();\n keep('race',setInterval(frame,1150));\n});\n})();\n\n// ════ mono-fancy4 · 2: dynamic data stream ════\n(()=>{\nconst WIN=50;\nobsReveal('stream',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let t=0,v=64;\n const data=[];\n for(;t\u003cWIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}\n const draw=()=>g.setOption({\n animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:x=>x+'k users'},\n grid:{left:14,right:58,top:44,bottom:20},\n xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},\n yAxis:{show:false,min:20,max:130},\n series:[{\n type:'line',data:[...data],smooth:.45,symbol:'none',\n lineStyle:{color:DATA,width:2.4},\n endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:TXT,\n formatter:p=>p.value+'k'},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.16)},{offset:1,color:alpha(DATA,0)}]}},\n }],\n graphic:[{type:'group',left:16,top:10,children:[\n {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:HERO}},\n {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:TXT}},\n ]}],\n });\n draw();\n keep('stream',setInterval(()=>{\n v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));\n data.push(Math.round(v));data.shift();t++;\n draw();\n },300));\n});\n})();\n\n// ════ mono-fancy4 · 3: draw-in + counter ════\n(()=>{\nconst days=Array.from({length:180},(_,i)=>i);\nconst daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);\nconst cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);\nconst TOTAL=cum[cum.length-1];\nconst DUR=2600;\nlet gen=0; // replay guard: only the latest counter loop may write\nobsReveal('stroke',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n gen++;const my=gen;\n g.setOption({\n animationDuration:DUR,animationEasing:'cubicOut',\n tooltip:{show:false},\n grid:{left:14,right:20,top:64,bottom:24},\n xAxis:{type:'category',data:days,boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},\n axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,fontWeight:600,\n interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},\n yAxis:{show:false,max:TOTAL*1.06},\n series:[{\n type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',\n lineStyle:{color:DATA,width:2.6},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.14)},{offset:1,color:alpha(DATA,0)}]}},\n }],\n });\n const t0=performance.now();\n const tick=()=>{\n if(my!==gen)return;\n const p=Math.min(1,(performance.now()-t0)/DUR);\n const eased=1-Math.pow(1-p,3);\n const val=(TOTAL*eased/1000).toFixed(2);\n g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[\n {type:'text',style:{text:'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+val+'M',font:'800 32px Inter',fill:hi(TXT)}},\n {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:MUT}},\n ]}]});\n if(p\u003c1)requestAnimationFrame(tick);\n };\n requestAnimationFrame(tick);\n});\n})();\n\naddEventListener('resize',()=>{\n ['forest','circular','tree','rainfall','morph','negbar','force','wave',\n 'custompie','singleaxis','jitter','race','stream','stroke']\n .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ G19 · violin ════\n// 小提琴:提前聚合的密度剪影,Glance 世界观。\n// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),\n// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。\n(()=>{\nconst G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];\nconst M=64;\nconst samples=g=>Array.from({length:M},(_,i)=>{\n const [,c,s]=G[g];\n const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);\n return Math.max(.2,v);\n});\nobsReveal('violin',s=>{\n const TOP=40,BASE=262,HMAX=17;\n const mapY=h=>BASE-h/HMAX*(BASE-TOP);\n const colX=g=>76+g*84;\n // hour grid\n [0,4,8,12,16].forEach(h=>{\n el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(88,64,46,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${h*.01}s`});\n txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.01}s`},h+'h');\n });\n const SHADE=[INK,'#77835A','#929960','#ACAD79'];\n G.forEach(([name],g)=>{\n const vs=samples(g).sort((a,b)=>a-b);\n const med=vs[Math.floor(M/2)];\n // fuller bandwidth = the classic rounded violin belly, not a spindle\n const bw=Math.max(.9,G[g][2]*.72);\n // pad past the data by 1.6 bandwidths so the density decays to ~0 and the\n // ends taper naturally — clipping tighter gives flat-cut lantern ends\n const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);\n const NPTS=44;\n const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));\n const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));\n const dmax=Math.max(...dens);\n const W=30;\n const x=colX(g);\n // smooth closed outline through the mirrored density — quadratic\n // midpoint smoothing, same trick as almanac's blob\n const pts=[];\n grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));\n for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let t=0;t\u003cpts.length;t++){\n const p=pts[t],q=pts[(t+1)%pts.length];\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)}`;\n }\n const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});\n tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);\n // median: paper tick across the body, value parked right\n const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));\n const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;\n el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),\n stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});\n txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');\n});\n})();\n\n// ════ G20 · matrix heat ════\n// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出\n// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。\n(()=>{\nconst FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];\nconst VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];\nconst BASEV=[88,74,61,42,35,27];\nconst v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));\nconst shade=t=>t>64?INK:t>46?'#77835A':t>30?'#929960':t>15?'#ACAD79':'#F2D17E';\nobsReveal('gheat',s=>{\n const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;\n FEAT.forEach((f,c)=>{\n txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);\n });\n VER.forEach((ver,r)=>{\n txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'rgba(88,64,46,.72)','text-anchor':'end',\n class:'fade',style:`animation-delay:${r*.05}s`},ver);\n FEAT.forEach((f,c)=>{\n const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;\n const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),\n class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});\n tip(cell,`${f} on ${ver} — ${t}% of accounts`);\n 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',\n class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);\n });\n });\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');\n});\n})();\n\n// ════ G21 · rank strip ════\n// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,\n// 和 slope 同一种丑(catalog 实测三连败那条),废弃。\n// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。\n// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。\n(()=>{\nconst RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],\n ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];\nconst SHADE=[INK,'#77835A','#929960','#ACAD79','#F2D17E']; // rank 1 → 5\nobsReveal('rankstrip',s=>{\n // rows sorted by final rank, the climber lands on top\n const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);\n const X0=110,PX=38,Y0=64,PY=42,C=32;\n ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>\n txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));\n rows.forEach(([p,rk],i)=>{\n const y=Y0+i*PY,hero=p==='Flows';\n txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'rgba(88,64,46,.72)','text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.07}s`},p);\n rk.forEach((r,q)=>{\n const dark=r\u003c=2;\n const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],\n stroke:'rgba(88,64,46,.24)','stroke-width':0.9,\n class:'pop',style:`animation-delay:${i*.07+q*.035}s`});\n tip(cell,`${p} — #${r} in Q${q+1}`);\n txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,\n fill:dark?PAPER:'#58402E','text-anchor':'middle',\n class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);\n });\n // arrival flag: climbed / fell / held, told at the row's end\n const d=rk[0]-rk[5];\n txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,\n fill:d>0?INK:d\u003c0?'#ACAD79':'rgba(88,64,46,.32)',\n class:'fade',style:`animation-delay:${.5+i*.07}s`},\n d>0?'▲'+d:d\u003c0?'▼'+(-d):'—');\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');\n});\n})();\n\n// ════ G22 · aggregate sankey ════\n// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——\n// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。\n// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。\n(()=>{\nconst SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];\nconst DST=['FREE','PRO','TEAM'];\n// 渠道是类目:一渠道一色,走 lupi-palm 的 SER 分类位(含琥珀、麦黄)\nconst SH=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79'];\nobsReveal('sankey',s=>{\n const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;\n const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));\n const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));\n const sy=[],dy=[];\n let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});\n y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});\n const syc=[...sy],dyc=[...dy];\n SRC.forEach((d,i)=>{\n d[1].forEach((v,j)=>{\n if(!v)return;\n const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;\n syc[i]=sb;dyc[j]=db;\n 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`,\n fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});\n tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);\n });\n });\n SRC.forEach((d,i)=>{\n el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],\n class:'fade',style:`animation-delay:${i*.06}s`});\n txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#77835A','text-anchor':'end',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);\n });\n DST.forEach((n,j)=>{\n el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#43593B',\n class:'fade',style:`animation-delay:${.1+j*.06}s`});\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#77835A',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);\n });\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'RIBBON WIDTH = ACCOUNTS OF 100 · COLOR = CHANNEL');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":49563,"content_sha256":"6090a6b954deb6787e7bf762c445149664005fe79faba1ad72842ceb369e0fd9"},{"filename":"templates/color/glance-porcelain.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Glance 20 × PORCELAIN\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/chart.js@4\">\u003c/script>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F7F2EB;--dark:#081F5C;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.40);--grid:rgba(8,31,92,.16);--lab:rgba(8,31,92,.72);--dmid:rgba(247,242,235,.62);--dq:rgba(247,242,235,.24)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.dark{background:var(--dark);color:var(--bg)}\n .card.dark .sub{color:var(--dmid)}\n .card.dark .src{color:var(--dq)}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .ch{height:320px}\n .wrap{position:relative;height:320px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .pagehead .badge{margin:0 2px}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px}\n .badge.del{background:var(--ink);color:var(--bg)}\n .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}\n .badge.pair{border:1px dashed var(--faint);color:var(--muted)}\n .card.dark .badge.del{background:var(--bg);color:var(--ink)}\n .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}\n .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Glance 系(快读)· 20 张 × PORCELAIN\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#334EAC\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#7096D1\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#BAD6EB\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#D0E3FF\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#081F5C\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#F7F2EB\">\u003c/span>  青瓷蓝家族 · Dawn → Sky → China → Royal → Midnight 五档全是原厂色号,零补档。Moon 当底,Midnight 当文字兼主角。\u003c/p>\n \u003cp style=\"margin-top:7px\">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 个来源 / 4 条产品线 / 5 个工作日 / 10 支小队 / 12 个集成)各走一个分类位,序数型(8 年营收排名 / 50 个市场 / 4 档优先级 / 树木年份 / 面板停留时长 / 情绪票数)走 5 档梯,深色卡换纸色梯、末档落在强调色。几何、动画、数据与 glance-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003c!-- ══ mono-demo ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Revenue by plan\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · MRR, $K\u003c/div>\n \u003cdiv class=\"wrap\">\u003ccanvas id=\"c3\">\u003c/canvas>\u003c/div>\n \u003cdiv class=\"src\">CHUNKY BARS · MONO-DEMO · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-redesign · dot waffle ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where sign-ups come from\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · every dot = 1% of new accounts\u003c/div>\n \u003csvg id=\"waffle\" viewBox=\"0 0 380 300\" width=\"100%\" style=\"max-height:320px\">\u003c/svg>\n \u003cdiv class=\"src\">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Trees planted, year by year\u003c/h2>\n \u003cdiv class=\"sub\">reforestation program · thousands of trees\u003c/div>\n \u003cdiv class=\"ch\" id=\"forest\">\u003c/div>\n \u003cdiv class=\"src\">PICTORIAL BAR · MONO-FANCY · GREEN OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Who works with whom\u003c/h2>\n \u003cdiv class=\"sub\">cross-team collaborations · Q2 2026 · line = shared project\u003c/div>\n \u003cdiv class=\"ch\" id=\"circular\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Everything the platform ships\u003c/h2>\n \u003cdiv class=\"sub\">product areas and capabilities · Jun 2026\u003c/div>\n \u003cdiv class=\"ch\" id=\"tree\">\u003c/div>\n \u003cdiv class=\"src\">TREE · MONO-FANCY · PRODUCT CATALOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Campaigns rain down, sign-ups flow\u003c/h2>\n \u003cdiv class=\"sub\">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days\u003c/div>\n \u003cdiv class=\"ch\" id=\"rainfall\">\u003c/div>\n \u003cdiv class=\"src\">DUAL AREA · MONO-FANCY · GROWTH + ADS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>One dataset, three views\u003c/h2>\n \u003cdiv class=\"sub\">watch the same 12 products fly between views · every 3s\u003c/div>\n \u003cdiv class=\"ch\" id=\"morph\">\u003c/div>\n \u003cdiv class=\"src\">SCATTER MORPH · MONO-FANCY · COHORTS\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy2 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where we gained, where we bled\u003c/h2>\n \u003cdiv class=\"sub\">net accounts per segment · Q2 2026 · churn left, growth right\u003c/div>\n \u003cdiv class=\"ch\" id=\"negbar\">\u003c/div>\n \u003cdiv class=\"src\">DIVERGING BAR · MONO-FANCY2 · CRM\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Integrations, pulled into orbit\u003c/h2>\n \u003cdiv class=\"sub\">force layout · node = monthly syncs · drag a node, the web fights back\u003c/div>\n \u003cdiv class=\"ch\" id=\"force\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · NEW · INTEGRATION HUB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Fifty markets, one wave\u003c/h2>\n \u003cdiv class=\"sub\">sign-ups per market · bars cascade in · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"wave\">\u003c/div>\n \u003cdiv class=\"src\">STAGGER DELAY · MONO-FANCY2 · GROWTH\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy3 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Big slice, deep engagement\u003c/h2>\n \u003cdiv class=\"sub\">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min\u003c/div>\n \u003cdiv class=\"ch\" id=\"custompie\">\u003c/div>\n \u003cdiv class=\"src\">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Support load, day by day\u003c/h2>\n \u003cdiv class=\"sub\">tickets per hour · each row is its own axis · dot = volume\u003c/div>\n \u003cdiv class=\"ch\" id=\"singleaxis\">\u003c/div>\n \u003cdiv class=\"src\">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Response times, spread out\u003c/h2>\n \u003cdiv class=\"sub\">every dot = one ticket · jitter reveals the pile-up · by priority\u003c/div>\n \u003cdiv class=\"ch\" id=\"jitter\">\u003c/div>\n \u003cdiv class=\"src\">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy4 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Eight products race for revenue\u003c/h2>\n \u003cdiv class=\"sub\">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"race\">\u003c/div>\n \u003cdiv class=\"src\">BAR RACE · MONO-FANCY4 · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Concurrent users, streaming\u003c/h2>\n \u003cdiv class=\"sub\">one new reading every 300ms · window of 50 · click to restart\u003c/div>\n \u003cdiv class=\"ch\" id=\"stream\">\u003c/div>\n \u003cdiv class=\"src\">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>H1 revenue, drawn in one stroke\u003c/h2>\n \u003cdiv class=\"sub\">cumulative ARR · the line draws, the number counts · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"stroke\">\u003c/div>\n \u003cdiv class=\"src\">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE\u003c/div>\n \u003c/div>\n\n\n \u003c!-- G19 · violin -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 分布型 · 小提琴\u003c/span>\n \u003ch2>How fast each plan gets an answer\u003c/h2>\n \u003cdiv class=\"sub\">hours to first reply · silhouette = where tickets pile up · paper tick = median\u003c/div>\n \u003csvg id=\"violin\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">VIOLIN · PORCELAIN · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- G20 · matrix heat -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 热力型 · 通用矩阵\u003c/span>\n \u003ch2>Adoption runs hot on the new versions\u003c/h2>\n \u003cdiv class=\"sub\">5 versions × 6 features · shade = % of accounts using it · every cell carries its number\u003c/div>\n \u003csvg id=\"gheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">MATRIX HEAT (GLANCE) · PORCELAIN · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- G21 · rank strip -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 排名型 · RANK STRIP\u003c/span>\n \u003ch2>Flows climbs to the top\u003c/h2>\n \u003cdiv class=\"sub\">one row per product, one cell per quarter · darker = higher rank · a bar race you can print\u003c/div>\n \u003csvg id=\"rankstrip\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RANK STRIP · PORCELAIN · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- G22 · aggregate sankey -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 流向型 · 聚合桑基\u003c/span>\n \u003ch2>Channels pour into plans\u003c/h2>\n \u003cdiv class=\"sub\">ribbon width = accounts · channels on the left, the plans they land in on the right\u003c/div>\n \u003csvg id=\"sankey\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">AGGREGATE SANKEY · PORCELAIN · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── PORCELAIN 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F7F2EB',DARK='#081F5C',TXT='#081F5C';\nconst MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.40)',QUIET='rgba(8,31,92,.15)',GRID='rgba(8,31,92,.16)';\nconst DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C';\nconst HERODK='#7096D1'; // 深色卡上的强调色(HERO 在深底上可能读不出)\nconst FAINTDATA='#BAD6EB'; // 静音数据档:周末 / 非主序列\nconst DMID='rgba(247,242,235,.62)',DQ='rgba(247,242,235,.24)'; // 深色卡上的浅墨 / 静音档\nconst SER=['#334EAC','#081F5C','#7096D1','#BAD6EB','#D0E3FF','rgba(51,78,172,.4)']; // 分类位:一图之内每个系列 / 每类 / 每行一个色\nconst RAMP=['#D0E3FF','#BAD6EB','#7096D1','#334EAC','#081F5C']; // 5 档序数梯(浅→重):排名 / 票量 / 峰值 / 停留时长\nconst DRAMP4=['#D0E3FF','#BAD6EB','#7096D1','rgba(247,242,235,.26)']; // 深色卡上的 4 档序数梯(浅→深)\nconst HEROMODE=false; // 主角模式:分类位的头号项也整体走 HERO\n\n// ── 别名与派生:深色卡的线全是卡底向纸色的插值,不引入新色相 ──\nconst INK=TXT,PAPER=BG,MUTED=MUT;\nconst rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));\nconst alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;\nconst mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');\nconst lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v\u003c=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));\n return .2126*r+.7152*g+.0722*b};\nconst ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};\nconst readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG); // 深色卡上落在色块里的字\nconst DLINE=mix(DARK,BG,.20),DLINE2=mix(DARK,BG,.13),DSHELL=mix(DARK,BG,.09);\nconst hi=c=>HEROMODE?HERO:c;\n// 名次 → 序数梯。末档(WIRE 的红)只发给第 1 名,其余摊在前四档上\nconst rampAt=(i,n)=>i===0?RAMP[4]\n :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];\nconst rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);\n return i=>r.indexOf(i)};\nconst tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── unified reveal: every chart waits to be scrolled into view; click replays.\n// fn charts manage their own timers via keep(); they're cleared on replay.\nconst timers={};\nconst obsReveal=(id,fn)=>{\n const el=document.getElementById(id);\n const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};\n const io=new IntersectionObserver(es=>{\n if(es[0].isIntersecting){go();io.disconnect()}\n },{threshold:.35});\n io.observe(el);\n el.style.cursor='pointer';\n el.addEventListener('click',go);\n};\nconst keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};\nconst eReveal=(id,opt)=>obsReveal(id,el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();g.setOption(opt);\n});\nconst cjs={};\nconst cReveal=(id,mkCfg)=>obsReveal(id,el=>{\n cjs[id]?.destroy();\n cjs[id]=new Chart(el,mkCfg());\n});\n\nChart.defaults.color=MUTED;\nChart.defaults.font.family='Inter';\nChart.defaults.font.size=10.5;\nChart.defaults.borderColor=GRID;\nChart.defaults.plugins.legend.display=false;\nChart.defaults.plugins.tooltip.backgroundColor=INK;\nChart.defaults.plugins.tooltip.titleColor=BG;\nChart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);\nChart.defaults.plugins.tooltip.padding=12;\nChart.defaults.plugins.tooltip.cornerRadius=12;\nChart.defaults.plugins.tooltip.displayColors=false;\n\n// ════ mono-demo · 3: chunky bars, values on top ════\n(()=>{\nconst V3=[182,486,391,274],rk3=rankOf(V3);\ncReveal('c3',()=>({\n type:'bar',\n data:{labels:['STARTER','PRO','TEAM','ENT'],\n datasets:[{data:V3,\n backgroundColor:V3.map((_,i)=>rampAt(rk3(i),4)),\n borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},\n options:{\n maintainAspectRatio:false,\n animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},\n plugins:{tooltip:{callbacks:{label:i=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+i.raw+'K MRR'}}},\n scales:{\n x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},\n y:{display:false,suggestedMax:540},\n },\n },\n plugins:[{\n id:'topValues',\n afterDatasetsDraw(chart){\n const {ctx}=chart;\n chart.getDatasetMeta(0).data.forEach((bar,i)=>{\n ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';\n ctx.fillText('

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();\n });\n },\n }],\n}));\n})();\n\n// ════ mono-redesign · donut → waffle of dots ════\n(()=>{\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];\nobsReveal('waffle',s=>{\n s.innerHTML='';\n const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;\n let idx=0;\n D.forEach(([name,v],g)=>{\n for(let k=0;k\u003cv;k++){\n const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;\n mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:g===0?hi(SER[0]):SER[g],\n class:'pop',style:`animation-delay:${c*.008+g*.05}s`});\n }\n idx+=v;\n });\n D.forEach(([name,v],g)=>{\n const y=26+g*40;\n mkEl(s,'circle',{cx:246,cy:y,r:6,fill:g===0?hi(SER[0]):SER[g]});\n const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});\n n.textContent=name;\n const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g\u003c2?TXT:MUT});\n val.textContent=v+'%';\n });\n});\n})();\n\n// ════ mono-fancy · 1: pictorial bar forest ════\n(()=>{\nconst 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';\nconst years=['2022','2023','2024','2025','2026'];\nconst vals=[26,41,63,88,117];\neReveal('forest',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},\n grid:{left:44,right:56,top:10,bottom:8},\n yAxis:{type:'category',data:years,inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},\n xAxis:{show:false,max:13},\n series:[{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:RAMP[i]}})),\n label:{show:true,position:'right',offset:[8,0],\n color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,\n formatter:p=>vals[p.dataIndex]+'k'},\n z:10,\n },{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n itemStyle:{color:QUIET},data:years.map(()=>13),silent:true,z:5,\n }],\n});\n})();\n\n// ════ mono-fancy · 2: circular graph on dark ════\n(()=>{\nconst teams=[\n ['Product',52],['Design',38],['Frontend',46],['Backend',44],\n ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],\n];\nconst links=[\n [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],\n [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],\n];\neReveal('circular',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:tipDark,\n series:[{\n type:'graph',layout:'circular',circular:{rotateLabel:false},\n left:52,right:52,top:34,bottom:34,\n data:teams.map(([n,v],i)=>({\n name:n,value:v,symbolSize:v*.62,\n itemStyle:{color:i===0?HERODK:i\u003c4?BG:i\u003c8?DMID:DQ,borderWidth:0},\n label:{show:true,position:'right',distance:7,color:DMID,fontFamily:'Inter',fontSize:9.5,fontWeight:600},\n })),\n links:links.map(([s,t,w])=>({\n source:s,target:t,\n lineStyle:{width:w*.7,color:DLINE,opacity:.55,curveness:.28},\n })),\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},\n }],\n});\n})();\n\n// ════ mono-fancy · 3: LR tree ════\n(()=>{\nconst b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},\n children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});\neReveal('tree',{\n animationDuration:1100,animationEasing:'quarticOut',\n tooltip:tipLight,\n series:[{\n type:'tree',layout:'orthogonal',orient:'LR',\n left:64,right:96,top:8,bottom:8,\n symbol:'circle',symbolSize:7,\n initialTreeDepth:2,expandAndCollapse:false,roam:false,\n itemStyle:{borderWidth:0},\n lineStyle:{width:1.4,curveness:.5},\n label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:LAB,position:'left'},\n leaves:{label:{position:'right',color:MUTED,fontWeight:500}},\n data:[{\n name:'Platform',itemStyle:{color:TXT},lineStyle:{color:FAINT},\n label:{fontSize:11.5,color:INK},\n children:[\n b('Editor',['Blocks','Tables','Comments','History'],hi(SER[0])),\n b('Automate',['Workflows','Triggers','Webhooks'],SER[1]),\n b('Collaborate',['Spaces','Guests','Mentions'],SER[2]),\n b('Integrate',['API','Slack','GitHub'],SER[3]),\n ],\n }],\n }],\n});\n})();\n\n// ════ mono-fancy · 4: rainfall dual area ════\n(()=>{\nconst days=Array.from({length:30},(_,i)=>i+1);\nconst 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];\nconst 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];\nconst RMAX=Math.max(...rain);\neReveal('rainfall',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipLight,trigger:'axis',\n formatter:p=>`Day ${p[0].axisValue} — spend ${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},\n axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:FAINT}},\n grid:[\n {left:44,right:14,top:8,height:'26%'},\n {left:44,right:14,top:'42%',bottom:26},\n ],\n xAxis:[\n {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},\n {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},\n ],\n yAxis:[\n {gridIndex:0,inverse:true,max:18,splitLine:{show:false},\n axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,formatter:'${value}K'},\n axisLine:{show:false},axisTick:{show:false}},\n {gridIndex:1,splitLine:{lineStyle:{color:GRID}},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},\n axisLine:{show:false},axisTick:{show:false}},\n ],\n series:[\n {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,\n data:rain.map(v=>({value:v,itemStyle:{color:v===RMAX?hi(DATA2):DATA2}})),\n itemStyle:{borderRadius:[0,0,4,4]},barWidth:'55%'},\n {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,\n smooth:.4,symbol:'none',lineStyle:{color:DATA,width:2.2},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.22)},{offset:1,color:alpha(DATA,0)}]}}},\n ],\n});\n})();\n\n// ════ mono-fancy · 5: universal transition morph ════\n(()=>{\nconst P12=[\n ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],\n ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],\n ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],\n ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],\n];\nconst RK12=[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]);\nconst shade=n=>rampAt(RK12.indexOf(n),12);\nconst axis={splitLine:{lineStyle:{color:GRID}},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};\nconst VIEWS=[\n {opt:{\n grid:{left:38,right:16,top:30,bottom:30},\n xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

,nameTextStyle:{color:FAINT,fontSize:8.5}},\n yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:FAINT,fontSize:8.5}},\n series:[{\n id:'p',type:'scatter',universalTransition:true,\n symbolSize:d=>Math.sqrt(d[3])*1.35,\n data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],\n groupId:d[0],itemStyle:{color:shade(d[0])}})),\n label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,\n formatter:p=>p.name},\n }],\n }},\n {opt:{\n grid:{left:38,right:16,top:30,bottom:44},\n xAxis:{...axis,type:'category',\n data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),\n axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},\n yAxis:{...axis,type:'value',max:520},\n series:[{\n id:'p',type:'bar',universalTransition:true,\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(d[0]),borderRadius:[6,6,0,0]}})),\n barCategoryGap:'32%',\n }],\n }},\n {opt:{\n grid:{left:0,right:0,top:0,bottom:0},\n xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},\n series:[{\n id:'p',type:'pie',universalTransition:true,\n radius:['26%','72%'],center:['50%','50%'],\n itemStyle:{borderColor:BG,borderWidth:2,borderRadius:6},\n label:{color:LAB,fontFamily:'Inter',fontSize:9,formatter:'{b}'},\n labelLine:{lineStyle:{color:FAINT}},\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(d[0])}})),\n }],\n }},\n];\nconst base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};\nobsReveal('morph',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let vi=0;\n g.setOption({...base,...VIEWS[0].opt});\n keep('morph',setInterval(()=>{\n vi=(vi+1)%VIEWS.length;\n g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});\n },3000));\n});\n})();\n\n// ════ mono-fancy2 · 2: diverging bar ════\n(()=>{\nconst D=[\n ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],\n ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],\n];\nconst MAXG=Math.max(...D.map(d=>d[1]));\neReveal('negbar',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},\n grid:{left:96,right:52,top:8,bottom:8},\n xAxis:{type:'value',\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',barWidth:16,\n data:D.map(([n,v])=>({name:n,value:v,\n itemStyle:{color:v>0?(v===MAXG?hi(DATA):DATA):DATA2,\n borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),\n label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,\n position:'outside',\n formatter:p=>(p.value>0?'+':'')+p.value,color:TXT},\n markLine:{symbol:'none',silent:true,label:{show:false},\n lineStyle:{color:MUT,width:1.5},\n data:[{xAxis:0}]},\n }],\n});\n})();\n\n// ════ force graph on dark: the network shakes itself into place ════\n// (replaced the axis-break bar — bars must never lie about length; house rule)\n(()=>{\nconst hubs=[['Core API',52,0]];\nconst apps=[\n ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],\n ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],\n ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],\n];\nconst nodes=[...hubs,...apps];\nconst links=[\n ...apps.map((a,i)=>({source:a[0],target:'Core API',\n lineStyle:{width:Math.max(.8,a[1]*.09),color:DLINE,opacity:.6}})),\n // a few app-to-app side roads\n {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},\n {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},\n {source:'Figma',target:'Notion',lineStyle:{width:1,color:DLINE2,opacity:.5}},\n {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:DLINE2,opacity:.5}},\n];\neReveal('force',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},\n roam:false,draggable:true,\n left:14,right:14,top:14,bottom:14,\n data:nodes.map(([n,v,tier])=>({\n name:n,value:v,symbolSize:8+v*.75,\n itemStyle:{color:tier===0?HERODK:tier===1?BG:DMID},\n label:{show:true,position:'right',distance:6,\n color:tier===0?BG:DMID,fontFamily:'Inter',\n fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},\n })),\n links,\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},\n }],\n});\n})();\n\n// ════ mono-fancy2 · 4: stagger wave ════\n(()=>{\nconst n=50;\nconst vals=Array.from({length:n},(_,i)=>\n Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));\nconst rkW=rankOf(vals);\neReveal('wave',{\n tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},\n grid:{left:36,right:10,top:14,bottom:24},\n xAxis:{type:'category',data:vals.map((_,i)=>i+1),\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},\n yAxis:{type:'value',\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},\n series:[{\n type:'bar',barCategoryGap:'26%',\n data:vals.map((v,i)=>({value:v,\n itemStyle:{color:rampAt(rkW(i),vals.length),borderRadius:[4,4,0,0]}})),\n animationDelay:i=>i*36,\n animationDuration:640,animationEasing:'elasticOut',\n }],\n animationDurationUpdate:640,\n animationDelayUpdate:i=>i*36,\n});\n})();\n\n// ════ mono-fancy3 · 1: custom double-encoded pie ════\n(()=>{\nconst D=[\n ['Editor',34,42],['Boards',22,31],['Docs',18,38],\n ['Chat',12,11],['Automations',8,24],['Other',6,7],\n];\nconst SCALE=45;\nconst TAU=Math.PI*2;\nconst byShare=[...D].sort((a,b)=>b[1]-a[1]);\nconst minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);\nlet acc=0;\nconst slices=byShare.map(([n,share,mins])=>{\n const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;\n acc+=share;\n return {n,share,mins,p0,p1,color:rampAt(minsDesc.indexOf(mins),6)};\n});\neReveal('custompie',{\n animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,\n tooltip:{...tipLight,formatter:p=>{\n const s=slices[p.dataIndex];\n return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';\n }},\n xAxis:{show:false,min:-1,max:1},\n yAxis:{show:false,min:-1,max:1},\n grid:{left:0,right:0,top:0,bottom:0},\n series:[{\n type:'custom',\n renderItem:(params,api)=>{\n const s=slices[params.dataIndex];\n const cx=api.getWidth()/2, cy=api.getHeight()/2+6;\n const R=Math.min(api.getWidth(),api.getHeight())*.37;\n const r0=R*.24;\n const rOf=m=>r0+(R-r0)*m/SCALE;\n const pm=(s.p0+s.p1)/2, lr=R+14;\n const cos=Math.cos(pm);\n const kids=[];\n if(params.dataIndex===0){\n [15,30,45].forEach(m=>kids.push({type:'circle',\n shape:{cx,cy,r:rOf(m)},\n style:{fill:'none',stroke:GRID,lineWidth:1,lineDash:[3,4]}}));\n }\n kids.push({type:'sector',\n shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},\n style:{fill:s.color,stroke:BG,lineWidth:3},\n enterFrom:{shape:{r:r0}}});\n kids.push({type:'text',style:{\n x:cx+cos*lr, y:cy+Math.sin(pm)*lr,\n text:s.n+' '+s.share+'% · '+s.mins+'m',\n font:'600 10px Inter',fill:LAB,\n align:Math.abs(cos)\u003c.25?'center':(cos>0?'left':'right'),\n verticalAlign:'middle'},\n enterFrom:{style:{opacity:0}}});\n return {type:'group',children:kids};\n },\n data:slices.map(s=>s.share),\n }],\n});\n})();\n\n// ════ mono-fancy3 · 3: single-axis small multiples ════\n(()=>{\nconst days=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nconst hours=Array.from({length:24},(_,i)=>i);\nconst mkRow=di=>hours.map(h=>{\n const workday=di\u003c5, core=h>=9&&h\u003c=17;\n 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\u003c=5?0:1));\n return [h,v];\n}).filter(d=>d[1]>0);\neReveal('singleaxis',{\n animationDuration:500,animationEasing:'backOut',\n tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},\n title:days.map((d,i)=>({\n text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,\n textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},\n })),\n singleAxis:days.map((d,i)=>({\n left:52,right:14,type:'category',boundaryGap:false,\n data:hours.map(h=>String(h).padStart(2,'0')),\n top:i*100/7+6+'%',height:100/7-8+'%',\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},\n splitLine:{show:false},\n })),\n series:days.map((d,i)=>({\n singleAxisIndex:i,coordinateSystem:'singleAxis',\n type:'scatter',\n data:mkRow(i),\n symbolSize:dd=>dd[1]*2.6,\n itemStyle:{color:i\u003c5?(i===0?hi(SER[0]):SER[i]):FAINTDATA},\n animationDelay:di=>i*160+di*14,\n })),\n});\n})();\n\n// ════ mono-fancy3 · 4: jitter strip ════\n(()=>{\nconst cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];\nconst mkPts=(ci,n,median,spread,outliers)=>{\n const pts=[];\n for(let i=0;i\u003cn;i++){\n const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);\n let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);\n if(i\u003coutliers) x=median+spread*2.2+u*spread*3;\n const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;\n pts.push([Math.max(.2,x),jy]);\n }\n return pts;\n};\nconst groups=[\n mkPts(0,38,.8,.5,2),\n mkPts(1,64,2.4,1.1,3),\n mkPts(2,110,6.5,2.4,4),\n mkPts(3,72,14,4.5,3),\n];\nconst flat=groups.flat();\neReveal('jitter',{\n animationDuration:450,animationEasing:'cubicOut',\n animationDelay:i=>i\u003cflat.length?Math.round(flat[i][1])*260+(i%37)*9:0,\n tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},\n grid:{left:86,right:16,top:14,bottom:30},\n xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:FAINT,fontSize:8.5},\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},\n yAxis:{type:'value',min:-.6,max:3.6,inverse:true,\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:false}},\n series:[{\n type:'scatter',\n data:flat,\n symbolSize:7,\n itemStyle:{color:p=>RAMP[4-Math.round(p.value[1])],opacity:.62},\n },{\n type:'scatter',silent:true,symbolSize:0,\n data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],\n color:LAB,fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),\n }],\n});\n})();\n\n// ════ mono-fancy4 · 1: bar race ════\n(()=>{\nconst P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];\nconst YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];\nconst vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];\nfor(let y=1;y\u003cYEARS.length;y++)\n vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));\nobsReveal('race',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let step=0;\n const frame=()=>{\n const row=vals[step];\n const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);\n g.setOption({\n animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:v=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v)+'K'},\n grid:{left:64,right:64,top:8,bottom:8},\n xAxis:{type:'value',max:'dataMax',\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:P,inverse:true,max:7,\n animationDuration:300,animationDurationUpdate:300,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',realtimeSort:true,barCategoryGap:'32%',\n data:row.map((v,i)=>({value:v,\n itemStyle:{color:rampAt(rank.indexOf(i),8),borderRadius:99}})),\n label:{show:true,position:'right',valueAnimation:true,\n fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,\n formatter:p=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(p.value)+'K'},\n }],\n graphic:[{id:'yr',type:'text',right:18,bottom:14,\n style:{text:YEARS[step],font:'800 44px Inter',fill:GRID}}],\n });\n step=(step+1)%YEARS.length;\n };\n frame();\n keep('race',setInterval(frame,1150));\n});\n})();\n\n// ════ mono-fancy4 · 2: dynamic data stream ════\n(()=>{\nconst WIN=50;\nobsReveal('stream',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let t=0,v=64;\n const data=[];\n for(;t\u003cWIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}\n const draw=()=>g.setOption({\n animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:x=>x+'k users'},\n grid:{left:14,right:58,top:44,bottom:20},\n xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},\n yAxis:{show:false,min:20,max:130},\n series:[{\n type:'line',data:[...data],smooth:.45,symbol:'none',\n lineStyle:{color:DATA,width:2.4},\n endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:TXT,\n formatter:p=>p.value+'k'},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.16)},{offset:1,color:alpha(DATA,0)}]}},\n }],\n graphic:[{type:'group',left:16,top:10,children:[\n {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:HERO}},\n {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:TXT}},\n ]}],\n });\n draw();\n keep('stream',setInterval(()=>{\n v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));\n data.push(Math.round(v));data.shift();t++;\n draw();\n },300));\n});\n})();\n\n// ════ mono-fancy4 · 3: draw-in + counter ════\n(()=>{\nconst days=Array.from({length:180},(_,i)=>i);\nconst daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);\nconst cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);\nconst TOTAL=cum[cum.length-1];\nconst DUR=2600;\nlet gen=0; // replay guard: only the latest counter loop may write\nobsReveal('stroke',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n gen++;const my=gen;\n g.setOption({\n animationDuration:DUR,animationEasing:'cubicOut',\n tooltip:{show:false},\n grid:{left:14,right:20,top:64,bottom:24},\n xAxis:{type:'category',data:days,boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},\n axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,fontWeight:600,\n interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},\n yAxis:{show:false,max:TOTAL*1.06},\n series:[{\n type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',\n lineStyle:{color:DATA,width:2.6},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.14)},{offset:1,color:alpha(DATA,0)}]}},\n }],\n });\n const t0=performance.now();\n const tick=()=>{\n if(my!==gen)return;\n const p=Math.min(1,(performance.now()-t0)/DUR);\n const eased=1-Math.pow(1-p,3);\n const val=(TOTAL*eased/1000).toFixed(2);\n g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[\n {type:'text',style:{text:'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+val+'M',font:'800 32px Inter',fill:hi(TXT)}},\n {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:MUT}},\n ]}]});\n if(p\u003c1)requestAnimationFrame(tick);\n };\n requestAnimationFrame(tick);\n});\n})();\n\naddEventListener('resize',()=>{\n ['forest','circular','tree','rainfall','morph','negbar','force','wave',\n 'custompie','singleaxis','jitter','race','stream','stroke']\n .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ G19 · violin ════\n// 小提琴:提前聚合的密度剪影,Glance 世界观。\n// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),\n// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。\n(()=>{\nconst G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];\nconst M=64;\nconst samples=g=>Array.from({length:M},(_,i)=>{\n const [,c,s]=G[g];\n const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);\n return Math.max(.2,v);\n});\nobsReveal('violin',s=>{\n const TOP=40,BASE=262,HMAX=17;\n const mapY=h=>BASE-h/HMAX*(BASE-TOP);\n const colX=g=>76+g*84;\n // hour grid\n [0,4,8,12,16].forEach(h=>{\n el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(8,31,92,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${h*.01}s`});\n txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.01}s`},h+'h');\n });\n const SHADE=[INK,'#334EAC','#7096D1','#BAD6EB'];\n G.forEach(([name],g)=>{\n const vs=samples(g).sort((a,b)=>a-b);\n const med=vs[Math.floor(M/2)];\n // fuller bandwidth = the classic rounded violin belly, not a spindle\n const bw=Math.max(.9,G[g][2]*.72);\n // pad past the data by 1.6 bandwidths so the density decays to ~0 and the\n // ends taper naturally — clipping tighter gives flat-cut lantern ends\n const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);\n const NPTS=44;\n const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));\n const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));\n const dmax=Math.max(...dens);\n const W=30;\n const x=colX(g);\n // smooth closed outline through the mirrored density — quadratic\n // midpoint smoothing, same trick as almanac's blob\n const pts=[];\n grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));\n for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let t=0;t\u003cpts.length;t++){\n const p=pts[t],q=pts[(t+1)%pts.length];\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)}`;\n }\n const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});\n tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);\n // median: paper tick across the body, value parked right\n const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));\n const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;\n el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),\n stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});\n txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');\n});\n})();\n\n// ════ G20 · matrix heat ════\n// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出\n// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。\n(()=>{\nconst FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];\nconst VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];\nconst BASEV=[88,74,61,42,35,27];\nconst v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));\nconst shade=t=>t>64?INK:t>46?'#334EAC':t>30?'#7096D1':t>15?'#BAD6EB':'#D0E3FF';\nobsReveal('gheat',s=>{\n const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;\n FEAT.forEach((f,c)=>{\n txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);\n });\n VER.forEach((ver,r)=>{\n txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'rgba(8,31,92,.72)','text-anchor':'end',\n class:'fade',style:`animation-delay:${r*.05}s`},ver);\n FEAT.forEach((f,c)=>{\n const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;\n const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),\n class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});\n tip(cell,`${f} on ${ver} — ${t}% of accounts`);\n 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',\n class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);\n });\n });\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');\n});\n})();\n\n// ════ G21 · rank strip ════\n// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,\n// 和 slope 同一种丑(catalog 实测三连败那条),废弃。\n// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。\n// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。\n(()=>{\nconst RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],\n ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];\nconst SHADE=[INK,'#334EAC','#7096D1','#BAD6EB','#D0E3FF']; // rank 1 → 5\nobsReveal('rankstrip',s=>{\n // rows sorted by final rank, the climber lands on top\n const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);\n const X0=110,PX=38,Y0=64,PY=42,C=32;\n ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>\n txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));\n rows.forEach(([p,rk],i)=>{\n const y=Y0+i*PY,hero=p==='Flows';\n txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'rgba(8,31,92,.72)','text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.07}s`},p);\n rk.forEach((r,q)=>{\n const dark=r\u003c=2;\n const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],\n stroke:'rgba(8,31,92,.24)','stroke-width':0.9,\n class:'pop',style:`animation-delay:${i*.07+q*.035}s`});\n tip(cell,`${p} — #${r} in Q${q+1}`);\n txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,\n fill:dark?PAPER:'rgba(8,31,92,.72)','text-anchor':'middle',\n class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);\n });\n // arrival flag: climbed / fell / held, told at the row's end\n const d=rk[0]-rk[5];\n txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,\n fill:d>0?INK:d\u003c0?'#BAD6EB':'rgba(8,31,92,.32)',\n class:'fade',style:`animation-delay:${.5+i*.07}s`},\n d>0?'▲'+d:d\u003c0?'▼'+(-d):'—');\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');\n});\n})();\n\n// ════ G22 · aggregate sankey ════\n// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——\n// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。\n// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。\n(()=>{\nconst SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];\nconst DST=['FREE','PRO','TEAM'];\nconst SH=[INK,'#334EAC','#7096D1','#BAD6EB','rgba(8,31,92,.32)'];\nobsReveal('sankey',s=>{\n const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;\n const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));\n const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));\n const sy=[],dy=[];\n let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});\n y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});\n const syc=[...sy],dyc=[...dy];\n SRC.forEach((d,i)=>{\n d[1].forEach((v,j)=>{\n if(!v)return;\n const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;\n syc[i]=sb;dyc[j]=db;\n 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`,\n fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});\n tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);\n });\n });\n SRC.forEach((d,i)=>{\n el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],\n class:'fade',style:`animation-delay:${i*.06}s`});\n txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#334EAC','text-anchor':'end',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);\n });\n DST.forEach((n,j)=>{\n el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#081F5C',\n class:'fade',style:`animation-delay:${.1+j*.06}s`});\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#334EAC',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);\n });\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":49445,"content_sha256":"54ded051437dedd40bba56ec18ee03cbbb8276c60452ddf6766f8218068a080a"},{"filename":"templates/color/glance-wire.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Glance 20 × WIRE\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/chart.js@4\">\u003c/script>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0F0EE;--dark:#1F1E1C;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.40);--grid:rgba(31,30,28,.16);--lab:rgba(31,30,28,.72);--dmid:rgba(240,240,238,.62);--dq:rgba(240,240,238,.24)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.dark{background:var(--dark);color:var(--bg)}\n .card.dark .sub{color:var(--dmid)}\n .card.dark .src{color:var(--dq)}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .ch{height:320px}\n .wrap{position:relative;height:320px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .pagehead .badge{margin:0 2px}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px}\n .badge.del{background:var(--ink);color:var(--bg)}\n .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}\n .badge.pair{border:1px dashed var(--faint);color:var(--muted)}\n .card.dark .badge.del{background:var(--bg);color:var(--ink)}\n .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}\n .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Glance 系(快读)· 20 张 × WIRE\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#F5572F\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#22211F\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#8F8E86\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#C0BFB7\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#DBDAD3\">\u003c/span>  编辑部红 · 分类位全走黑灰,红只在两处出现:每张图的主角元素,以及序数梯爬完的最后一档(营收榜首 / P0 / 最新年份 / 峰值市场)。红是稀客,这是它的身份。\u003c/p>\n \u003cp style=\"margin-top:7px\">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 个来源 / 4 条产品线 / 5 个工作日 / 10 支小队 / 12 个集成)各走一个分类位,序数型(8 年营收排名 / 50 个市场 / 4 档优先级 / 树木年份 / 面板停留时长 / 情绪票数)走 5 档梯,深色卡换纸色梯、末档落在强调色。几何、动画、数据与 glance-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003c!-- ══ mono-demo ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Revenue by plan\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · MRR, $K\u003c/div>\n \u003cdiv class=\"wrap\">\u003ccanvas id=\"c3\">\u003c/canvas>\u003c/div>\n \u003cdiv class=\"src\">CHUNKY BARS · MONO-DEMO · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-redesign · dot waffle ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where sign-ups come from\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · every dot = 1% of new accounts\u003c/div>\n \u003csvg id=\"waffle\" viewBox=\"0 0 380 300\" width=\"100%\" style=\"max-height:320px\">\u003c/svg>\n \u003cdiv class=\"src\">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Trees planted, year by year\u003c/h2>\n \u003cdiv class=\"sub\">reforestation program · thousands of trees\u003c/div>\n \u003cdiv class=\"ch\" id=\"forest\">\u003c/div>\n \u003cdiv class=\"src\">PICTORIAL BAR · MONO-FANCY · GREEN OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Who works with whom\u003c/h2>\n \u003cdiv class=\"sub\">cross-team collaborations · Q2 2026 · line = shared project\u003c/div>\n \u003cdiv class=\"ch\" id=\"circular\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Everything the platform ships\u003c/h2>\n \u003cdiv class=\"sub\">product areas and capabilities · Jun 2026\u003c/div>\n \u003cdiv class=\"ch\" id=\"tree\">\u003c/div>\n \u003cdiv class=\"src\">TREE · MONO-FANCY · PRODUCT CATALOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Campaigns rain down, sign-ups flow\u003c/h2>\n \u003cdiv class=\"sub\">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days\u003c/div>\n \u003cdiv class=\"ch\" id=\"rainfall\">\u003c/div>\n \u003cdiv class=\"src\">DUAL AREA · MONO-FANCY · GROWTH + ADS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>One dataset, three views\u003c/h2>\n \u003cdiv class=\"sub\">watch the same 12 products fly between views · every 3s\u003c/div>\n \u003cdiv class=\"ch\" id=\"morph\">\u003c/div>\n \u003cdiv class=\"src\">SCATTER MORPH · MONO-FANCY · COHORTS\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy2 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where we gained, where we bled\u003c/h2>\n \u003cdiv class=\"sub\">net accounts per segment · Q2 2026 · churn left, growth right\u003c/div>\n \u003cdiv class=\"ch\" id=\"negbar\">\u003c/div>\n \u003cdiv class=\"src\">DIVERGING BAR · MONO-FANCY2 · CRM\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Integrations, pulled into orbit\u003c/h2>\n \u003cdiv class=\"sub\">force layout · node = monthly syncs · drag a node, the web fights back\u003c/div>\n \u003cdiv class=\"ch\" id=\"force\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · NEW · INTEGRATION HUB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Fifty markets, one wave\u003c/h2>\n \u003cdiv class=\"sub\">sign-ups per market · bars cascade in · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"wave\">\u003c/div>\n \u003cdiv class=\"src\">STAGGER DELAY · MONO-FANCY2 · GROWTH\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy3 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Big slice, deep engagement\u003c/h2>\n \u003cdiv class=\"sub\">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min\u003c/div>\n \u003cdiv class=\"ch\" id=\"custompie\">\u003c/div>\n \u003cdiv class=\"src\">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Support load, day by day\u003c/h2>\n \u003cdiv class=\"sub\">tickets per hour · each row is its own axis · dot = volume\u003c/div>\n \u003cdiv class=\"ch\" id=\"singleaxis\">\u003c/div>\n \u003cdiv class=\"src\">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Response times, spread out\u003c/h2>\n \u003cdiv class=\"sub\">every dot = one ticket · jitter reveals the pile-up · by priority\u003c/div>\n \u003cdiv class=\"ch\" id=\"jitter\">\u003c/div>\n \u003cdiv class=\"src\">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy4 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Eight products race for revenue\u003c/h2>\n \u003cdiv class=\"sub\">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"race\">\u003c/div>\n \u003cdiv class=\"src\">BAR RACE · MONO-FANCY4 · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Concurrent users, streaming\u003c/h2>\n \u003cdiv class=\"sub\">one new reading every 300ms · window of 50 · click to restart\u003c/div>\n \u003cdiv class=\"ch\" id=\"stream\">\u003c/div>\n \u003cdiv class=\"src\">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>H1 revenue, drawn in one stroke\u003c/h2>\n \u003cdiv class=\"sub\">cumulative ARR · the line draws, the number counts · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"stroke\">\u003c/div>\n \u003cdiv class=\"src\">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE\u003c/div>\n \u003c/div>\n\n\n \u003c!-- G19 · violin -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 分布型 · 小提琴\u003c/span>\n \u003ch2>How fast each plan gets an answer\u003c/h2>\n \u003cdiv class=\"sub\">hours to first reply · silhouette = where tickets pile up · paper tick = median\u003c/div>\n \u003csvg id=\"violin\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">VIOLIN · WIRE · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- G20 · matrix heat -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 热力型 · 通用矩阵\u003c/span>\n \u003ch2>Adoption runs hot on the new versions\u003c/h2>\n \u003cdiv class=\"sub\">5 versions × 6 features · shade = % of accounts using it · every cell carries its number\u003c/div>\n \u003csvg id=\"gheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">MATRIX HEAT (GLANCE) · WIRE · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- G21 · rank strip -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 排名型 · RANK STRIP\u003c/span>\n \u003ch2>Flows climbs to the top\u003c/h2>\n \u003cdiv class=\"sub\">one row per product, one cell per quarter · darker = higher rank · a bar race you can print\u003c/div>\n \u003csvg id=\"rankstrip\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RANK STRIP · WIRE · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- G22 · aggregate sankey -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 流向型 · 聚合桑基\u003c/span>\n \u003ch2>Channels pour into plans\u003c/h2>\n \u003cdiv class=\"sub\">ribbon width = accounts · channels on the left, the plans they land in on the right\u003c/div>\n \u003csvg id=\"sankey\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">AGGREGATE SANKEY · WIRE · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── WIRE 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F0F0EE',DARK='#1F1E1C',TXT='#1F1E1C';\nconst MUT='rgba(31,30,28,.60)',LAB='rgba(31,30,28,.72)',FAINT='rgba(31,30,28,.40)',QUIET='rgba(31,30,28,.15)',GRID='rgba(31,30,28,.16)';\nconst DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F';\nconst HERODK='#F5572F'; // 深色卡上的强调色(HERO 在深底上可能读不出)\nconst FAINTDATA='#C0BFB7'; // 静音数据档:周末 / 非主序列\nconst DMID='rgba(240,240,238,.62)',DQ='rgba(240,240,238,.24)'; // 深色卡上的浅墨 / 静音档\nconst SER=['#22211F','#8F8E86','#C0BFB7','rgba(34,33,31,.52)','#DBDAD3','rgba(34,33,31,.3)']; // 分类位:一图之内每个系列 / 每类 / 每行一个色\nconst RAMP=['#DBDAD3','#C0BFB7','#8F8E86','#22211F','#F5572F']; // 5 档序数梯(浅→重):排名 / 票量 / 峰值 / 停留时长\nconst DRAMP4=['#F5572F','#C0BFB7','#8F8E86','rgba(240,240,238,.26)']; // 深色卡上的 4 档序数梯(浅→深)\nconst HEROMODE=true; // 主角模式:分类位的头号项也整体走 HERO\n\n// ── 别名与派生:深色卡的线全是卡底向纸色的插值,不引入新色相 ──\nconst INK=TXT,PAPER=BG,MUTED=MUT;\nconst rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));\nconst alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;\nconst mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');\nconst lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v\u003c=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));\n return .2126*r+.7152*g+.0722*b};\nconst ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};\nconst readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG); // 深色卡上落在色块里的字\nconst DLINE=mix(DARK,BG,.20),DLINE2=mix(DARK,BG,.13),DSHELL=mix(DARK,BG,.09);\nconst hi=c=>HEROMODE?HERO:c;\n// 名次 → 序数梯。末档(WIRE 的红)只发给第 1 名,其余摊在前四档上\nconst rampAt=(i,n)=>i===0?RAMP[4]\n :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];\nconst rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);\n return i=>r.indexOf(i)};\nconst tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── unified reveal: every chart waits to be scrolled into view; click replays.\n// fn charts manage their own timers via keep(); they're cleared on replay.\nconst timers={};\nconst obsReveal=(id,fn)=>{\n const el=document.getElementById(id);\n const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};\n const io=new IntersectionObserver(es=>{\n if(es[0].isIntersecting){go();io.disconnect()}\n },{threshold:.35});\n io.observe(el);\n el.style.cursor='pointer';\n el.addEventListener('click',go);\n};\nconst keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};\nconst eReveal=(id,opt)=>obsReveal(id,el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();g.setOption(opt);\n});\nconst cjs={};\nconst cReveal=(id,mkCfg)=>obsReveal(id,el=>{\n cjs[id]?.destroy();\n cjs[id]=new Chart(el,mkCfg());\n});\n\nChart.defaults.color=MUTED;\nChart.defaults.font.family='Inter';\nChart.defaults.font.size=10.5;\nChart.defaults.borderColor=GRID;\nChart.defaults.plugins.legend.display=false;\nChart.defaults.plugins.tooltip.backgroundColor=INK;\nChart.defaults.plugins.tooltip.titleColor=BG;\nChart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);\nChart.defaults.plugins.tooltip.padding=12;\nChart.defaults.plugins.tooltip.cornerRadius=12;\nChart.defaults.plugins.tooltip.displayColors=false;\n\n// ════ mono-demo · 3: chunky bars, values on top ════\n(()=>{\nconst V3=[182,486,391,274],rk3=rankOf(V3);\ncReveal('c3',()=>({\n type:'bar',\n data:{labels:['STARTER','PRO','TEAM','ENT'],\n datasets:[{data:V3,\n backgroundColor:V3.map((_,i)=>rampAt(rk3(i),4)),\n borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},\n options:{\n maintainAspectRatio:false,\n animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},\n plugins:{tooltip:{callbacks:{label:i=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+i.raw+'K MRR'}}},\n scales:{\n x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},\n y:{display:false,suggestedMax:540},\n },\n },\n plugins:[{\n id:'topValues',\n afterDatasetsDraw(chart){\n const {ctx}=chart;\n chart.getDatasetMeta(0).data.forEach((bar,i)=>{\n ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';\n ctx.fillText('

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();\n });\n },\n }],\n}));\n})();\n\n// ════ mono-redesign · donut → waffle of dots ════\n(()=>{\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];\nobsReveal('waffle',s=>{\n s.innerHTML='';\n const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;\n let idx=0;\n D.forEach(([name,v],g)=>{\n for(let k=0;k\u003cv;k++){\n const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;\n mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:g===0?hi(SER[0]):SER[g],\n class:'pop',style:`animation-delay:${c*.008+g*.05}s`});\n }\n idx+=v;\n });\n D.forEach(([name,v],g)=>{\n const y=26+g*40;\n mkEl(s,'circle',{cx:246,cy:y,r:6,fill:g===0?hi(SER[0]):SER[g]});\n const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});\n n.textContent=name;\n const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g\u003c2?TXT:MUT});\n val.textContent=v+'%';\n });\n});\n})();\n\n// ════ mono-fancy · 1: pictorial bar forest ════\n(()=>{\nconst 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';\nconst years=['2022','2023','2024','2025','2026'];\nconst vals=[26,41,63,88,117];\neReveal('forest',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},\n grid:{left:44,right:56,top:10,bottom:8},\n yAxis:{type:'category',data:years,inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},\n xAxis:{show:false,max:13},\n series:[{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:RAMP[i]}})),\n label:{show:true,position:'right',offset:[8,0],\n color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,\n formatter:p=>vals[p.dataIndex]+'k'},\n z:10,\n },{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n itemStyle:{color:QUIET},data:years.map(()=>13),silent:true,z:5,\n }],\n});\n})();\n\n// ════ mono-fancy · 2: circular graph on dark ════\n(()=>{\nconst teams=[\n ['Product',52],['Design',38],['Frontend',46],['Backend',44],\n ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],\n];\nconst links=[\n [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],\n [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],\n];\neReveal('circular',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:tipDark,\n series:[{\n type:'graph',layout:'circular',circular:{rotateLabel:false},\n left:52,right:52,top:34,bottom:34,\n data:teams.map(([n,v],i)=>({\n name:n,value:v,symbolSize:v*.62,\n itemStyle:{color:i===0?HERODK:i\u003c4?BG:i\u003c8?DMID:DQ,borderWidth:0},\n label:{show:true,position:'right',distance:7,color:DMID,fontFamily:'Inter',fontSize:9.5,fontWeight:600},\n })),\n links:links.map(([s,t,w])=>({\n source:s,target:t,\n lineStyle:{width:w*.7,color:DLINE,opacity:.55,curveness:.28},\n })),\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},\n }],\n});\n})();\n\n// ════ mono-fancy · 3: LR tree ════\n(()=>{\nconst b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},\n children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});\neReveal('tree',{\n animationDuration:1100,animationEasing:'quarticOut',\n tooltip:tipLight,\n series:[{\n type:'tree',layout:'orthogonal',orient:'LR',\n left:64,right:96,top:8,bottom:8,\n symbol:'circle',symbolSize:7,\n initialTreeDepth:2,expandAndCollapse:false,roam:false,\n itemStyle:{borderWidth:0},\n lineStyle:{width:1.4,curveness:.5},\n label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:LAB,position:'left'},\n leaves:{label:{position:'right',color:MUTED,fontWeight:500}},\n data:[{\n name:'Platform',itemStyle:{color:TXT},lineStyle:{color:FAINT},\n label:{fontSize:11.5,color:INK},\n children:[\n b('Editor',['Blocks','Tables','Comments','History'],hi(SER[0])),\n b('Automate',['Workflows','Triggers','Webhooks'],SER[1]),\n b('Collaborate',['Spaces','Guests','Mentions'],SER[2]),\n b('Integrate',['API','Slack','GitHub'],SER[3]),\n ],\n }],\n }],\n});\n})();\n\n// ════ mono-fancy · 4: rainfall dual area ════\n(()=>{\nconst days=Array.from({length:30},(_,i)=>i+1);\nconst 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];\nconst 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];\nconst RMAX=Math.max(...rain);\neReveal('rainfall',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipLight,trigger:'axis',\n formatter:p=>`Day ${p[0].axisValue} — spend ${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},\n axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:FAINT}},\n grid:[\n {left:44,right:14,top:8,height:'26%'},\n {left:44,right:14,top:'42%',bottom:26},\n ],\n xAxis:[\n {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},\n {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},\n ],\n yAxis:[\n {gridIndex:0,inverse:true,max:18,splitLine:{show:false},\n axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,formatter:'${value}K'},\n axisLine:{show:false},axisTick:{show:false}},\n {gridIndex:1,splitLine:{lineStyle:{color:GRID}},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},\n axisLine:{show:false},axisTick:{show:false}},\n ],\n series:[\n {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,\n data:rain.map(v=>({value:v,itemStyle:{color:v===RMAX?hi(DATA2):DATA2}})),\n itemStyle:{borderRadius:[0,0,4,4]},barWidth:'55%'},\n {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,\n smooth:.4,symbol:'none',lineStyle:{color:DATA,width:2.2},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.22)},{offset:1,color:alpha(DATA,0)}]}}},\n ],\n});\n})();\n\n// ════ mono-fancy · 5: universal transition morph ════\n(()=>{\nconst P12=[\n ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],\n ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],\n ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],\n ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],\n];\nconst RK12=[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]);\nconst shade=n=>rampAt(RK12.indexOf(n),12);\nconst axis={splitLine:{lineStyle:{color:GRID}},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};\nconst VIEWS=[\n {opt:{\n grid:{left:38,right:16,top:30,bottom:30},\n xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

,nameTextStyle:{color:FAINT,fontSize:8.5}},\n yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:FAINT,fontSize:8.5}},\n series:[{\n id:'p',type:'scatter',universalTransition:true,\n symbolSize:d=>Math.sqrt(d[3])*1.35,\n data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],\n groupId:d[0],itemStyle:{color:shade(d[0])}})),\n label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,\n formatter:p=>p.name},\n }],\n }},\n {opt:{\n grid:{left:38,right:16,top:30,bottom:44},\n xAxis:{...axis,type:'category',\n data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),\n axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},\n yAxis:{...axis,type:'value',max:520},\n series:[{\n id:'p',type:'bar',universalTransition:true,\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(d[0]),borderRadius:[6,6,0,0]}})),\n barCategoryGap:'32%',\n }],\n }},\n {opt:{\n grid:{left:0,right:0,top:0,bottom:0},\n xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},\n series:[{\n id:'p',type:'pie',universalTransition:true,\n radius:['26%','72%'],center:['50%','50%'],\n itemStyle:{borderColor:BG,borderWidth:2,borderRadius:6},\n label:{color:LAB,fontFamily:'Inter',fontSize:9,formatter:'{b}'},\n labelLine:{lineStyle:{color:FAINT}},\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(d[0])}})),\n }],\n }},\n];\nconst base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};\nobsReveal('morph',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let vi=0;\n g.setOption({...base,...VIEWS[0].opt});\n keep('morph',setInterval(()=>{\n vi=(vi+1)%VIEWS.length;\n g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});\n },3000));\n});\n})();\n\n// ════ mono-fancy2 · 2: diverging bar ════\n(()=>{\nconst D=[\n ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],\n ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],\n];\nconst MAXG=Math.max(...D.map(d=>d[1]));\neReveal('negbar',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},\n grid:{left:96,right:52,top:8,bottom:8},\n xAxis:{type:'value',\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',barWidth:16,\n data:D.map(([n,v])=>({name:n,value:v,\n itemStyle:{color:v>0?(v===MAXG?hi(DATA):DATA):DATA2,\n borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),\n label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,\n position:'outside',\n formatter:p=>(p.value>0?'+':'')+p.value,color:TXT},\n markLine:{symbol:'none',silent:true,label:{show:false},\n lineStyle:{color:MUT,width:1.5},\n data:[{xAxis:0}]},\n }],\n});\n})();\n\n// ════ force graph on dark: the network shakes itself into place ════\n// (replaced the axis-break bar — bars must never lie about length; house rule)\n(()=>{\nconst hubs=[['Core API',52,0]];\nconst apps=[\n ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],\n ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],\n ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],\n];\nconst nodes=[...hubs,...apps];\nconst links=[\n ...apps.map((a,i)=>({source:a[0],target:'Core API',\n lineStyle:{width:Math.max(.8,a[1]*.09),color:DLINE,opacity:.6}})),\n // a few app-to-app side roads\n {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},\n {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},\n {source:'Figma',target:'Notion',lineStyle:{width:1,color:DLINE2,opacity:.5}},\n {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:DLINE2,opacity:.5}},\n];\neReveal('force',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},\n roam:false,draggable:true,\n left:14,right:14,top:14,bottom:14,\n data:nodes.map(([n,v,tier])=>({\n name:n,value:v,symbolSize:8+v*.75,\n itemStyle:{color:tier===0?HERODK:tier===1?BG:DMID},\n label:{show:true,position:'right',distance:6,\n color:tier===0?BG:DMID,fontFamily:'Inter',\n fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},\n })),\n links,\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},\n }],\n});\n})();\n\n// ════ mono-fancy2 · 4: stagger wave ════\n(()=>{\nconst n=50;\nconst vals=Array.from({length:n},(_,i)=>\n Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));\nconst rkW=rankOf(vals);\neReveal('wave',{\n tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},\n grid:{left:36,right:10,top:14,bottom:24},\n xAxis:{type:'category',data:vals.map((_,i)=>i+1),\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},\n yAxis:{type:'value',\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},\n series:[{\n type:'bar',barCategoryGap:'26%',\n data:vals.map((v,i)=>({value:v,\n itemStyle:{color:rampAt(rkW(i),vals.length),borderRadius:[4,4,0,0]}})),\n animationDelay:i=>i*36,\n animationDuration:640,animationEasing:'elasticOut',\n }],\n animationDurationUpdate:640,\n animationDelayUpdate:i=>i*36,\n});\n})();\n\n// ════ mono-fancy3 · 1: custom double-encoded pie ════\n(()=>{\nconst D=[\n ['Editor',34,42],['Boards',22,31],['Docs',18,38],\n ['Chat',12,11],['Automations',8,24],['Other',6,7],\n];\nconst SCALE=45;\nconst TAU=Math.PI*2;\nconst byShare=[...D].sort((a,b)=>b[1]-a[1]);\nconst minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);\nlet acc=0;\nconst slices=byShare.map(([n,share,mins])=>{\n const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;\n acc+=share;\n return {n,share,mins,p0,p1,color:rampAt(minsDesc.indexOf(mins),6)};\n});\neReveal('custompie',{\n animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,\n tooltip:{...tipLight,formatter:p=>{\n const s=slices[p.dataIndex];\n return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';\n }},\n xAxis:{show:false,min:-1,max:1},\n yAxis:{show:false,min:-1,max:1},\n grid:{left:0,right:0,top:0,bottom:0},\n series:[{\n type:'custom',\n renderItem:(params,api)=>{\n const s=slices[params.dataIndex];\n const cx=api.getWidth()/2, cy=api.getHeight()/2+6;\n const R=Math.min(api.getWidth(),api.getHeight())*.37;\n const r0=R*.24;\n const rOf=m=>r0+(R-r0)*m/SCALE;\n const pm=(s.p0+s.p1)/2, lr=R+14;\n const cos=Math.cos(pm);\n const kids=[];\n if(params.dataIndex===0){\n [15,30,45].forEach(m=>kids.push({type:'circle',\n shape:{cx,cy,r:rOf(m)},\n style:{fill:'none',stroke:GRID,lineWidth:1,lineDash:[3,4]}}));\n }\n kids.push({type:'sector',\n shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},\n style:{fill:s.color,stroke:BG,lineWidth:3},\n enterFrom:{shape:{r:r0}}});\n kids.push({type:'text',style:{\n x:cx+cos*lr, y:cy+Math.sin(pm)*lr,\n text:s.n+' '+s.share+'% · '+s.mins+'m',\n font:'600 10px Inter',fill:LAB,\n align:Math.abs(cos)\u003c.25?'center':(cos>0?'left':'right'),\n verticalAlign:'middle'},\n enterFrom:{style:{opacity:0}}});\n return {type:'group',children:kids};\n },\n data:slices.map(s=>s.share),\n }],\n});\n})();\n\n// ════ mono-fancy3 · 3: single-axis small multiples ════\n(()=>{\nconst days=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nconst hours=Array.from({length:24},(_,i)=>i);\nconst mkRow=di=>hours.map(h=>{\n const workday=di\u003c5, core=h>=9&&h\u003c=17;\n 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\u003c=5?0:1));\n return [h,v];\n}).filter(d=>d[1]>0);\neReveal('singleaxis',{\n animationDuration:500,animationEasing:'backOut',\n tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},\n title:days.map((d,i)=>({\n text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,\n textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},\n })),\n singleAxis:days.map((d,i)=>({\n left:52,right:14,type:'category',boundaryGap:false,\n data:hours.map(h=>String(h).padStart(2,'0')),\n top:i*100/7+6+'%',height:100/7-8+'%',\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},\n splitLine:{show:false},\n })),\n series:days.map((d,i)=>({\n singleAxisIndex:i,coordinateSystem:'singleAxis',\n type:'scatter',\n data:mkRow(i),\n symbolSize:dd=>dd[1]*2.6,\n itemStyle:{color:i\u003c5?(i===0?hi(SER[0]):SER[i]):FAINTDATA},\n animationDelay:di=>i*160+di*14,\n })),\n});\n})();\n\n// ════ mono-fancy3 · 4: jitter strip ════\n(()=>{\nconst cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];\nconst mkPts=(ci,n,median,spread,outliers)=>{\n const pts=[];\n for(let i=0;i\u003cn;i++){\n const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);\n let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);\n if(i\u003coutliers) x=median+spread*2.2+u*spread*3;\n const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;\n pts.push([Math.max(.2,x),jy]);\n }\n return pts;\n};\nconst groups=[\n mkPts(0,38,.8,.5,2),\n mkPts(1,64,2.4,1.1,3),\n mkPts(2,110,6.5,2.4,4),\n mkPts(3,72,14,4.5,3),\n];\nconst flat=groups.flat();\neReveal('jitter',{\n animationDuration:450,animationEasing:'cubicOut',\n animationDelay:i=>i\u003cflat.length?Math.round(flat[i][1])*260+(i%37)*9:0,\n tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},\n grid:{left:86,right:16,top:14,bottom:30},\n xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:FAINT,fontSize:8.5},\n splitLine:{lineStyle:{color:GRID}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},\n yAxis:{type:'value',min:-.6,max:3.6,inverse:true,\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:false}},\n series:[{\n type:'scatter',\n data:flat,\n symbolSize:7,\n itemStyle:{color:p=>RAMP[4-Math.round(p.value[1])],opacity:.62},\n },{\n type:'scatter',silent:true,symbolSize:0,\n data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],\n color:LAB,fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),\n }],\n});\n})();\n\n// ════ mono-fancy4 · 1: bar race ════\n(()=>{\nconst P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];\nconst YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];\nconst vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];\nfor(let y=1;y\u003cYEARS.length;y++)\n vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));\nobsReveal('race',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let step=0;\n const frame=()=>{\n const row=vals[step];\n const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);\n g.setOption({\n animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:v=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v)+'K'},\n grid:{left:64,right:64,top:8,bottom:8},\n xAxis:{type:'value',max:'dataMax',\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:P,inverse:true,max:7,\n animationDuration:300,animationDurationUpdate:300,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',realtimeSort:true,barCategoryGap:'32%',\n data:row.map((v,i)=>({value:v,\n itemStyle:{color:rampAt(rank.indexOf(i),8),borderRadius:99}})),\n label:{show:true,position:'right',valueAnimation:true,\n fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,\n formatter:p=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(p.value)+'K'},\n }],\n graphic:[{id:'yr',type:'text',right:18,bottom:14,\n style:{text:YEARS[step],font:'800 44px Inter',fill:GRID}}],\n });\n step=(step+1)%YEARS.length;\n };\n frame();\n keep('race',setInterval(frame,1150));\n});\n})();\n\n// ════ mono-fancy4 · 2: dynamic data stream ════\n(()=>{\nconst WIN=50;\nobsReveal('stream',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let t=0,v=64;\n const data=[];\n for(;t\u003cWIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}\n const draw=()=>g.setOption({\n animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:x=>x+'k users'},\n grid:{left:14,right:58,top:44,bottom:20},\n xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},\n yAxis:{show:false,min:20,max:130},\n series:[{\n type:'line',data:[...data],smooth:.45,symbol:'none',\n lineStyle:{color:DATA,width:2.4},\n endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:TXT,\n formatter:p=>p.value+'k'},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.16)},{offset:1,color:alpha(DATA,0)}]}},\n }],\n graphic:[{type:'group',left:16,top:10,children:[\n {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:HERO}},\n {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:TXT}},\n ]}],\n });\n draw();\n keep('stream',setInterval(()=>{\n v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));\n data.push(Math.round(v));data.shift();t++;\n draw();\n },300));\n});\n})();\n\n// ════ mono-fancy4 · 3: draw-in + counter ════\n(()=>{\nconst days=Array.from({length:180},(_,i)=>i);\nconst daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);\nconst cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);\nconst TOTAL=cum[cum.length-1];\nconst DUR=2600;\nlet gen=0; // replay guard: only the latest counter loop may write\nobsReveal('stroke',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n gen++;const my=gen;\n g.setOption({\n animationDuration:DUR,animationEasing:'cubicOut',\n tooltip:{show:false},\n grid:{left:14,right:20,top:64,bottom:24},\n xAxis:{type:'category',data:days,boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},\n axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,fontWeight:600,\n interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},\n yAxis:{show:false,max:TOTAL*1.06},\n series:[{\n type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',\n lineStyle:{color:DATA,width:2.6},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:alpha(DATA,.14)},{offset:1,color:alpha(DATA,0)}]}},\n }],\n });\n const t0=performance.now();\n const tick=()=>{\n if(my!==gen)return;\n const p=Math.min(1,(performance.now()-t0)/DUR);\n const eased=1-Math.pow(1-p,3);\n const val=(TOTAL*eased/1000).toFixed(2);\n g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[\n {type:'text',style:{text:'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+val+'M',font:'800 32px Inter',fill:hi(TXT)}},\n {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:MUT}},\n ]}]});\n if(p\u003c1)requestAnimationFrame(tick);\n };\n requestAnimationFrame(tick);\n});\n})();\n\naddEventListener('resize',()=>{\n ['forest','circular','tree','rainfall','morph','negbar','force','wave',\n 'custompie','singleaxis','jitter','race','stream','stroke']\n .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ G19 · violin ════\n// 小提琴:提前聚合的密度剪影,Glance 世界观。\n// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),\n// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。\n(()=>{\nconst G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];\nconst M=64;\nconst samples=g=>Array.from({length:M},(_,i)=>{\n const [,c,s]=G[g];\n const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);\n return Math.max(.2,v);\n});\nobsReveal('violin',s=>{\n const TOP=40,BASE=262,HMAX=17;\n const mapY=h=>BASE-h/HMAX*(BASE-TOP);\n const colX=g=>76+g*84;\n // hour grid\n [0,4,8,12,16].forEach(h=>{\n el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(31,30,28,.12)','stroke-width':1.44,\n class:'fade',style:`animation-delay:${h*.01}s`});\n txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.01}s`},h+'h');\n });\n const SHADE=[INK,'#6E6D66','#8F8E86','#C0BFB7'];\n G.forEach(([name],g)=>{\n const vs=samples(g).sort((a,b)=>a-b);\n const med=vs[Math.floor(M/2)];\n // fuller bandwidth = the classic rounded violin belly, not a spindle\n const bw=Math.max(.9,G[g][2]*.72);\n // pad past the data by 1.6 bandwidths so the density decays to ~0 and the\n // ends taper naturally — clipping tighter gives flat-cut lantern ends\n const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);\n const NPTS=44;\n const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));\n const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));\n const dmax=Math.max(...dens);\n const W=30;\n const x=colX(g);\n // smooth closed outline through the mirrored density — quadratic\n // midpoint smoothing, same trick as almanac's blob\n const pts=[];\n grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));\n for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let t=0;t\u003cpts.length;t++){\n const p=pts[t],q=pts[(t+1)%pts.length];\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)}`;\n }\n const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});\n tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);\n // median: paper tick across the body, value parked right\n const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));\n const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;\n el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),\n stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});\n // 同箱线:红只标最慢的 FREE,其余走墨\n const slow=g===G.length-1;\n txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':slow?11:9.5,'font-weight':800,\n fill:slow?'#F5572F':INK,\n class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'WIDTH = HOW MANY TICKETS WAIT THAT LONG · RED = THE SLOWEST PLAN · DARKEST = FASTEST');\n});\n})();\n\n// ════ G20 · matrix heat ════\n// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出\n// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。\n(()=>{\nconst FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];\nconst VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];\nconst BASEV=[88,74,61,42,35,27];\nconst v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));\nconst shade=t=>t>64?'#F5572F':t>46?'#6E6D66':t>30?'#8F8E86':t>15?'#C0BFB7':'#DBDAD3';\nobsReveal('gheat',s=>{\n const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;\n FEAT.forEach((f,c)=>{\n txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);\n });\n VER.forEach((ver,r)=>{\n txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'rgba(31,30,28,.72)','text-anchor':'end',\n class:'fade',style:`animation-delay:${r*.05}s`},ver);\n FEAT.forEach((f,c)=>{\n const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;\n const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),\n class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});\n tip(cell,`${f} on ${ver} — ${t}% of accounts`);\n 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',\n class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);\n });\n });\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');\n});\n})();\n\n// ════ G21 · rank strip ════\n// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,\n// 和 slope 同一种丑(catalog 实测三连败那条),废弃。\n// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。\n// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。\n(()=>{\nconst RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],\n ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];\nconst SHADE=['#F5572F','#6E6D66','#8F8E86','#C0BFB7','#DBDAD3']; // rank 1 → 5, #1 wears the accent\nobsReveal('rankstrip',s=>{\n // rows sorted by final rank, the climber lands on top\n const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);\n const X0=110,PX=38,Y0=64,PY=42,C=32;\n ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>\n txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));\n rows.forEach(([p,rk],i)=>{\n const y=Y0+i*PY,hero=p==='Flows';\n txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'rgba(31,30,28,.72)','text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.07}s`},p);\n rk.forEach((r,q)=>{\n const dark=r\u003c=2;\n const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],\n stroke:'rgba(31,30,28,.24)','stroke-width':0.9,\n class:'pop',style:`animation-delay:${i*.07+q*.035}s`});\n tip(cell,`${p} — #${r} in Q${q+1}`);\n txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,\n fill:dark?PAPER:'#1F1E1C','text-anchor':'middle',\n class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);\n });\n // arrival flag: climbed / fell / held, told at the row's end\n const d=rk[0]-rk[5];\n txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,\n fill:d>0?INK:d\u003c0?'#C0BFB7':'rgba(31,30,28,.32)',\n class:'fade',style:`animation-delay:${.5+i*.07}s`},\n d>0?'▲'+d:d\u003c0?'▼'+(-d):'—');\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');\n});\n})();\n\n// ════ G22 · aggregate sankey ════\n// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——\n// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。\n// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。\n(()=>{\nconst SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];\nconst DST=['FREE','PRO','TEAM'];\nconst SH=[INK,'#6E6D66','#8F8E86','#C0BFB7','rgba(31,30,28,.32)'];\nobsReveal('sankey',s=>{\n const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;\n const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));\n const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));\n const sy=[],dy=[];\n let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});\n y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});\n const syc=[...sy],dyc=[...dy];\n SRC.forEach((d,i)=>{\n d[1].forEach((v,j)=>{\n if(!v)return;\n const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;\n syc[i]=sb;dyc[j]=db;\n // 最粗的一条缎带是主角(SEARCH→FREE 20),上红\n const heroRib=i===0&&j===0;\n 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`,\n fill:heroRib?'#F5572F':SH[i],opacity:heroRib?.68:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});\n tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);\n });\n });\n SRC.forEach((d,i)=>{\n el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],\n class:'fade',style:`animation-delay:${i*.06}s`});\n txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#6E6D66','text-anchor':'end',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);\n });\n DST.forEach((n,j)=>{\n el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#22211F',\n class:'fade',style:`animation-delay:${.1+j*.06}s`});\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#6E6D66',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);\n });\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL · RED = THE WIDEST RIBBON');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":49761,"content_sha256":"5fd5332cfa98fc227c7ac07c820adea20a8b2357fb29ca1093d30fdcdb61dec4"},{"filename":"templates/color/lupi-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Lupi L1–L19 × PALM\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#58402E;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.40);--grid:rgba(88,64,46,.16);--lab:rgba(88,64,46,.72);--dmid:rgba(240,239,235,.62);--dq:rgba(240,239,235,.24)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.dark{background:var(--dark);color:#F0EFEB}\n .card.dark .sub{color:var(--dmid)}\n .card.dark .src{color:var(--dq)}\n .card.wide{grid-column:1/-1}\n .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .note{font-size:11px;color:var(--lab);line-height:1.65;margin-bottom:16px}\n .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n svg text{font-family:'Inter','Noto Sans SC',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}\n .pagehead .badge{margin:0 2px}\n .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px}\n .badge.del{background:var(--ink);color:var(--bg)}\n .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}\n .badge.pair{border:1px dashed var(--faint);color:var(--muted)}\n .card.dark .badge.del{background:var(--bg);color:var(--ink)}\n .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}\n .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Lupi 系 L1–L19 × PALM\u003c/h1>\n \u003cp>\u003cspan class=\"chip\" style=\"background:#43593B\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#F2D17E\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#77835A\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#ACAD79\">\u003c/span>\u003cspan class=\"chip\" style=\"background:#58402E\">\u003c/span>  椰林 · 绿黄六色板:麦黄 → 橄榄 → 浅苔 → 苔绿 → 中绿 → 深绿。深绿当主墨、琥珀当主角、浓咖当文字;暗卡上强调改麦黄(峰值 / 最优 / 事故楔形)——深绿在浓咖底上读不出。\u003c/p>\n \u003cp style=\"margin-top:7px\">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 主题 / 4 小队 / 4 簇 / 5 卫星岛 / 每行选项)各走一个色,序数型(漏斗 5 级 / 票量 / 峰值 / 账户数 / 持有量 / 上线周次)走 5 档梯。几何、动画、数据与 lupi-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003c!-- 1 · launch fan -->\n \u003cdiv class=\"card\">\n \u003ch2>Twelve features, fanned out\u003c/h2>\n \u003cdiv class=\"sub\">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since\u003c/div>\n \u003csvg id=\"fan\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">LAUNCH FAN · PALM · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- 4 · dot cascade on dark -->\n \u003cdiv class=\"card dark\">\n \u003ch2>What breaks, stacked and ranked\u003c/h2>\n \u003cdiv class=\"sub\">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending\u003c/div>\n \u003csvg id=\"cascade\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DOT CASCADE · PALM · INCIDENT LOG\u003c/div>\n \u003c/div>\n\n \u003c!-- 2 · barcode lollipop, wide + editorial text column -->\n \u003cdiv class=\"card wide\">\n \u003cdiv class=\"split\">\n \u003cdiv>\n \u003ch2>Ninety days as a barcode\u003c/h2>\n \u003cdiv class=\"sub\">peak concurrent users, daily · Apr – Jun 2026\u003c/div>\n \u003cdiv class=\"note\">Every hairline is a day, whether or not anything\n happened in it. The dot marks the day's peak; the stem below it is\n just gravity. Read the field, not the numbers — the season has a\n texture before it has a value.\u003c/div>\n \u003cdiv class=\"legend\">\n ●  WEEKDAY PEAK\u003cbr>\n ○  WEEKEND PEAK\u003cbr>\n │  ONE CALENDAR DAY\u003cbr>\n ◉  TOP-3, LABELED\n \u003c/div>\n \u003c/div>\n \u003cdiv>\n \u003csvg id=\"barcode\" viewBox=\"0 0 800 300\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:340px\">\u003c/svg>\n \u003cdiv class=\"src\">BARCODE LOLLIPOP · PALM · LIVE OPS\u003c/div>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003c!-- 3 · arc bubble matrix -->\n \u003cdiv class=\"card\">\n \u003ch2>Eight products land in twelve cities\u003c/h2>\n \u003cdiv class=\"sub\">bubble = paying accounts · each row bows like a horizon\u003c/div>\n \u003csvg id=\"arcmatrix\" viewBox=\"0 0 430 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">ARC MATRIX · PALM · SALES GEO\u003c/div>\n \u003c/div>\n\n \u003c!-- 5 · radial convergence -->\n \u003cdiv class=\"card\">\n \u003ch2>48 requests pull toward five themes\u003c/h2>\n \u003cdiv class=\"sub\">every rim node = a feature request · lines bundle into the theme that claimed it\u003c/div>\n \u003csvg id=\"converge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RADIAL CONVERGENCE · PALM · FEEDBACK BOARD\u003c/div>\n \u003c/div>\n\n \u003c!-- 6 · cluster field, wide -->\n \u003cdiv class=\"card wide\">\n \u003ch2>The contributor field\u003c/h2>\n \u003cdiv class=\"sub\">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution\u003c/div>\n \u003csvg id=\"clusters\" viewBox=\"0 0 840 320\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:360px\">\u003c/svg>\n \u003cdiv class=\"src\">CLUSTER FIELD · PALM · CONTRIBUTOR GRAPH\u003c/div>\n \u003c/div>\n\n\n\n \u003c!-- 1 · brand spectrum -->\n \u003cdiv class=\"card\">\n \u003ch2>Where the brand sits\u003c/h2>\n \u003cdiv class=\"sub\">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors\u003c/div>\n \u003csvg id=\"spectrum\" viewBox=\"0 0 460 300\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">BRAND SPECTRUM · PALM · BRAND SURVEY\u003c/div>\n \u003c/div>\n\n \u003c!-- 2 · dotty matrix -->\n \u003cdiv class=\"card\">\n \u003ch2>Four squads, stacked in space\u003c/h2>\n \u003cdiv class=\"sub\">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed\u003c/div>\n \u003csvg id=\"dotty\" viewBox=\"0 0 400 372\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:340px\">\u003c/svg>\n \u003cdiv class=\"src\">DOTTY MATRIX · PALM · SPRINT BOARD\u003c/div>\n \u003c/div>\n\n \u003c!-- 3 · ticket almanac, wide -->\n \u003cdiv class=\"card wide\">\n \u003ch2>Eight years of tickets, one almanac\u003c/h2>\n \u003cdiv 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\u003c/div>\n \u003csvg id=\"almanac\" viewBox=\"0 0 840 400\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:420px\">\u003c/svg>\n \u003cdiv class=\"src\">BUBBLE ALMANAC · PALM · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- 4 · deploy patchwork -->\n \u003cdiv class=\"card\">\n \u003ch2>A quarter of deploys, overlaid\u003c/h2>\n \u003cdiv class=\"sub\">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up\u003c/div>\n \u003csvg id=\"patchwork\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RADIAL PATCHWORK · PALM · DEPLOY LOG\u003c/div>\n \u003c/div>\n\n \u003c!-- 6 · trend lineage -->\n \u003cdiv class=\"card\">\n \u003ch2>Features rise, fall, come back\u003c/h2>\n \u003cdiv class=\"sub\">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today\u003c/div>\n \u003csvg id=\"lineage\" viewBox=\"0 0 400 340\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TREND LINEAGE · PALM · PRODUCT HISTORY\u003c/div>\n \u003c/div>\n\n \u003c!-- 8 · type colonnade -->\n \u003cdiv class=\"card\">\n \u003ch2>Forty-four repos, ten owners\u003c/h2>\n \u003cdiv class=\"sub\">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew\u003c/div>\n \u003csvg id=\"colonnade\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TYPE COLONNADE · PALM · CODE OWNERSHIP\u003c/div>\n \u003c/div>\n\n \u003c!-- 9 · funnel hourglass -->\n \u003cdiv class=\"card\">\n \u003ch2>The funnel, poured\u003c/h2>\n \u003cdiv class=\"sub\">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort\u003c/div>\n \u003csvg id=\"hourglass\" viewBox=\"0 0 400 330\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HOURGLASS STREAM · PALM · GROWTH\u003c/div>\n \u003c/div>\n\n \u003c!-- 14 · hundred field -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge pair\">小数据 · 单位分解\u003c/span>\n \u003ch2>A hundred of us, four minds\u003c/h2>\n \u003cdiv class=\"sub\">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey\u003c/div>\n \u003csvg id=\"hundredfield\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HUNDRED FIELD · PALM · ATTITUDE SURVEY\u003c/div>\n \u003c/div>\n\n \u003c!-- 15 · ballot tally -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge pair\">小数据 · 单位分解\u003c/span>\n \u003ch2>What they fear, tick by tick\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one respondent in a hundred · inked = picked it · they could pick several\u003c/div>\n \u003csvg id=\"ballottally\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">BALLOT TALLY · PALM · WORKPLACE SURVEY\u003c/div>\n \u003c/div>\n\n\n \u003c!-- L16 · matrix heat -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 编辑型 · 矩阵热力\u003c/span>\n \u003ch2>Which features get used together\u003c/h2>\n \u003cdiv class=\"sub\">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair\u003c/div>\n \u003csvg id=\"matheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">MATRIX HEAT · PALM · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- L17 · calendar heat -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 编辑型 · 日历热力\u003c/span>\n \u003ch2>A year of deploys, day by day\u003c/h2>\n \u003cdiv class=\"sub\">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025\u003c/div>\n \u003csvg id=\"calheat\" viewBox=\"0 0 840 232\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:260px\">\u003c/svg>\n \u003cdiv class=\"src\">CALENDAR HEAT · PALM · DEPLOY LOG\u003c/div>\n \u003c/div>\n\n \u003c!-- L19 · ridgeline -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 编辑型 · RIDGELINE\u003c/span>\n \u003ch2>Five pipelines, five tempos\u003c/h2>\n \u003cdiv class=\"sub\">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak\u003c/div>\n \u003csvg id=\"ridge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RIDGELINE · PALM · DATA PLATFORM\u003c/div>\n \u003c/div>\n\n \u003c!-- L20 · parallel coordinates -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 编辑型 · 平行坐标\u003c/span>\n \u003ch2>Twelve products, four dimensions\u003c/h2>\n \u003cdiv class=\"sub\">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere\u003c/div>\n \u003csvg id=\"parallel\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PARALLEL COORDS · PALM · PRODUCT PANEL\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── PALM 角色 token(结构灰一律为文字色的透明档,不引入新色相)──\nconst BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)';\nconst FLOOR='rgba(88,64,46,.24)',QUIET='rgba(88,64,46,.15)',TRACK='rgba(88,64,46,.12)',GRID='rgba(88,64,46,.16)';\nconst DATA='#43593B',DATA2='#58402E',HERO='#D4A017';\nconst FAINTDATA='#ACAD79',BEAD='#77835A',HALO='rgba(240,239,235,.92)';\nconst HERODK='#F2D17E'; // 深色卡上的强调色(HERO 在深底上可能读不出)\nconst DMID='rgba(240,239,235,.62)',DQ='rgba(240,239,235,.24)'; // 深色卡(L4 dot cascade)上的浅墨 / 静音档\nconst SER=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79','#58402E']; // 分类色:一图之内每个系列 / 每行 / 每簇一个色\nconst RING='rgba(88,64,46,.30)'; // 散点的铅笔细边:让浅色号在纸上有形,不用改色号\nconst RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B']; // 5 档序数梯(浅→重):漏斗 / 票量 / 峰值 / 持有量\nconst RAMPDK=['rgba(240,239,235,.26)','rgba(240,239,235,.42)','rgba(240,239,235,.60)','rgba(240,239,235,.84)','#F2D17E']; // 深色卡上的 5 档梯,末档落在强调色\nconst HEROMODE=false; // 主角模式:launch fan 的头号功能是否也整体走 HERO\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic\nconst D2R=Math.PI/180;\nconst pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];\nconst sect=(cx,cy,r0,r1,a0,a1)=>{\n const big=a1-a0>180?1:0;\n const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);\n const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);\n return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;\n};\n// unified reveal: draw when scrolled into view; click replays\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>{n.innerHTML='';fn(n)};\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ 1 · launch fan ════\n// spokes sorted by launch week → the big dots cascade along a diagonal arc\n(()=>{\nconst FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];\nconst LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19]; // week of launch\nconst CX=40,CY=304,rOf=w=>56+w*9.7;\nobsReveal('fan',s=>{\n const ang=i=>-84+i*(76/11);\n // dotted week guides\n [5,10,15,20].forEach((w,gi)=>{\n const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);\n el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',\n stroke:QUIET,'stroke-width':1,'stroke-dasharray':'2 5',\n class:'fade',style:`animation-delay:${gi*.1}s`});\n const [lx,ly]=pol(CX,CY,r,-87.5);\n txt(s,{x:lx,y:ly,'font-size':6.5,fill:FAINT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);\n });\n FEAT.forEach((name,i)=>{\n const a=ang(i),L=LAUNCH[i];\n const mau=Math.round(48-i*3+rnd(i+1,3)*10);\n const col=RAMP[4-Math.min(4,Math.floor((L-1)/4))]; // 上线越早 = 越重\n // hairline spoke from launch to rim\n const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);\n el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:QUIET,'stroke-width':.7,\n class:'fade',style:`animation-delay:${.15+i*.05}s`});\n // weekly texture dots\n for(let w=L+1;w\u003c=20;w++){\n const [x,y]=pol(CX,CY,rOf(w),a);\n el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:col,opacity:.55,\n class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});\n }\n // launch dot, sized by MAU\n const [bx,by]=pol(CX,CY,rOf(L),a);\n const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:(HEROMODE&&i===0)?HERO:col,\n class:'pop',style:`animation-delay:${.15+i*.07}s`});\n tip(big,`${name} — launched W${L} · ${mau}k MAU`);\n // rim label along the spoke\n const [tx,ty]=pol(CX,CY,rOf(20)+9,a);\n txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:LAB,\n transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',\n class:'fade',style:`animation-delay:${.3+i*.05}s`},name);\n });\n});\n})();\n\n// ════ 4 · dot cascade on dark ════\n(()=>{\nconst CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];\nconst COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];\nobsReveal('cascade',s=>{\n const x=i=>34+i*22.2, base=i=>272-i*7.5;\n const worst=COUNT.indexOf(Math.max(...COUNT)); // 主角:最大根因\n // dotted diagonal baseline\n el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,\n stroke:DQ,'stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});\n CAUSE.forEach((name,i)=>{\n const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);\n const bd=Math.min(4,Math.floor(COUNT[i]/6)); // 事故量分 5 档\n for(let k=0;k\u003cn-1;k++)\n el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:RAMPDK[bd],opacity:.95,\n class:'pop',style:`animation-delay:${i*.05+k*.03}s`});\n const topY=by-12-(n-1)*8.8;\n const top=el(s,'circle',{cx:bx,cy:topY,r:i===worst?5.6:4.6,\n fill:i===worst?HERODK:RAMPDK[Math.min(4,bd+1)],\n class:'pop',style:`animation-delay:${i*.05+n*.03}s`});\n tip(top,`${name} — ${COUNT[i]} incidents`);\n txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:i===worst?HERODK:BG,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);\n txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:DMID,'text-anchor':'end',\n transform:`rotate(-90 ${bx} ${by+12})`,\n class:'fade',style:`animation-delay:${.1+i*.05}s`},name);\n });\n});\n})();\n\n// ════ 2 · barcode lollipop ════\n(()=>{\nconst N=90;\nconst val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;\nobsReveal('barcode',s=>{\n // the barcode: one hairline per day, full height\n for(let d=0;d\u003cN;d++){\n const x=18+d*8.53;\n el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,\n class:'fade',style:`animation-delay:${d*.004}s`});\n }\n const vs=Array.from({length:N},(_,d)=>val(d));\n // top-3 peaks, kept at least 6 days apart so labels never collide\n const top3=[];\n for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){\n if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);\n if(top3.length===3)break;\n }\n const vlo=Math.min(...vs),vhi=Math.max(...vs);\n // 峰值分 4 档,最浅那档不用(5 月的低谷也得看得见)\n const vband=v=>1+Math.min(3,Math.floor((v-vlo)/(vhi-vlo+1e-9)*4));\n for(let d=0;d\u003cN;d++){\n const x=18+d*8.53,v=vs[d],y=252-v*.75;\n const weekend=d%7===5||d%7===6,col=RAMP[vband(v)];\n const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);\n el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:col,'stroke-width':1.5,\n class:'fade',style:`animation-delay:${.3+d*.008}s`});\n const big=top3.includes(d);\n const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,\n fill:weekend?BG:(big?HERO:col),stroke:big?HERO:col,'stroke-width':weekend?1.4:0,\n class:'pop',style:`animation-delay:${.3+d*.008}s`});\n tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);\n if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:TXT,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');\n }\n [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>\n txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUT,\n 'letter-spacing':'.12em',class:'fade'},m));\n});\n})();\n\n// ════ 3 · arc bubble matrix ════\n(()=>{\nconst PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];\nconst CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];\nconst W=[9,8,7,7,6,5,5,4,4,3,3,2]; // market weight\nobsReveal('arcmatrix',s=>{\n const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);\n const v=(i,j)=>{\n if(rnd(i*13+1,j*7+3)\u003c.09)return 0; // a few true absences\n const age=1-i*.085; // older rows reach further\n return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));\n };\n // find top-4 cells for labels\n const all=[];\n PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));\n const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);\n PROD.forEach((p,i)=>{\n // the horizon arc\n const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');\n el(s,'path',{d,fill:'none',stroke:QUIET,'stroke-width':1,pathLength:1,\n class:'draw',style:`animation-delay:${i*.08}s`});\n txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.08}s`},p);\n CITY.forEach((c,j)=>{\n const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);\n if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:FLOOR,\n class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}\n const fill=RAMP[Math.min(4,Math.floor(vv/8))]; // 账户数 5 档\n const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,\n class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});\n tip(dot,`${p} · ${c} — ${vv} accounts`);\n if(top.includes(i*100+j))\n txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:TXT,\n 'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);\n });\n });\n CITY.forEach((c,j)=>{\n const x=colX(j),y=74+dy(j)-24;\n txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUT,'letter-spacing':'.08em',\n transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);\n });\n});\n})();\n\n// ════ 5 · radial convergence ════\n(()=>{\nconst N=48,CX=200,CY=162,R=116;\nconst THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];\nobsReveal('converge',s=>{\n // hub assignment: contiguous blocks with a little organic leakage\n const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));\n const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];\n const hubPt=h=>pol(CX,CY,34,THEME[h][2]);\n const counts=[0,0,0,0,0];\n for(let i=0;i\u003cN;i++)counts[hubOf(i)]++;\n for(let i=0;i\u003cN;i++){\n const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);\n const h=hubOf(i),[hx,hy]=hubPt(h);\n const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;\n const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;\n el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,\n fill:'none',stroke:SER[h],'stroke-width':.9,opacity:.62,pathLength:1,\n class:'draw',style:`animation-delay:${i*.018}s`});\n el(s,'circle',{cx:px,cy:py,r:1.6,fill:SER[h],\n class:'pop',style:`animation-delay:${i*.018}s`});\n // tiny rim code, flipped on the left half so it stays readable\n const flip=deg>90&°\u003c270,[lx,ly]=pol(CX,CY,R+7,deg);\n txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUT,\n 'text-anchor':flip?'end':'start','dominant-baseline':'middle',\n transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,\n class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));\n }\n THEME.forEach(([name,,deg],h)=>{\n const [hx,hy]=hubPt(h);\n const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:counts[h]===Math.max(...counts)?HERO:SER[h],\n class:'pop',style:`animation-delay:${.5+h*.08}s`});\n tip(hub,`${name} — ${counts[h]} requests`);\n // theme label parked outside the rim, past the code ring\n const [tx,ty]=pol(CX,CY,R+34,deg);\n txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',\n 'dominant-baseline':'middle','letter-spacing':'.08em',\n style:`animation-delay:${.6+h*.08}s`,\n class:'fade'},`${name} · ${counts[h]}`);\n // hairline tying the label to its hub, skimming past the rim\n const [gx,gy]=pol(CX,CY,R+22,deg);\n el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:TRACK,'stroke-width':.7,\n 'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});\n });\n});\n})();\n\n// ════ 6 · cluster field ════\n(()=>{\nconst SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];\nobsReveal('clusters',s=>{\n const HX=300,HY=172;\n // dotted cross-contribution paths, hub → each island\n SAT.forEach(([x,y],ci)=>{\n const mx=(HX+x)/2,my=(HY+y)/2-30;\n el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',\n stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,\n class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});\n });\n // main hub: 80 golden-angle spokes\n for(let i=0;i\u003c80;i++){\n const a=i*137.508,len=28+rnd(i+1,7)*88;\n const [x,y]=pol(HX,HY,len,a);\n el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,pathLength:1,\n class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:SER[0],opacity:.9,\n class:'pop',style:`animation-delay:${.15+i*.012}s`});\n tip(dot,`core contributor #${i+1}`);\n }\n el(s,'circle',{cx:HX,cy:HY,r:7,fill:HERO,class:'pop'});\n txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:HERO,'text-anchor':'middle',\n 'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,\n class:'fade'},'CORE · 80');\n // satellite islands\n SAT.forEach(([x,y,name,n],ci)=>{\n const col=RAMP[1+Math.round((n-9)/13*3)]; // 岛越大越重(22→4 档,9→1 档)\n el(s,'circle',{cx:x,cy:y,r:3.5,fill:col,\n class:'pop',style:`animation-delay:${.9+ci*.12}s`});\n for(let k=0;k\u003cn;k++){\n const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;\n const [dx,dy]=pol(x,y,rr,a);\n if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:TRACK,'stroke-width':.5,\n class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});\n el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:col,\n stroke:RING,'stroke-width':.4,\n class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});\n }\n txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',\n 'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,\n class:'fade'},`${name} · ${n}`);\n });\n});\n})();\n\n// ════ 1 · brand spectrum ════\n// paired opposites; the pale ribbon threads our position down the page\n(()=>{\nconst ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];\nconst US=[.74,.80,.40,.22]; // 0 = left trait, 1 = right trait\nconst COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];\nconst X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);\nobsReveal('spectrum',s=>{\n // the ribbon first, underneath everything\n let d=`M${px(US[0])} ${Y(0)}`;\n for(let i=1;i\u003c4;i++)\n d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;\n el(s,'path',{d,fill:'none',stroke:HALO,'stroke-width':30,'stroke-linecap':'round',\n 'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',\n style:'animation-duration:1.4s'});\n ROWS.forEach(([a,b],i)=>{\n const y=Y(i);\n el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:FAINT,'stroke-width':1,\n class:'fade',style:`animation-delay:${i*.08}s`});\n [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:FAINT,'stroke-width':1,\n class:'fade',style:`animation-delay:${i*.08}s`}));\n txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',\n 'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);\n txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,\n 'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);\n COMP[i].forEach((t,k)=>{\n const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:SER[(i+1)%4],\n stroke:RING,'stroke-width':.6,\n class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});\n tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);\n });\n const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:HERO,\n class:'pop',style:`animation-delay:${.5+i*.1}s`});\n tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);\n });\n txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:FAINT,'letter-spacing':'.12em',\n class:'fade',style:'animation-delay:.9s'},'● US ● COMPETITORS A · B · C');\n});\n})();\n\n// ════ 2 · dotty matrix — isometric stacked dot planes ════\n// pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:\n// each plane reads as its own quiet grid instead of a smudge\n(()=>{\nconst SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];\nconst SHADE=[SER[3],SER[2],SER[1],SER[0]]; // 底层→顶层,各队一色\nconst P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];\nobsReveal('dotty',s=>{\n for(let k=0;k\u003c4;k++){\n // plane slab: paper fill so upper decks occlude lower ones\n const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];\n el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',\n fill:BG,'fill-opacity':.96,stroke:GRID,'stroke-width':.9,\n class:'fade',style:`animation-delay:${k*.16}s`});\n for(let r=0;r\u003c6;r++)for(let c=0;c\u003c6;c++){\n const load=rnd(k*37+r*6+c+1,k+2);\n const v=load\u003c.3?0:Math.round(load*12); // sparser: silence is part of the texture\n const [x,y]=P(c,r,k);\n if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:FLOOR,\n class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}\n const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],\n stroke:RING,'stroke-width':.45,\n class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});\n tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);\n }\n // label off the plane's right corner, tied by a short hairline\n const [cxr,cyr]=P(5.8,-.8,k);\n el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:FAINT,'stroke-width':.8,\n class:'fade',style:`animation-delay:${.2+k*.16}s`});\n txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?TXT:MUT,\n 'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);\n }\n});\n})();\n\n// ════ 3 · bubble almanac ════\n// the vintage-almanac feel lives in three things: ledger-paper hairlines\n// under everything, bubbles big enough to collide, and marginalia. do all three.\n(()=>{\nconst AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];\nconst BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025}; // beta until this year\nconst 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};\nobsReveal('almanac',s=>{\n const rowY=i=>62+i*34,colX=j=>132+j*57;\n // ledger paper: hairlines every 6.8px, edge to edge, under everything\n for(let y=40;y\u003c=318;y+=6.8)\n el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:QUIET,'stroke-width':.5,\n class:'fade',style:`animation-delay:${(y-40)*.001}s`});\n const vals=[];\n for(let i=0;i\u003c8;i++){\n const yr=2019+i;\n el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:GRID,'stroke-width':.9,\n class:'fade',style:`animation-delay:${i*.04}s`});\n txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:MUT,'text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.04}s`},yr);\n AREAS.forEach((a,j)=>{\n if(yr\u003cBORN[a])return;\n const age=yr-BORN[a];\n // steep spread: quiet areas stay pinpricks, busy ones balloon\n const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));\n vals.push([Math.max(1,v),i,j,a,yr]);\n });\n }\n // draw small over large so the giants sit underneath as washes\n vals.sort((a,b)=>b[0]-a[0]);\n const top3=vals.slice(0,3);\n // hand-drawn circle: radius wobbles around the rim on two slow sine waves,\n // so every bubble is its own slightly-off print — never a compass circle\n const blob=(x,y,r,seed)=>{\n const n=Math.max(14,Math.round(r*1.6)),pts=[];\n for(let t=0;t\u003cn;t++){\n const a=t/n*Math.PI*2;\n const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;\n pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);\n }\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let t=0;t\u003cn;t++){\n const p=pts[t],q=pts[(t+1)%n];\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)}`;\n }\n return d+' Z';\n };\n vals.forEach(([v,i,j,a,yr],k)=>{\n const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10, // jitter off the gridline\n y=rowY(i),r=Math.sqrt(v)*3.8; // big: rows are 34 apart, giants reach ~28 and collide\n const beta=BETA[a]&&yr\u003c=BETA[a];\n const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});\n const seed=i*12+j,isTop=top3.includes(vals[k]); // 主角:票量 top-3\n const col=RAMP[Math.min(4,Math.floor(v/12))]; // 票量 5 档\n const dot=el(g,'path',beta\n ?{d:blob(x,y,r,seed),fill:'none',stroke:DATA2,'stroke-width':1.2,'stroke-dasharray':'3 3'}\n :{d:blob(x,y,r,seed),fill:col,'fill-opacity':.16+rnd(i+2,j+4)*.14,\n stroke:isTop?HERO:col,'stroke-opacity':isTop?.95:.5+rnd(i+5,j+7)*.38,\n 'stroke-width':isTop?1.7:.8+rnd(i+3,j+11)*1});\n tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);\n // escalation core: small wobbly blot, nudged off-center like a press mark\n if(!beta){\n const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;\n el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:isTop?HERO:TXT});\n }\n if(isTop)\n txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:HERO,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},v*10);\n });\n AREAS.forEach((a,j)=>{\n const x=colX(j);\n txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUT,'letter-spacing':'.1em',\n transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);\n });\n // marginalia: two annotations with pointer hairlines, like the original's notes\n 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}`,\n fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});\n txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:LAB,'font-style':'italic',\n class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');\n 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}`,\n fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});\n txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:LAB,'font-style':'italic',\n class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');\n // footer strip: important events, the almanac's timeline shelf\n const evY=356;\n el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:FAINT,'stroke-width':.8,\n class:'fade',style:'animation-delay:1.2s'});\n txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:MUT,'letter-spacing':'.16em',\n class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');\n [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]\n .forEach(([yr,note],k)=>{\n const x=90+k*196;\n el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:MUT,'stroke-width':1,\n class:'fade',style:`animation-delay:${1.3+k*.08}s`});\n txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:TXT,\n class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);\n txt(s,{x,y:evY+15,'font-size':7,fill:MUT,\n class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);\n });\n});\n})();\n\n// ════ 4 · radial patchwork ════\n(()=>{\nconst CX=200,CY=168,N=46;\nobsReveal('patchwork',s=>{\n // 24h rim ticks\n for(let h=0;h\u003c96;h++){\n const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);\n el(s,'line',{x1,y1,x2,y2,stroke:FAINT,'stroke-width':h%4===0?1:.5,\n class:'fade',style:`animation-delay:${h*.006}s`});\n }\n [0,6,12,18].forEach(h=>{\n const [x,y]=pol(CX,CY,157,-90+h*15);\n txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',\n class:'fade'},String(h).padStart(2,'0'));\n });\n // the patchwork: translucent wedges pile into ink\n const wedges=[];\n for(let i=0;i\u003cN;i++){\n // deploys cluster around 11h and 16h, thin overnight\n const peak=rnd(i+1,2)>.5?11:16;\n const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;\n const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;\n const r1=34+rnd(i+1,5)*100;\n wedges.push([a0,sw,r1,hour,i]);\n }\n wedges.forEach(([a0,sw,r1,hour,i])=>{\n const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:RAMP[Math.min(4,Math.floor(hour/4.8))],\n 'fill-opacity':.17+rnd(i+1,6)*.15,class:'fade',\n style:`animation-delay:${.2+i*.022}s`});\n 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`);\n });\n // three outlined wedges: the ones that paged somebody\n [4,17,31].forEach((i,k)=>{\n const [a0,sw,r1]=wedges[i];\n const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',\n stroke:HERO,'stroke-width':1.5,class:'fade',style:`animation-delay:${1.2+k*.15}s`});\n tip(w,`deploy #${i+1} — triggered an incident`);\n });\n el(s,'circle',{cx:CX,cy:CY,r:3,fill:DATA,class:'pop',style:'animation-delay:1.3s'});\n txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');\n});\n})();\n\n// ════ 6 · trend lineage ════\n(()=>{\n// [name, events [year, type f|o], aliveToday]\nconst TR=[\n ['Dark mode',[[2017,'f'],[2022,'o']],true],\n ['Kanban',[[2016,'f']],true],\n ['Wikis',[[2016,'f'],[2019,'o']],false],\n ['Slash cmds',[[2018,'f']],true],\n ['Templates',[[2019,'f'],[2024,'o']],true],\n ['Inbox',[[2020,'f']],false],\n ['Live cursors',[[2020,'f'],[2023,'o']],true],\n ['AI drafts',[[2023,'f'],[2025,'o']],true],\n ['Voice notes',[[2021,'f']],false],\n ['Offline',[[2018,'f'],[2026,'o']],true],\n];\nconst Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;\n// 主角:最近一次有动作的功能线 —— 标题里的 \"come back\"\nconst HC=TR.map((r,i)=>[r[1][r[1].length-1][0],i]).sort((a,b)=>b[0]-a[0])[0][1];\nobsReveal('lineage',s=>{\n for(let yr=Y0;yr\u003c=2026;yr++){\n el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:QUIET,'stroke-width':.8,\n class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});\n if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'end',\n class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);\n }\n TR.forEach(([name,ev,alive],c)=>{\n const x=colX(c),hero=c===HC;\n const lastYr=ev[ev.length-1][0];\n // 越近期动过 = 越重;已死的走最浅一档(虚线本身已经在说休眠)\n const col=hero?HERO:RAMP[alive?Math.min(4,Math.max(2,Math.round((lastYr-2016)/10*4))):1];\n // segments between events: solid while fresh, dashed once dormant\n for(let k=0;k\u003cev.length-1;k++){\n const gap=ev[k+1][0]-ev[k][0];\n el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,\n stroke:col,'stroke-width':hero?1.8:1.4,\n 'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,\n class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});\n }\n const last=ev[ev.length-1][0];\n if(alive)\n el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:col,'stroke-width':hero?1.8:1.4,\n pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});\n ev.forEach(([yr,t])=>{\n const node=el(s,'circle',t==='f'\n ?{cx:x,cy:yY(yr),r:5.5,fill:col}\n :{cx:x,cy:yY(yr),r:5.5,fill:BG,stroke:col,'stroke-width':1.6});\n node.setAttribute('class','pop');\n node.style.animationDelay=(.35+c*.07)+'s';\n tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);\n });\n // terminal dot: alive lands on today's baseline, dead stops where it stopped\n el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,\n fill:col,class:'pop',style:`animation-delay:${.6+c*.07}s`});\n const ly=yY(2026)+26;\n txt(s,{x,y:ly,'font-size':6.8,'font-weight':hero?800:600,fill:hero?HERO:(alive?LAB:FAINT),\n transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',\n class:'fade',style:`${hero?`paint-order:stroke;stroke:${BG};stroke-width:3px;`:''}animation-delay:${.7+c*.05}s`},name);\n });\n});\n})();\n\n// ════ 8 · type colonnade ════\n(()=>{\nconst PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];\nconst SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];\nconst TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];\nobsReveal('colonnade',s=>{\n const N=44;\n const teamY=j=>36+j*28,repoY=i=>16+i*6.7;\n const counts=Array(10).fill(0);\n const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard\n for(let i=0;i\u003cN;i++)counts[owner(i)]++;\n for(let i=0;i\u003cN;i++){\n const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');\n const yi=repoY(i),j=owner(i),yj=teamY(j);\n const col=RAMP[Math.min(4,Math.floor(counts[j]/3))]; // 团队持有量 5 档\n txt(s,{x:112,y:yi+2,'font-size':5,fill:MUT,'text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.012}s`},name);\n el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:col,\n class:'fade',style:`animation-delay:${i*.012}s`});\n el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',\n stroke:col,'stroke-width':.8,opacity:.78,pathLength:1,\n class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});\n }\n TEAM.forEach((t,j)=>{\n const y=teamY(j);\n el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:RAMP[Math.min(4,Math.floor(counts[j]/3))],\n class:'pop',style:`animation-delay:${.6+j*.05}s`});\n const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:LAB,\n class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);\n tip(lab,`${t} owns ${counts[j]} repos`);\n });\n});\n})();\n\n// ════ 9 · hourglass stream ════\n(()=>{\nconst STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];\nobsReveal('hourglass',s=>{\n const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;\n // 主角:通过率最低的一级 —— 漏得最狠的地方\n const PCT=STAGE.slice(1).map((n,k)=>Math.round(n[1]/STAGE[k][1]*100));\n const leak=PCT.indexOf(Math.min(...PCT));\n STAGE.forEach(([name,c],k)=>{\n const y=sy(k),hw=w(c)/2;\n // barcode strip: one tick ≈ 40 people\n const n=Math.round(c/40);\n for(let t=0;t\u003cn;t++){\n const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;\n el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:RAMP[k],'stroke-width':1.1,\n opacity:.6+rnd(t+2,k+5)*.4,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});\n }\n // threads trickling to the next strip\n if(k\u003c4){\n const hw1=w(STAGE[k+1][1])/2;\n for(let t=0;t\u003c34;t++){\n const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;\n const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;\n el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,\n fill:'none',stroke:FAINTDATA,'stroke-width':.7,opacity:.45,pathLength:1,\n class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});\n }\n // conversion between stages, parked in the left margin\n const pct=Math.round(STAGE[k+1][1]/c*100);\n txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':k===leak?10.5:8.5,'font-weight':800,\n fill:k===leak?HERO:MUT,\n class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');\n txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:k===leak?HERO:FAINT,\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');\n }\n // stage label, tied to the strip edge\n el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:GRID,'stroke-width':.8,\n class:'fade',style:`animation-delay:${.3+k*.12}s`});\n txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:LAB,\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);\n txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:TXT,\n class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());\n });\n});\n})();\n\n// ════ 14 · hundred field ════\n// small data, unit-decomposed: four shares of 100% become 100 people.\n// phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.\n(()=>{\nconst SEG=[['CHARGED',41,SER[0]],['TORN',35,SER[1]],['ADRIFT',12,SER[2]],['AVERSE',12,SER[3]]];\nconst POS=[[132,140],[276,116],[186,252],[322,238]];\nobsReveal('hundredfield',s=>{\n // dashed constellation hairlines tying the cluster cores\n [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{\n el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],\n stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',\n class:'fade',style:`animation-delay:${.9+k*.1}s`});\n });\n SEG.forEach(([name,v,shade],ci)=>{\n const [cx,cy]=POS[ci];\n let edge=0;\n for(let k=0;k\u003cv;k++){\n const a=k*137.508+ci*55;\n const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;\n edge=Math.max(edge,rr);\n const [x,y]=pol(cx,cy,rr,a);\n // hairline spoke for every 5th person, core to dot\n if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,\n class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,\n stroke:RING,'stroke-width':.4,\n class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});\n tip(dot,`${name} — one of ${v} in 100`);\n }\n el(s,'circle',{cx,cy,r:2.4,fill:TXT,class:'pop',style:`animation-delay:${ci*.14}s`});\n txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',\n 'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,\n class:'fade'},`${name} · ${v}`);\n });\n txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');\n});\n})();\n\n// ════ 15 · ballot tally ════\n// multi-select survey (each option independent, 0–100): every option is a row of\n// 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.\n(()=>{\nconst ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];\nobsReveal('ballottally',s=>{\n const VS=ASK.map(a=>a[1]),topI=VS.indexOf(Math.max(...VS)); // 主角:得票最高项\n ASK.forEach(([name,v],i)=>{\n const base=78+i*62;\n txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:LAB,\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);\n el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,\n class:'fade',style:`animation-delay:${i*.1}s`});\n for(let k=0;k\u003c100;k++){\n const x=28+k*3.44,picked=k\u003cv;\n const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;\n el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,\n stroke:picked?(i===topI?HERO:SER[i]):FLOOR,'stroke-width':picked?1.3:.6,\n class:'fade',style:`animation-delay:${i*.1+k*.006}s`});\n if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:FAINT,\n class:'fade',style:`animation-delay:${i*.1+k*.006}s`});\n }\n const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':i===topI?12.5:11,'font-weight':800,\n fill:i===topI?HERO:TXT,\n style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,\n class:'fade'},v);\n tip(lab,`${v} of 100 picked this — they could pick several`);\n });\n txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');\n});\n})();\n\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ L16 · matrix heat ════\n// 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,\n// 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。\n// 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,\n// 值是占比/强度而不是可数件数。\n(()=>{\nconst FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];\nconst v=(i,j)=>{\n if(i===j)return -1;\n const a=Math.min(i,j),b=Math.max(i,j);\n const pop=(8-a)/8*(8-b)/8; // 头部功能重合度高\n const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);\n return Math.round(raw);\n};\nconst shade=t=>t>36?INK:t>24?'#77835A':t>14?'#929960':t>6?'#ACAD79':'#F2D17E';\nobsReveal('matheat',s=>{\n const X0=118,Y0=70,P=26,C=22;\n let max=0,mi=0,mj=0;\n for(let i=0;i\u003c8;i++)for(let j=0;j\u003c8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}\n FEAT.forEach((f,i)=>{\n txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'rgba(88,64,46,.72)','text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);\n const cy=Y0-10;\n txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',\n transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);\n });\n for(let i=0;i\u003c8;i++)for(let j=0;j\u003c8;j++){\n const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;\n if(t\u003c0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'rgba(88,64,46,.15)',\n class:'fade',style:`animation-delay:${d}s`});continue}\n if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'rgba(88,64,46,.15)',\n class:'pop',style:`animation-delay:${d}s`});continue}\n const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),\n class:'pop',style:`animation-delay:${d}s`});\n tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);\n if(i===mi&&j===mj){\n el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',\n stroke:'#D4A017','stroke-width':1.8,'stroke-dasharray':'2 3',\n class:'fade',style:'animation-delay:.9s'});\n txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},t);\n }\n }\n // shade legend, countable buckets\n const LG=[['#F2D17E','1–6'],['#ACAD79','7–14'],['#929960','15–24'],['#77835A','25–36'],[INK,'37+']];\n LG.forEach(([c,lab],k)=>{\n const x=118+k*54;\n el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});\n txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#929960',\n class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');\n });\n txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#ACAD79','letter-spacing':'.1em',\n class:'fade',style:'animation-delay:1s'},'SHADE = %');\n});\n})();\n\n// ════ L17 · calendar heat ════\n// 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,\n// 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,\n// 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。\n(()=>{\nconst MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];\nconst DAY=['MON','WED','FRI','SUN'];\nconst val=(w,d)=>{\n const wd=d\u003c5;\n const season=1+.55*Math.sin((w-8)/9); // 春秋两个发布季\n if(!wd)return rnd(w*7+d+1,3)>.82?1:0; // 周末基本静默\n const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);\n return Math.round(raw);\n};\nobsReveal('calheat',s=>{\n const X0=56,Y0=42,P=14.8;\n let max=0,mw=0,md=0;\n for(let w=0;w\u003c52;w++)for(let d=0;d\u003c7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}\n DAY.forEach((lab,k)=>{\n txt(s,{x:X0-10,y:Y0+[0,2,4,6][k]*P+3,'font-size':6.5,'font-weight':700,fill:'#929960','text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);\n });\n MON.forEach((m,k)=>{\n const x=X0+Math.round(k*52/12)*P;\n txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',\n class:'fade',style:`animation-delay:${k*.03}s`},m);\n el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'rgba(88,64,46,.32)','stroke-width':1.26,\n class:'fade',style:`animation-delay:${k*.03}s`});\n });\n for(let w=0;w\u003c52;w++)for(let d=0;d\u003c7;d++){\n const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;\n if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'rgba(88,64,46,.15)',\n class:'pop',style:`animation-delay:${delay}s`});continue}\n const r=1.1+Math.sqrt(t)*1.55;\n const dot=el(s,'circle',{cx:x,cy:y,r,\n fill:t>max*.66?INK:t>max*.33?'#77835A':'#ACAD79',\n class:'pop',style:`animation-delay:${delay}s`});\n tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);\n }\n // peak: dashed ring + marginalia hairline\n const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;\n el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:'#D4A017','stroke-width':1.8,\n 'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});\n el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,\n fill:'none',stroke:'#ACAD79','stroke-width':1.26,class:'fade',style:'animation-delay:1.1s'});\n txt(s,{x:px+78,y:py+39,'font-size':7,fill:'rgba(88,64,46,.72)','font-style':'italic',\n class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);\n txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');\n});\n})();\n\n// ════ L19 · ridgeline ════\n// 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成\n// (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上\n// 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。\n(()=>{\nconst 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]];\n// 每行一档绿:越快越深(RAMP6 前五档),行既是类目也是快慢的序\nconst RC=['#43593B','#5A7049','#77835A','#929960','#ACAD79'];\nobsReveal('ridge',s=>{\n const X0=92,X1=372,MAXH=24,N=72,LIFT=54;\n const rowY=i=>96+i*44;\n ROWS.forEach(([name,c,sp],i)=>{\n const y0=rowY(i);\n const dens=Array.from({length:N},(_,k)=>{\n const h=k/(N-1)*MAXH;\n return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));\n });\n const dmax=Math.max(...dens);\n const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);\n const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');\n el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,\n class:'fade',style:`animation-delay:${i*.1}s`});\n pts.forEach(([px,py],k)=>{\n if(k%2)return;\n el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:RC[i],'stroke-width':0.9,\n opacity:.34+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});\n });\n el(s,'path',{d:crest,fill:'none',stroke:RC[i],'stroke-width':1.62,pathLength:1,\n class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});\n el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':1.26,\n class:'fade',style:`animation-delay:${i*.1}s`});\n const pk=dens.indexOf(dmax),[px,py]=pts[pk];\n const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:RC[i],class:'pop',style:`animation-delay:${.6+i*.1}s`});\n tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);\n txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:INK,\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,\n class:'fade'},c.toFixed(1)+'h');\n txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'rgba(88,64,46,.72)','text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);\n });\n const yb=rowY(4);\n [0,6,12,18,24].forEach(h=>{\n const x=X0+h/MAXH*(X1-X0);\n el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'rgba(88,64,46,.24)','stroke-width':1.08,\n class:'fade',style:'animation-delay:.8s'});\n txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.85s'},h+'h');\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'EACH CREST = ONE PIPELINE · DEEPER GREEN = FASTER · DOT = THE PEAK');\n});\n})();\n\n// ════ L20 · parallel coordinates ════\n// 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。\n// 线束的形状就是数据结构(哪两维相关、谁全线领先)。\n// 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。\n(()=>{\nconst DIMS=[['PRICE

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

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

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

,8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];\nconst P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],\n ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],\n ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],\n ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];\nobsReveal('parallel',s=>{\n const AX=[64,156,248,340],TOP=64,BOT=252;\n const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};\n // hero: judged on csat+retention+growth (price is a fact, not a virtue)\n const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;\n const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);\n DIMS.forEach(([name,lo,hi],di)=>{\n el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#BAD6EB','stroke-width':1.8,\n class:'fade',style:`animation-delay:${di*.06}s`});\n [[TOP-8],[BOT+8]].forEach(([yy])=>\n el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#BAD6EB','stroke-width':1.8,\n class:'fade',style:`animation-delay:${di*.06}s`}));\n txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'rgba(8,31,92,.72)','text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);\n txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'rgba(8,31,92,.32)',\n class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);\n txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'rgba(8,31,92,.32)',\n class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);\n });\n P.forEach((p,i)=>{\n const isHero=p===hero;\n const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];\n let d=`M${AX[0]} ${ys[0].toFixed(1)}`;\n for(let k=0;k\u003c3;k++){\n const mx=(AX[k]+AX[k+1])/2;\n d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;\n }\n const ln=el(s,'path',{d,fill:'none',\n stroke:isHero?INK:'#7096D1','stroke-width':isHero?2:.65,\n opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,\n class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});\n tip(ln,`${p[0]} — ${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);\n ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,\n fill:isHero?INK:'#7096D1',opacity:isHero?1:.7,\n class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));\n if(isHero)\n txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:INK,\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},p[0]);\n });\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":62408,"content_sha256":"995425e38fe58be5ac4da3241da8006e7f85e18e4420660f9c25a3a5cff18ac9"},{"filename":"templates/color/maps-palm.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Charts · Maps M1–M2 × PALM\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n:root{--bg:#F0EFEB;--dark:#43593B;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.32);--grid:rgba(88,64,46,.16)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #ACAD79;color:#929960}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:#929960;margin-top:5px;line-height:1.7}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Maps M1–M2 × PALM\u003c/h1>\n \u003cp>椰林绿地图换肤样张。明度表示区域数值;仅在用户明确要求地图时使用。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n \u003c!-- M1 · US Choropleth -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">MAPS · US CHOROPLETH\u003c/span>\n \u003ch2>Sign-ups across the states\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapus\" style=\"height:340px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · PALM · GROWTH GEO\u003c/div>\n \u003c/div>\n\n \u003c!-- M2 · World Choropleth -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">MAPS · WORLD CHOROPLETH\u003c/span>\n \u003ch2>Where the users are\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · shade = monthly active accounts · gray = markets we haven't opened\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapworld\" style=\"height:400px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · PALM · GLOBAL OPS\u003c/div>\n \u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>fn(n);\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ M1 · US choropleth ════\n// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。\n// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角\n// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。\n// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、\n// 纸色描边当州界发丝、无数据州留 rgba(88,64,46,.10)(沉默可见)。\n// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。\n// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线\n// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。\nconst MAP_LEGEND={\n itemWidth:11,itemHeight:11,itemSymbol:'rect',\n textStyle:{fontFamily:'Inter',fontSize:9,color:'#929960'},\n inRange:{color:['#F2D17E','#ACAD79','#929960','#77835A','#43593B']},\n};\n(()=>{\nconst D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,\n 'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,\n 'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,\n 'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,\n 'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,\n 'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,\n 'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};\nfetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-usa',geo,{\n Alaska:{left:-131,top:25,width:15},\n Hawaii:{left:-110,top:28,width:5},\n 'Puerto Rico':{left:-76,top:26,width:2},\n });\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},\n visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-usa',top:10,bottom:48,\n itemStyle:{areaColor:'rgba(88,64,46,.10)',borderColor:PAPER,borderWidth:1.8},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,\n fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),\n }],\n };\n obsReveal('mapus',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\n// ════ M2 · World choropleth ════\n// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,\n// 关键市场(≥39k)直接把数值标在版图上。\n(()=>{\nconst D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,\n 'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,\n 'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,\n 'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,\n 'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,\n 'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,\n 'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,\n 'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};\nconst BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],\n 'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};\nfetch('https://cdn.jsdelivr.net/npm/[email protected]/map/json/world.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-world',geo);\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},\n visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-world',top:4,bottom:36,\n boundingCoords:[[-170,76],[190,-58]], // crop antarctica\n itemStyle:{areaColor:'rgba(88,64,46,.10)',borderColor:PAPER,borderWidth:1.26},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.98}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,\n fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),\n }],\n };\n obsReveal('mapworld',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\naddEventListener('resize',()=>{\n ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":9813,"content_sha256":"a582e1de10001e183316d75ee2ba39275cf29542f6187dc6965462bc2b6f507b"},{"filename":"templates/color/maps-porcelain.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Charts · Maps M1–M2 × PORCELAIN\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n:root{--bg:#F7F2EB;--dark:#081F5C;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.32);--grid:rgba(8,31,92,.16)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #BAD6EB;color:#7096D1}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:#7096D1;margin-top:5px;line-height:1.7}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Maps M1–M2 × PORCELAIN\u003c/h1>\n \u003cp>青瓷蓝地图换肤样张。明度表示区域数值;仅在用户明确要求地图时使用。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n \u003c!-- M1 · US Choropleth -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">MAPS · US CHOROPLETH\u003c/span>\n \u003ch2>Sign-ups across the states\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapus\" style=\"height:340px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · PORCELAIN · GROWTH GEO\u003c/div>\n \u003c/div>\n\n \u003c!-- M2 · World Choropleth -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">MAPS · WORLD CHOROPLETH\u003c/span>\n \u003ch2>Where the users are\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · shade = monthly active accounts · gray = markets we haven't opened\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapworld\" style=\"height:400px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · PORCELAIN · GLOBAL OPS\u003c/div>\n \u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>fn(n);\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ M1 · US choropleth ════\n// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。\n// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角\n// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。\n// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、\n// 纸色描边当州界发丝、无数据州留 rgba(8,31,92,.10)(沉默可见)。\n// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。\n// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线\n// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。\nconst MAP_LEGEND={\n itemWidth:11,itemHeight:11,itemSymbol:'rect',\n textStyle:{fontFamily:'Inter',fontSize:9,color:'#7096D1'},\n inRange:{color:['#D0E3FF','#BAD6EB','#7096D1','#334EAC','#081F5C']},\n};\n(()=>{\nconst D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,\n 'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,\n 'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,\n 'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,\n 'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,\n 'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,\n 'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};\nfetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-usa',geo,{\n Alaska:{left:-131,top:25,width:15},\n Hawaii:{left:-110,top:28,width:5},\n 'Puerto Rico':{left:-76,top:26,width:2},\n });\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},\n visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-usa',top:10,bottom:48,\n itemStyle:{areaColor:'rgba(8,31,92,.10)',borderColor:PAPER,borderWidth:1.8},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,\n fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),\n }],\n };\n obsReveal('mapus',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\n// ════ M2 · World choropleth ════\n// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,\n// 关键市场(≥39k)直接把数值标在版图上。\n(()=>{\nconst D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,\n 'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,\n 'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,\n 'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,\n 'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,\n 'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,\n 'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,\n 'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};\nconst BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],\n 'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};\nfetch('https://cdn.jsdelivr.net/npm/[email protected]/map/json/world.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-world',geo);\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},\n visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-world',top:4,bottom:36,\n boundingCoords:[[-170,76],[190,-58]], // crop antarctica\n itemStyle:{areaColor:'rgba(8,31,92,.10)',borderColor:PAPER,borderWidth:1.26},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.98}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,\n fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),\n }],\n };\n obsReveal('mapworld',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\naddEventListener('resize',()=>{\n ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":9825,"content_sha256":"00668509410b25770f83353a1fefb980530f59f0c95cacda049a198d856e0987"},{"filename":"templates/color/maps-wire.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Charts · Maps M1–M2 × WIRE\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n:root{--bg:#F0F0EE;--dark:#1F1E1C;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.32);--grid:rgba(31,30,28,.16)}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #C0BFB7;color:#8F8E86}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:#8F8E86;margin-top:5px;line-height:1.7}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Maps M1–M2 × WIRE\u003c/h1>\n \u003cp>编辑部红地图换肤样张。灰阶承载区域数值,红色只标最高档;仅在用户明确要求地图时使用。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n \u003c!-- M1 · US Choropleth -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">MAPS · US CHOROPLETH\u003c/span>\n \u003ch2>Sign-ups across the states\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapus\" style=\"height:340px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · WIRE · GROWTH GEO\u003c/div>\n \u003c/div>\n\n \u003c!-- M2 · World Choropleth -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">MAPS · WORLD CHOROPLETH\u003c/span>\n \u003ch2>Where the users are\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · shade = monthly active accounts · gray = markets we haven't opened\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapworld\" style=\"height:400px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · WIRE · GLOBAL OPS\u003c/div>\n \u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>fn(n);\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ M1 · US choropleth ════\n// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。\n// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角\n// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。\n// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、\n// 纸色描边当州界发丝、无数据州留 rgba(31,30,28,.10)(沉默可见)。\n// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。\n// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线\n// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。\nconst MAP_LEGEND={\n itemWidth:11,itemHeight:11,itemSymbol:'rect',\n textStyle:{fontFamily:'Inter',fontSize:9,color:'#8F8E86'},\n inRange:{color:['#DBDAD3','#C0BFB7','#8F8E86','#6E6D66','#F5572F']},\n};\n(()=>{\nconst D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,\n 'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,\n 'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,\n 'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,\n 'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,\n 'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,\n 'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};\nfetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-usa',geo,{\n Alaska:{left:-131,top:25,width:15},\n Hawaii:{left:-110,top:28,width:5},\n 'Puerto Rico':{left:-76,top:26,width:2},\n });\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},\n visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-usa',top:10,bottom:48,\n itemStyle:{areaColor:'rgba(31,30,28,.10)',borderColor:PAPER,borderWidth:1.8},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,\n fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),\n }],\n };\n obsReveal('mapus',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\n// ════ M2 · World choropleth ════\n// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,\n// 关键市场(≥39k)直接把数值标在版图上。\n(()=>{\nconst D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,\n 'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,\n 'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,\n 'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,\n 'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,\n 'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,\n 'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,\n 'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};\nconst BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],\n 'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};\nfetch('https://cdn.jsdelivr.net/npm/[email protected]/map/json/world.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-world',geo);\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},\n visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-world',top:4,bottom:36,\n boundingCoords:[[-170,76],[190,-58]], // crop antarctica\n itemStyle:{areaColor:'rgba(31,30,28,.10)',borderColor:PAPER,borderWidth:1.26},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.98}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,\n fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),\n }],\n };\n obsReveal('mapworld',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\naddEventListener('resize',()=>{\n ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":9840,"content_sha256":"c53a8eeb86005d6afe23eada7498275d1b1324a24f8c5c43eb4b1ab52bda42f5"},{"filename":"templates/color/README.md","content":"# 彩色换肤样张\n\n**这里只查配色,不查结构。**\n\n渲染代码、几何、动画的正本永远是 `templates/` 根目录那四个 gallery。彩色样张是它们的换肤版——逐节点对应同一份数据和动效,从这里照抄结构等于绕远路,而且容易漂。\n\n内置预设的色值正本是 `color-presets.js`。样张 header 里那些散落的说明(加墨系数、色板顺序)只是自述,规则本体在 `SKILL.md` 第六点五节。Agent 可以根据数据结构和使用场景自动选择一套预设,不要求用户先触发彩色。同一份 HTML 或同一组交付只能使用一种色彩系统,禁止跨预设混用;用户拿到成品后可以继续调色,但要重新检查对比度、视觉主次和数据语义。\n\n## Custom 不是第四套内置预设\n\n用户明确提供品牌色、hex 或完整色板时,可以按 `SKILL.md` 的角色规则建立一套交付内 `CUSTOM`。它不出现在这里,因为不存在一套对所有用户都成立的 custom 样张。Custom 不得从三套内置预设借色补齐,也不得回写 `color-presets.js`,除非用户明确要求把品牌色固化为项目级预设。\n\nTreemap 的四种实现已经加入 Basics gallery:Mono 靠父级标题带和留白分组;Porcelain 用明度对应有序父组;Palm 用色相对应顶层类目;Wire 只给一个叶子使用荧光橙。\n\n## 三套\n\n| 文件 | 色系 | 色彩逻辑 | 适配 |\n| - | - | - | - |\n| `*-porcelain.html` | 青瓷蓝 | 单色相明度阶 | 有序数据、单序列 |\n| `*-palm.html` | 椰林绿 | 色相 = 类目 | 无序类目 ≤4(5–6 勉强) |\n| `*-wire.html` | 编辑部红 | 灰阶 + 一个荧光橙 | 几乎所有场合,要一个视线落点 |\n\n每套四个 gallery(basics / glance / lupi / maps),共 12 个,没有缺口。地图样张只在用户明确要求地图时才用得上。\n\narchive(陶土青灰)已归档,不再是候选。它和 palm 都是\"色相=类目\",功能重叠。\n\n## 三个交互大图\n\n`big-circular` / `big-force` / `big-threads` 只有 porcelain 和 palm 两套,没有 wire。\n\n原因:wire 本身就是黑灰阶加一个荧光橙,和原版 mono 大图的差别只在强调色,做出来近乎重复。要 wire 风格的大图,直接用 `templates/` 根目录的原版。\n\n这三张是**暗底图**,色彩逻辑和浅卡不同:卡底压深,8 档单色相阶从亮到暗,编码的是\"离核心多远\"(亮 = 核心),不是类目。色值在 `color-presets.js` 各套的 `DARK` 块里。\n\n为什么不用色相分类目:这三张分别需要 5 / 8 / 三列结构的档位,超过 palm 绿黄区间的容量上限(≤6)。按 SKILL.md 门一的规则,只能走单色相明度阶。\n\n`big-threads` 的 hover 高亮是改 opacity(线恒为 `DK8[0]`),不用 `DARK.HL`。\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2836,"content_sha256":"cb402b5f57a5746aee98ed10f48a23db7168fe613c80cf6342dc99a76f783659"},{"filename":"templates/glance-gallery.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Charts — Glance Gallery · 20 张参考实现\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/chart.js@4\">\u003c/script>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.dark{background:var(--dark);color:#F0EFEB}\n .card.dark .sub{color:#8F8E88}\n .card.dark .src{color:#55554F}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .ch{height:320px}\n .wrap{position:relative;height:320px}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}\n .pagehead .badge{margin:0 2px}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px}\n .badge.del{background:#1C1C1A;color:#F0EFEB}\n .badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}\n .badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}\n .card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}\n .card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}\n .card.dark .badge.pair{border-color:#55554F;color:#8F8E88}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n\n \u003ch1>Glance 系(快读)· 20 张参考实现\u003c/h1>\n \u003cp>粗笔画 · 提前聚合 · 3 秒读完。每张卡是一个图型的参考实现;代码在页内 \u003ccode>// ════ 图型名 ════\u003c/code> 注释块里,按 catalog.md 的卡内标题检索。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003c!-- ══ mono-demo ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Revenue by plan\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · MRR, $K\u003c/div>\n \u003cdiv class=\"wrap\">\u003ccanvas id=\"c3\">\u003c/canvas>\u003c/div>\n \u003cdiv class=\"src\">CHUNKY BARS · MONO-DEMO · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-redesign · dot waffle ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where sign-ups come from\u003c/h2>\n \u003cdiv class=\"sub\">Q2 2026 · every dot = 1% of new accounts\u003c/div>\n \u003csvg id=\"waffle\" viewBox=\"0 0 380 300\" width=\"100%\" style=\"max-height:320px\">\u003c/svg>\n \u003cdiv class=\"src\">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Trees planted, year by year\u003c/h2>\n \u003cdiv class=\"sub\">reforestation program · thousands of trees\u003c/div>\n \u003cdiv class=\"ch\" id=\"forest\">\u003c/div>\n \u003cdiv class=\"src\">PICTORIAL BAR · MONO-FANCY · GREEN OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Who works with whom\u003c/h2>\n \u003cdiv class=\"sub\">cross-team collaborations · Q2 2026 · line = shared project\u003c/div>\n \u003cdiv class=\"ch\" id=\"circular\">\u003c/div>\n \u003cdiv class=\"src\">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Everything the platform ships\u003c/h2>\n \u003cdiv class=\"sub\">product areas and capabilities · Jun 2026\u003c/div>\n \u003cdiv class=\"ch\" id=\"tree\">\u003c/div>\n \u003cdiv class=\"src\">TREE · MONO-FANCY · PRODUCT CATALOG\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Campaigns rain down, sign-ups flow\u003c/h2>\n \u003cdiv class=\"sub\">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days\u003c/div>\n \u003cdiv class=\"ch\" id=\"rainfall\">\u003c/div>\n \u003cdiv class=\"src\">DUAL AREA · MONO-FANCY · GROWTH + ADS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>One dataset, three views\u003c/h2>\n \u003cdiv class=\"sub\">watch the same 12 products fly between views · every 3s\u003c/div>\n \u003cdiv class=\"ch\" id=\"morph\">\u003c/div>\n \u003cdiv class=\"src\">SCATTER MORPH · MONO-FANCY · COHORTS\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy2 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Where we gained, where we bled\u003c/h2>\n \u003cdiv class=\"sub\">net accounts per segment · Q2 2026 · churn left, growth right\u003c/div>\n \u003cdiv class=\"ch\" id=\"negbar\">\u003c/div>\n \u003cdiv class=\"src\">DIVERGING BAR · MONO-FANCY2 · CRM\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card dark\">\n \u003ch2>Integrations, pulled into orbit\u003c/h2>\n \u003cdiv class=\"sub\">force layout · node = monthly syncs · drag a node, the web fights back\u003c/div>\n \u003cdiv class=\"ch\" id=\"force\">\u003c/div>\n \u003cdiv class=\"src\">FORCE GRAPH · NEW · INTEGRATION HUB\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Fifty markets, one wave\u003c/h2>\n \u003cdiv class=\"sub\">sign-ups per market · bars cascade in · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"wave\">\u003c/div>\n \u003cdiv class=\"src\">STAGGER DELAY · MONO-FANCY2 · GROWTH\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy3 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Big slice, deep engagement\u003c/h2>\n \u003cdiv class=\"sub\">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min\u003c/div>\n \u003cdiv class=\"ch\" id=\"custompie\">\u003c/div>\n \u003cdiv class=\"src\">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Support load, day by day\u003c/h2>\n \u003cdiv class=\"sub\">tickets per hour · each row is its own axis · dot = volume\u003c/div>\n \u003cdiv class=\"ch\" id=\"singleaxis\">\u003c/div>\n \u003cdiv class=\"src\">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Response times, spread out\u003c/h2>\n \u003cdiv class=\"sub\">every dot = one ticket · jitter reveals the pile-up · by priority\u003c/div>\n \u003cdiv class=\"ch\" id=\"jitter\">\u003c/div>\n \u003cdiv class=\"src\">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- ══ mono-fancy4 ══ -->\n \u003cdiv class=\"card\">\n \u003ch2>Eight products race for revenue\u003c/h2>\n \u003cdiv class=\"sub\">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"race\">\u003c/div>\n \u003cdiv class=\"src\">BAR RACE · MONO-FANCY4 · BILLING\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>Concurrent users, streaming\u003c/h2>\n \u003cdiv class=\"sub\">one new reading every 300ms · window of 50 · click to restart\u003c/div>\n \u003cdiv class=\"ch\" id=\"stream\">\u003c/div>\n \u003cdiv class=\"src\">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS\u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"card\">\n \u003ch2>H1 revenue, drawn in one stroke\u003c/h2>\n \u003cdiv class=\"sub\">cumulative ARR · the line draws, the number counts · click to replay\u003c/div>\n \u003cdiv class=\"ch\" id=\"stroke\">\u003c/div>\n \u003cdiv class=\"src\">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE\u003c/div>\n \u003c/div>\n\n\n \u003c!-- G19 · violin -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 分布型 · 小提琴\u003c/span>\n \u003ch2>How fast each plan gets an answer\u003c/h2>\n \u003cdiv class=\"sub\">hours to first reply · silhouette = where tickets pile up · paper tick = median\u003c/div>\n \u003csvg id=\"violin\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">VIOLIN · MONO-GLANCE · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- G20 · matrix heat -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 热力型 · 通用矩阵\u003c/span>\n \u003ch2>Adoption runs hot on the new versions\u003c/h2>\n \u003cdiv class=\"sub\">5 versions × 6 features · shade = % of accounts using it · every cell carries its number\u003c/div>\n \u003csvg id=\"gheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">MATRIX HEAT (GLANCE) · MONO-GLANCE · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- G21 · rank strip -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 排名型 · RANK STRIP\u003c/span>\n \u003ch2>Flows climbs to the top\u003c/h2>\n \u003cdiv class=\"sub\">one row per product, one cell per quarter · darker = higher rank · a bar race you can print\u003c/div>\n \u003csvg id=\"rankstrip\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RANK STRIP · MONO-GLANCE · BILLING\u003c/div>\n \u003c/div>\n\n \u003c!-- G22 · aggregate sankey -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">GLANCE 流向型 · 聚合桑基\u003c/span>\n \u003ch2>Channels pour into plans\u003c/h2>\n \u003cdiv class=\"sub\">ribbon width = accounts · channels on the left, the plans they land in on the right\u003c/div>\n \u003csvg id=\"sankey\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">AGGREGATE SANKEY · MONO-GLANCE · ATTRIBUTION\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── shared Mono tokens ──\nconst INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';\nconst L=['#1C1C1A','#4A4944','#6A6963','#8F8E88','#B0AFA9','#C6C5BF'];\nconst LAD=['#1C1C1A','#4A4944','#8F8E88','#B0AFA9','#D8D7D1'];\nconst tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};\nconst rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic\n\n// ── unified reveal: every chart waits to be scrolled into view; click replays.\n// fn charts manage their own timers via keep(); they're cleared on replay.\nconst timers={};\nconst obsReveal=(id,fn)=>{\n const el=document.getElementById(id);\n const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};\n const io=new IntersectionObserver(es=>{\n if(es[0].isIntersecting){go();io.disconnect()}\n },{threshold:.35});\n io.observe(el);\n el.style.cursor='pointer';\n el.addEventListener('click',go);\n};\nconst keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};\nconst eReveal=(id,opt)=>obsReveal(id,el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();g.setOption(opt);\n});\nconst cjs={};\nconst cReveal=(id,mkCfg)=>obsReveal(id,el=>{\n cjs[id]?.destroy();\n cjs[id]=new Chart(el,mkCfg());\n});\n\nChart.defaults.color=MUTED;\nChart.defaults.font.family='Inter';\nChart.defaults.font.size=10.5;\nChart.defaults.borderColor=GRID;\nChart.defaults.plugins.legend.display=false;\nChart.defaults.plugins.tooltip.backgroundColor=INK;\nChart.defaults.plugins.tooltip.titleColor='#F0EFEB';\nChart.defaults.plugins.tooltip.bodyColor='#A5A49E';\nChart.defaults.plugins.tooltip.padding=12;\nChart.defaults.plugins.tooltip.cornerRadius=12;\nChart.defaults.plugins.tooltip.displayColors=false;\n\n// ════ mono-demo · 3: chunky bars, values on top ════\n(()=>{\ncReveal('c3',()=>({\n type:'bar',\n data:{labels:['STARTER','PRO','TEAM','ENT'],\n datasets:[{data:[182,486,391,274],\n backgroundColor:['#C6C5BF',INK,'#8F8E88','#B0AFA9'],\n borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},\n options:{\n maintainAspectRatio:false,\n animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},\n plugins:{tooltip:{callbacks:{label:i=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+i.raw+'K MRR'}}},\n scales:{\n x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},\n y:{display:false,suggestedMax:540},\n },\n },\n plugins:[{\n id:'topValues',\n afterDatasetsDraw(chart){\n const {ctx}=chart;\n chart.getDatasetMeta(0).data.forEach((bar,i)=>{\n ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';\n ctx.fillText('

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();\n });\n },\n }],\n}));\n})();\n\n// ════ mono-redesign · donut → waffle of dots ════\n(()=>{\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];\nobsReveal('waffle',s=>{\n s.innerHTML='';\n const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;\n let idx=0;\n D.forEach(([name,v],g)=>{\n for(let k=0;k\u003cv;k++){\n const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;\n mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:LAD[g],\n class:'pop',style:`animation-delay:${c*.008+g*.05}s`});\n }\n idx+=v;\n });\n D.forEach(([name,v],g)=>{\n const y=26+g*40;\n mkEl(s,'circle',{cx:246,cy:y,r:6,fill:LAD[g]});\n const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});\n n.textContent=name;\n const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g\u003c2?INK:'#8F8E88'});\n val.textContent=v+'%';\n });\n});\n})();\n\n// ════ mono-fancy · 1: pictorial bar forest ════\n(()=>{\nconst 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';\nconst years=['2022','2023','2024','2025','2026'];\nconst vals=[26,41,63,88,117];\neReveal('forest',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},\n grid:{left:44,right:56,top:10,bottom:8},\n yAxis:{type:'category',data:years,inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},\n xAxis:{show:false,max:13},\n series:[{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:L[Math.max(0,4-i)]}})),\n label:{show:true,position:'right',offset:[8,0],\n color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,\n formatter:p=>vals[p.dataIndex]+'k'},\n z:10,\n },{\n type:'pictorialBar',symbol:TREE,\n symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,\n itemStyle:{color:'#DEDDD6'},data:years.map(()=>13),silent:true,z:5,\n }],\n});\n})();\n\n// ════ mono-fancy · 2: circular graph on dark ════\n(()=>{\nconst teams=[\n ['Product',52],['Design',38],['Frontend',46],['Backend',44],\n ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],\n];\nconst links=[\n [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],\n [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],\n];\neReveal('circular',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:tipDark,\n series:[{\n type:'graph',layout:'circular',circular:{rotateLabel:false},\n left:52,right:52,top:34,bottom:34,\n data:teams.map(([n,v],i)=>({\n name:n,value:v,symbolSize:v*.62,\n itemStyle:{color:i\u003c4?PAPER:i\u003c8?'#8F8E88':'#4A4944',borderWidth:0},\n label:{show:true,position:'right',distance:7,color:'#B3B0A4',fontFamily:'Inter',fontSize:9.5,fontWeight:600},\n })),\n links:links.map(([s,t,w])=>({\n source:s,target:t,\n lineStyle:{width:w*.7,color:'#4A4840',opacity:.55,curveness:.28},\n })),\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},\n }],\n});\n})();\n\n// ════ mono-fancy · 3: LR tree ════\n(()=>{\nconst b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},\n children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});\neReveal('tree',{\n animationDuration:1100,animationEasing:'quarticOut',\n tooltip:tipLight,\n series:[{\n type:'tree',layout:'orthogonal',orient:'LR',\n left:64,right:96,top:8,bottom:8,\n symbol:'circle',symbolSize:7,\n initialTreeDepth:2,expandAndCollapse:false,roam:false,\n itemStyle:{borderWidth:0},\n lineStyle:{width:1.4,curveness:.5},\n label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:'#6A6963',position:'left'},\n leaves:{label:{position:'right',color:MUTED,fontWeight:500}},\n data:[{\n name:'Platform',itemStyle:{color:INK},lineStyle:{color:'#C6C5BF'},\n label:{fontSize:11.5,color:INK},\n children:[\n b('Editor',['Blocks','Tables','Comments','History'],L[0]),\n b('Automate',['Workflows','Triggers','Webhooks'],L[2]),\n b('Collaborate',['Spaces','Guests','Mentions'],L[3]),\n b('Integrate',['API','Slack','GitHub'],L[4]),\n ],\n }],\n }],\n});\n})();\n\n// ════ mono-fancy · 4: rainfall dual area ════\n(()=>{\nconst days=Array.from({length:30},(_,i)=>i+1);\nconst 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];\nconst 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];\neReveal('rainfall',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipLight,trigger:'axis',\n formatter:p=>`Day ${p[0].axisValue} — spend ${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},\n axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:'#B0AFA9'}},\n grid:[\n {left:44,right:14,top:8,height:'26%'},\n {left:44,right:14,top:'42%',bottom:26},\n ],\n xAxis:[\n {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},\n {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},\n ],\n yAxis:[\n {gridIndex:0,inverse:true,max:18,splitLine:{show:false},\n axisLabel:{color:'#B0AFA9',fontFamily:'Inter',fontSize:9,formatter:'${value}K'},\n axisLine:{show:false},axisTick:{show:false}},\n {gridIndex:1,splitLine:{lineStyle:{color:'#DEDDD6'}},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},\n axisLine:{show:false},axisTick:{show:false}},\n ],\n series:[\n {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,data:rain,\n itemStyle:{color:'#B0AFA9',borderRadius:[0,0,4,4]},barWidth:'55%'},\n {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,\n smooth:.4,symbol:'none',lineStyle:{color:INK,width:2.2},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:'rgba(28,28,26,.22)'},{offset:1,color:'rgba(28,28,26,0)'}]}}},\n ],\n});\n})();\n\n// ════ mono-fancy · 5: universal transition morph ════\n(()=>{\nconst P12=[\n ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],\n ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],\n ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],\n ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],\n];\nconst shade=i=>L[Math.min(5,Math.floor(i/2))];\nconst axis={splitLine:{lineStyle:{color:'#DEDDD6'}},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};\nconst VIEWS=[\n {opt:{\n grid:{left:38,right:16,top:30,bottom:30},\n xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

,nameTextStyle:{color:'#C6C5BF',fontSize:8.5}},\n yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:'#C6C5BF',fontSize:8.5}},\n series:[{\n id:'p',type:'scatter',universalTransition:true,\n symbolSize:d=>Math.sqrt(d[3])*1.35,\n data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],\n groupId:d[0],itemStyle:{color:shade(i)}})),\n label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,\n formatter:p=>p.name},\n }],\n }},\n {opt:{\n grid:{left:38,right:16,top:30,bottom:44},\n xAxis:{...axis,type:'category',\n data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),\n axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},\n yAxis:{...axis,type:'value',max:520},\n series:[{\n id:'p',type:'bar',universalTransition:true,\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(i),borderRadius:[6,6,0,0]}})),\n barCategoryGap:'32%',\n }],\n }},\n {opt:{\n grid:{left:0,right:0,top:0,bottom:0},\n xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},\n series:[{\n id:'p',type:'pie',universalTransition:true,\n radius:['26%','72%'],center:['50%','50%'],\n itemStyle:{borderColor:'#F0EFEB',borderWidth:2,borderRadius:6},\n label:{color:'#6A6963',fontFamily:'Inter',fontSize:9,formatter:'{b}'},\n labelLine:{lineStyle:{color:'#C6C5BF'}},\n data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],\n groupId:d[0],itemStyle:{color:shade(i)}})),\n }],\n }},\n];\nconst base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};\nobsReveal('morph',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let vi=0;\n g.setOption({...base,...VIEWS[0].opt});\n keep('morph',setInterval(()=>{\n vi=(vi+1)%VIEWS.length;\n g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});\n },3000));\n});\n})();\n\n// ════ mono-fancy2 · 2: diverging bar ════\n(()=>{\nconst D=[\n ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],\n ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],\n];\neReveal('negbar',{\n animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,\n tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},\n grid:{left:96,right:52,top:8,bottom:8},\n xAxis:{type:'value',\n splitLine:{lineStyle:{color:'#DEDDD6'}},\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:'#6A6963',fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',barWidth:16,\n data:D.map(([n,v])=>({name:n,value:v,\n itemStyle:{color:v>0?INK:'#B0AFA9',\n borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),\n label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,\n position:'outside',\n formatter:p=>(p.value>0?'+':'')+p.value,color:INK},\n markLine:{symbol:'none',silent:true,label:{show:false},\n lineStyle:{color:'#8F8E88',width:1.5},\n data:[{xAxis:0}]},\n }],\n});\n})();\n\n// ════ force graph on dark: the network shakes itself into place ════\n// (replaced the axis-break bar — bars must never lie about length; house rule)\n(()=>{\nconst hubs=[['Core API',52,0]];\nconst apps=[\n ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],\n ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],\n ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],\n];\nconst nodes=[...hubs,...apps];\nconst links=[\n ...apps.map((a,i)=>({source:a[0],target:'Core API',\n lineStyle:{width:Math.max(.8,a[1]*.09),color:'#4A4840',opacity:.6}})),\n // a few app-to-app side roads\n {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:'#3A3934',opacity:.5}},\n {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:'#3A3934',opacity:.5}},\n {source:'Figma',target:'Notion',lineStyle:{width:1,color:'#3A3934',opacity:.5}},\n {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:'#3A3934',opacity:.5}},\n];\neReveal('force',{\n animationDuration:1200,animationEasing:'quarticOut',\n tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},\n series:[{\n type:'graph',layout:'force',\n force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},\n roam:false,draggable:true,\n left:14,right:14,top:14,bottom:14,\n data:nodes.map(([n,v,tier])=>({\n name:n,value:v,symbolSize:8+v*.75,\n itemStyle:{color:tier===0?PAPER:tier===1?'#B3B0A4':'#6A6963'},\n label:{show:true,position:'right',distance:6,\n color:tier===0?PAPER:'#8F8E88',fontFamily:'Inter',\n fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},\n })),\n links,\n emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},\n }],\n});\n})();\n\n// ════ mono-fancy2 · 4: stagger wave ════\n(()=>{\nconst n=50;\nconst vals=Array.from({length:n},(_,i)=>\n Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));\neReveal('wave',{\n tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},\n grid:{left:36,right:10,top:14,bottom:24},\n xAxis:{type:'category',data:vals.map((_,i)=>i+1),\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},\n yAxis:{type:'value',\n splitLine:{lineStyle:{color:'#DEDDD6'}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},\n series:[{\n type:'bar',barCategoryGap:'26%',\n data:vals.map((v,i)=>({value:v,\n itemStyle:{color:L[Math.min(5,Math.floor((120-v)/16))],borderRadius:[4,4,0,0]}})),\n animationDelay:i=>i*36,\n animationDuration:640,animationEasing:'elasticOut',\n }],\n animationDurationUpdate:640,\n animationDelayUpdate:i=>i*36,\n});\n})();\n\n// ════ mono-fancy3 · 1: custom double-encoded pie ════\n(()=>{\nconst D=[\n ['Editor',34,42],['Boards',22,31],['Docs',18,38],\n ['Chat',12,11],['Automations',8,24],['Other',6,7],\n];\nconst SCALE=45;\nconst TAU=Math.PI*2;\nconst byShare=[...D].sort((a,b)=>b[1]-a[1]);\nconst minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);\nlet acc=0;\nconst slices=byShare.map(([n,share,mins])=>{\n const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;\n acc+=share;\n return {n,share,mins,p0,p1,color:L[minsDesc.indexOf(mins)]};\n});\neReveal('custompie',{\n animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,\n tooltip:{...tipLight,formatter:p=>{\n const s=slices[p.dataIndex];\n return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';\n }},\n xAxis:{show:false,min:-1,max:1},\n yAxis:{show:false,min:-1,max:1},\n grid:{left:0,right:0,top:0,bottom:0},\n series:[{\n type:'custom',\n renderItem:(params,api)=>{\n const s=slices[params.dataIndex];\n const cx=api.getWidth()/2, cy=api.getHeight()/2+6;\n const R=Math.min(api.getWidth(),api.getHeight())*.37;\n const r0=R*.24;\n const rOf=m=>r0+(R-r0)*m/SCALE;\n const pm=(s.p0+s.p1)/2, lr=R+14;\n const cos=Math.cos(pm);\n const kids=[];\n if(params.dataIndex===0){\n [15,30,45].forEach(m=>kids.push({type:'circle',\n shape:{cx,cy,r:rOf(m)},\n style:{fill:'none',stroke:'#D8D6CE',lineWidth:1,lineDash:[3,4]}}));\n }\n kids.push({type:'sector',\n shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},\n style:{fill:s.color,stroke:PAPER,lineWidth:3},\n enterFrom:{shape:{r:r0}}});\n kids.push({type:'text',style:{\n x:cx+cos*lr, y:cy+Math.sin(pm)*lr,\n text:s.n+' '+s.share+'% · '+s.mins+'m',\n font:'600 10px Inter',fill:'#55544E',\n align:Math.abs(cos)\u003c.25?'center':(cos>0?'left':'right'),\n verticalAlign:'middle'},\n enterFrom:{style:{opacity:0}}});\n return {type:'group',children:kids};\n },\n data:slices.map(s=>s.share),\n }],\n});\n})();\n\n// ════ mono-fancy3 · 3: single-axis small multiples ════\n(()=>{\nconst days=['MON','TUE','WED','THU','FRI','SAT','SUN'];\nconst hours=Array.from({length:24},(_,i)=>i);\nconst mkRow=di=>hours.map(h=>{\n const workday=di\u003c5, core=h>=9&&h\u003c=17;\n 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\u003c=5?0:1));\n return [h,v];\n}).filter(d=>d[1]>0);\neReveal('singleaxis',{\n animationDuration:500,animationEasing:'backOut',\n tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},\n title:days.map((d,i)=>({\n text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,\n textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},\n })),\n singleAxis:days.map((d,i)=>({\n left:52,right:14,type:'category',boundaryGap:false,\n data:hours.map(h=>String(h).padStart(2,'0')),\n top:i*100/7+6+'%',height:100/7-8+'%',\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},\n splitLine:{show:false},\n })),\n series:days.map((d,i)=>({\n singleAxisIndex:i,coordinateSystem:'singleAxis',\n type:'scatter',\n data:mkRow(i),\n symbolSize:dd=>dd[1]*2.6,\n itemStyle:{color:i\u003c5?L[Math.min(4,i)]:'#C6C5BF'},\n animationDelay:di=>i*160+di*14,\n })),\n});\n})();\n\n// ════ mono-fancy3 · 4: jitter strip ════\n(()=>{\nconst cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];\nconst mkPts=(ci,n,median,spread,outliers)=>{\n const pts=[];\n for(let i=0;i\u003cn;i++){\n const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);\n let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);\n if(i\u003coutliers) x=median+spread*2.2+u*spread*3;\n const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;\n pts.push([Math.max(.2,x),jy]);\n }\n return pts;\n};\nconst groups=[\n mkPts(0,38,.8,.5,2),\n mkPts(1,64,2.4,1.1,3),\n mkPts(2,110,6.5,2.4,4),\n mkPts(3,72,14,4.5,3),\n];\nconst flat=groups.flat();\neReveal('jitter',{\n animationDuration:450,animationEasing:'cubicOut',\n animationDelay:i=>i\u003cflat.length?Math.round(flat[i][1])*260+(i%37)*9:0,\n tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},\n grid:{left:86,right:16,top:14,bottom:30},\n xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:'#C6C5BF',fontSize:8.5},\n splitLine:{lineStyle:{color:'#DEDDD6'}},\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},\n yAxis:{type:'value',min:-.6,max:3.6,inverse:true,\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},\n axisLabel:{show:false}},\n series:[{\n type:'scatter',\n data:flat,\n symbolSize:7,\n itemStyle:{color:p=>L[Math.round(p.value[1])],opacity:.62},\n },{\n type:'scatter',silent:true,symbolSize:0,\n data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],\n color:'#6A6963',fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),\n }],\n});\n})();\n\n// ════ mono-fancy4 · 1: bar race ════\n(()=>{\nconst P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];\nconst YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];\nconst vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];\nfor(let y=1;y\u003cYEARS.length;y++)\n vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));\nobsReveal('race',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let step=0;\n const frame=()=>{\n const row=vals[step];\n const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);\n g.setOption({\n animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:v=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(v)+'K'},\n grid:{left:64,right:64,top:8,bottom:8},\n xAxis:{type:'value',max:'dataMax',\n splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},\n yAxis:{type:'category',data:P,inverse:true,max:7,\n animationDuration:300,animationDurationUpdate:300,\n axisLine:{show:false},axisTick:{show:false},\n axisLabel:{color:'#6A6963',fontFamily:'Inter',fontSize:9.5,fontWeight:600}},\n series:[{\n type:'bar',realtimeSort:true,barCategoryGap:'32%',\n data:row.map((v,i)=>({value:v,\n itemStyle:{color:L[Math.min(5,rank.indexOf(i))],borderRadius:99}})),\n label:{show:true,position:'right',valueAnimation:true,\n fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,\n formatter:p=>'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+Math.round(p.value)+'K'},\n }],\n graphic:[{id:'yr',type:'text',right:18,bottom:14,\n style:{text:YEARS[step],font:'800 44px Inter',fill:'#DEDDD6'}}],\n });\n step=(step+1)%YEARS.length;\n };\n frame();\n keep('race',setInterval(frame,1150));\n});\n})();\n\n// ════ mono-fancy4 · 2: dynamic data stream ════\n(()=>{\nconst WIN=50;\nobsReveal('stream',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n let t=0,v=64;\n const data=[];\n for(;t\u003cWIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}\n const draw=()=>g.setOption({\n animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',\n tooltip:{...tipLight,valueFormatter:x=>x+'k users'},\n grid:{left:14,right:58,top:44,bottom:20},\n xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},\n yAxis:{show:false,min:20,max:130},\n series:[{\n type:'line',data:[...data],smooth:.45,symbol:'none',\n lineStyle:{color:INK,width:2.4},\n endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:INK,\n formatter:p=>p.value+'k'},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:'rgba(28,28,26,.16)'},{offset:1,color:'rgba(28,28,26,0)'}]}},\n }],\n graphic:[{type:'group',left:16,top:10,children:[\n {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:'#4A4944'}},\n {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:INK}},\n ]}],\n });\n draw();\n keep('stream',setInterval(()=>{\n v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));\n data.push(Math.round(v));data.shift();t++;\n draw();\n },300));\n});\n})();\n\n// ════ mono-fancy4 · 3: draw-in + counter ════\n(()=>{\nconst days=Array.from({length:180},(_,i)=>i);\nconst daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);\nconst cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);\nconst TOTAL=cum[cum.length-1];\nconst DUR=2600;\nlet gen=0; // replay guard: only the latest counter loop may write\nobsReveal('stroke',el=>{\n const g=echarts.getInstanceByDom(el)||echarts.init(el);\n g.clear();\n gen++;const my=gen;\n g.setOption({\n animationDuration:DUR,animationEasing:'cubicOut',\n tooltip:{show:false},\n grid:{left:14,right:20,top:64,bottom:24},\n xAxis:{type:'category',data:days,boundaryGap:false,\n axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},\n axisLabel:{color:'#C6C5BF',fontFamily:'Inter',fontSize:9,fontWeight:600,\n interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},\n yAxis:{show:false,max:TOTAL*1.06},\n series:[{\n type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',\n lineStyle:{color:INK,width:2.6},\n areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,\n colorStops:[{offset:0,color:'rgba(28,28,26,.14)'},{offset:1,color:'rgba(28,28,26,0)'}]}},\n }],\n });\n const t0=performance.now();\n const tick=()=>{\n if(my!==gen)return;\n const p=Math.min(1,(performance.now()-t0)/DUR);\n const eased=1-Math.pow(1-p,3);\n const val=(TOTAL*eased/1000).toFixed(2);\n g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[\n {type:'text',style:{text:'

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

+val+'M',font:'800 32px Inter',fill:INK}},\n {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:'#8F8E88'}},\n ]}]});\n if(p\u003c1)requestAnimationFrame(tick);\n };\n requestAnimationFrame(tick);\n});\n})();\n\naddEventListener('resize',()=>{\n ['forest','circular','tree','rainfall','morph','negbar','force','wave',\n 'custompie','singleaxis','jitter','race','stream','stroke']\n .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ G19 · violin ════\n// 小提琴:提前聚合的密度剪影,Glance 世界观。\n// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),\n// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。\n(()=>{\nconst G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];\nconst M=64;\nconst samples=g=>Array.from({length:M},(_,i)=>{\n const [,c,s]=G[g];\n const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);\n return Math.max(.2,v);\n});\nobsReveal('violin',s=>{\n const TOP=40,BASE=262,HMAX=17;\n const mapY=h=>BASE-h/HMAX*(BASE-TOP);\n const colX=g=>76+g*84;\n // hour grid\n [0,4,8,12,16].forEach(h=>{\n el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'#E3E2DB','stroke-width':.8,\n class:'fade',style:`animation-delay:${h*.01}s`});\n txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${h*.01}s`},h+'h');\n });\n const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9'];\n G.forEach(([name],g)=>{\n const vs=samples(g).sort((a,b)=>a-b);\n const med=vs[Math.floor(M/2)];\n // fuller bandwidth = the classic rounded violin belly, not a spindle\n const bw=Math.max(.9,G[g][2]*.72);\n // pad past the data by 1.6 bandwidths so the density decays to ~0 and the\n // ends taper naturally — clipping tighter gives flat-cut lantern ends\n const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);\n const NPTS=44;\n const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));\n const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));\n const dmax=Math.max(...dens);\n const W=30;\n const x=colX(g);\n // smooth closed outline through the mirrored density — quadratic\n // midpoint smoothing, same trick as almanac's blob\n const pts=[];\n grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));\n for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let t=0;t\u003cpts.length;t++){\n const p=pts[t],q=pts[(t+1)%pts.length];\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)}`;\n }\n const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});\n tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);\n // median: paper tick across the body, value parked right\n const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));\n const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;\n el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),\n stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});\n txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');\n txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);\n });\n txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');\n});\n})();\n\n// ════ G20 · matrix heat ════\n// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出\n// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。\n(()=>{\nconst FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];\nconst VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];\nconst BASEV=[88,74,61,42,35,27];\nconst v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));\nconst shade=t=>t>64?INK:t>46?'#4A4944':t>30?'#8F8E88':t>15?'#B0AFA9':'#D8D7D1';\nobsReveal('gheat',s=>{\n const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;\n FEAT.forEach((f,c)=>{\n txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);\n });\n VER.forEach((ver,r)=>{\n txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',\n class:'fade',style:`animation-delay:${r*.05}s`},ver);\n FEAT.forEach((f,c)=>{\n const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;\n const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),\n class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});\n tip(cell,`${f} on ${ver} — ${t}% of accounts`);\n 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',\n class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);\n });\n });\n txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},\n 'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');\n});\n})();\n\n// ════ G21 · rank strip ════\n// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,\n// 和 slope 同一种丑(catalog 实测三连败那条),废弃。\n// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。\n// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。\n(()=>{\nconst RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],\n ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];\nconst SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9','#D8D7D1']; // rank 1 → 5\nobsReveal('rankstrip',s=>{\n // rows sorted by final rank, the climber lands on top\n const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);\n const X0=110,PX=38,Y0=64,PY=42,C=32;\n ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>\n txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));\n rows.forEach(([p,rk],i)=>{\n const y=Y0+i*PY,hero=p==='Flows';\n 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',\n class:'fade',style:`animation-delay:${i*.07}s`},p);\n rk.forEach((r,q)=>{\n const dark=r\u003c=2;\n const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],\n stroke:'#CFCEC7','stroke-width':.5,\n class:'pop',style:`animation-delay:${i*.07+q*.035}s`});\n tip(cell,`${p} — #${r} in Q${q+1}`);\n txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,\n fill:dark?PAPER:'#55544E','text-anchor':'middle',\n class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);\n });\n // arrival flag: climbed / fell / held, told at the row's end\n const d=rk[0]-rk[5];\n txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,\n fill:d>0?INK:d\u003c0?'#B0AFA9':'#C6C5BF',\n class:'fade',style:`animation-delay:${.5+i*.07}s`},\n d>0?'▲'+d:d\u003c0?'▼'+(-d):'—');\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');\n});\n})();\n\n// ════ G22 · aggregate sankey ════\n// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——\n// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。\n// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。\n(()=>{\nconst SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];\nconst DST=['FREE','PRO','TEAM'];\nconst SH=[INK,'#4A4944','#8F8E88','#B0AFA9','#C6C5BF'];\nobsReveal('sankey',s=>{\n const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;\n const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));\n const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));\n const sy=[],dy=[];\n let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});\n y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});\n const syc=[...sy],dyc=[...dy];\n SRC.forEach((d,i)=>{\n d[1].forEach((v,j)=>{\n if(!v)return;\n const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;\n syc[i]=sb;dyc[j]=db;\n 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`,\n fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});\n tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);\n });\n });\n SRC.forEach((d,i)=>{\n el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],\n class:'fade',style:`animation-delay:${i*.06}s`});\n 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',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);\n });\n DST.forEach((n,j)=>{\n el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#3A3934',\n class:'fade',style:`animation-delay:${.1+j*.06}s`});\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#4A4944',\n 'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);\n txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);\n });\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":46157,"content_sha256":"d2e99307cb35417405a1df9edc608651a5868c05322eaf8a79191fb8fab12ced"},{"filename":"templates/lupi-gallery.html","content":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Charts — Lupi Gallery · 19 张参考实现\u003c/title>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n :root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.dark{background:var(--dark);color:#F0EFEB}\n .card.dark .sub{color:#8F8E88}\n .card.dark .src{color:#55554F}\n .card.wide{grid-column:1/-1}\n .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .note{font-size:11px;color:#6A6963;line-height:1.65;margin-bottom:16px}\n .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}\n .pagehead .badge{margin:0 2px}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px}\n .badge.del{background:#1C1C1A;color:#F0EFEB}\n .badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}\n .badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}\n .card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}\n .card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}\n .card.dark .badge.pair{border-color:#55554F;color:#8F8E88}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n\n \u003ch1>Lupi 系(细读)· 19 张参考实现\u003c/h1>\n \u003cp>发丝线 · 逐记录 · 30 秒阅读。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n\n \u003c!-- 1 · launch fan -->\n \u003cdiv class=\"card\">\n \u003ch2>Twelve features, fanned out\u003c/h2>\n \u003cdiv class=\"sub\">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since\u003c/div>\n \u003csvg id=\"fan\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">LAUNCH FAN · MONO-EDITORIAL · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- 4 · dot cascade on dark -->\n \u003cdiv class=\"card dark\">\n \u003ch2>What breaks, stacked and ranked\u003c/h2>\n \u003cdiv class=\"sub\">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending\u003c/div>\n \u003csvg id=\"cascade\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">DOT CASCADE · MONO-EDITORIAL · INCIDENT LOG\u003c/div>\n \u003c/div>\n\n \u003c!-- 2 · barcode lollipop, wide + editorial text column -->\n \u003cdiv class=\"card wide\">\n \u003cdiv class=\"split\">\n \u003cdiv>\n \u003ch2>Ninety days as a barcode\u003c/h2>\n \u003cdiv class=\"sub\">peak concurrent users, daily · Apr – Jun 2026\u003c/div>\n \u003cdiv class=\"note\">Every hairline is a day, whether or not anything\n happened in it. The dot marks the day's peak; the stem below it is\n just gravity. Read the field, not the numbers — the season has a\n texture before it has a value.\u003c/div>\n \u003cdiv class=\"legend\">\n ●  WEEKDAY PEAK\u003cbr>\n ○  WEEKEND PEAK\u003cbr>\n │  ONE CALENDAR DAY\u003cbr>\n ◉  TOP-3, LABELED\n \u003c/div>\n \u003c/div>\n \u003cdiv>\n \u003csvg id=\"barcode\" viewBox=\"0 0 800 300\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:340px\">\u003c/svg>\n \u003cdiv class=\"src\">BARCODE LOLLIPOP · MONO-EDITORIAL · LIVE OPS\u003c/div>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003c!-- 3 · arc bubble matrix -->\n \u003cdiv class=\"card\">\n \u003ch2>Eight products land in twelve cities\u003c/h2>\n \u003cdiv class=\"sub\">bubble = paying accounts · each row bows like a horizon\u003c/div>\n \u003csvg id=\"arcmatrix\" viewBox=\"0 0 430 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">ARC MATRIX · MONO-EDITORIAL · SALES GEO\u003c/div>\n \u003c/div>\n\n \u003c!-- 5 · radial convergence -->\n \u003cdiv class=\"card\">\n \u003ch2>48 requests pull toward five themes\u003c/h2>\n \u003cdiv class=\"sub\">every rim node = a feature request · lines bundle into the theme that claimed it\u003c/div>\n \u003csvg id=\"converge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RADIAL CONVERGENCE · MONO-EDITORIAL · FEEDBACK BOARD\u003c/div>\n \u003c/div>\n\n \u003c!-- 6 · cluster field, wide -->\n \u003cdiv class=\"card wide\">\n \u003ch2>The contributor field\u003c/h2>\n \u003cdiv class=\"sub\">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution\u003c/div>\n \u003csvg id=\"clusters\" viewBox=\"0 0 840 320\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:360px\">\u003c/svg>\n \u003cdiv class=\"src\">CLUSTER FIELD · MONO-EDITORIAL · CONTRIBUTOR GRAPH\u003c/div>\n \u003c/div>\n\n\n\n \u003c!-- 1 · brand spectrum -->\n \u003cdiv class=\"card\">\n \u003ch2>Where the brand sits\u003c/h2>\n \u003cdiv class=\"sub\">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors\u003c/div>\n \u003csvg id=\"spectrum\" viewBox=\"0 0 460 300\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">BRAND SPECTRUM · MONO-EDITORIAL2 · BRAND SURVEY\u003c/div>\n \u003c/div>\n\n \u003c!-- 2 · dotty matrix -->\n \u003cdiv class=\"card\">\n \u003ch2>Four squads, stacked in space\u003c/h2>\n \u003cdiv class=\"sub\">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed\u003c/div>\n \u003csvg id=\"dotty\" viewBox=\"0 0 400 372\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:340px\">\u003c/svg>\n \u003cdiv class=\"src\">DOTTY MATRIX · MONO-EDITORIAL2 · SPRINT BOARD\u003c/div>\n \u003c/div>\n\n \u003c!-- 3 · ticket almanac, wide -->\n \u003cdiv class=\"card wide\">\n \u003ch2>Eight years of tickets, one almanac\u003c/h2>\n \u003cdiv 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\u003c/div>\n \u003csvg id=\"almanac\" viewBox=\"0 0 840 400\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:420px\">\u003c/svg>\n \u003cdiv class=\"src\">BUBBLE ALMANAC · MONO-EDITORIAL2 · SUPPORT DESK\u003c/div>\n \u003c/div>\n\n \u003c!-- 4 · deploy patchwork -->\n \u003cdiv class=\"card\">\n \u003ch2>A quarter of deploys, overlaid\u003c/h2>\n \u003cdiv class=\"sub\">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up\u003c/div>\n \u003csvg id=\"patchwork\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RADIAL PATCHWORK · MONO-EDITORIAL2 · DEPLOY LOG\u003c/div>\n \u003c/div>\n\n \u003c!-- 6 · trend lineage -->\n \u003cdiv class=\"card\">\n \u003ch2>Features rise, fall, come back\u003c/h2>\n \u003cdiv class=\"sub\">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today\u003c/div>\n \u003csvg id=\"lineage\" viewBox=\"0 0 400 340\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TREND LINEAGE · MONO-EDITORIAL2 · PRODUCT HISTORY\u003c/div>\n \u003c/div>\n\n \u003c!-- 8 · type colonnade -->\n \u003cdiv class=\"card\">\n \u003ch2>Forty-four repos, ten owners\u003c/h2>\n \u003cdiv class=\"sub\">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew\u003c/div>\n \u003csvg id=\"colonnade\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">TYPE COLONNADE · MONO-EDITORIAL2 · CODE OWNERSHIP\u003c/div>\n \u003c/div>\n\n \u003c!-- 9 · funnel hourglass -->\n \u003cdiv class=\"card\">\n \u003ch2>The funnel, poured\u003c/h2>\n \u003cdiv class=\"sub\">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort\u003c/div>\n \u003csvg id=\"hourglass\" viewBox=\"0 0 400 330\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HOURGLASS STREAM · MONO-EDITORIAL2 · GROWTH\u003c/div>\n \u003c/div>\n\n \u003c!-- 14 · hundred field -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge pair\">小数据 · 单位分解\u003c/span>\n \u003ch2>A hundred of us, four minds\u003c/h2>\n \u003cdiv class=\"sub\">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey\u003c/div>\n \u003csvg id=\"hundredfield\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">HUNDRED FIELD · MONO-SMALL · ATTITUDE SURVEY\u003c/div>\n \u003c/div>\n\n \u003c!-- 15 · ballot tally -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge pair\">小数据 · 单位分解\u003c/span>\n \u003ch2>What they fear, tick by tick\u003c/h2>\n \u003cdiv class=\"sub\">one tick = one respondent in a hundred · inked = picked it · they could pick several\u003c/div>\n \u003csvg id=\"ballottally\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">BALLOT TALLY · MONO-SMALL · WORKPLACE SURVEY\u003c/div>\n \u003c/div>\n\n\n \u003c!-- L16 · matrix heat -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 编辑型 · 矩阵热力\u003c/span>\n \u003ch2>Which features get used together\u003c/h2>\n \u003cdiv class=\"sub\">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair\u003c/div>\n \u003csvg id=\"matheat\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">MATRIX HEAT · MONO-EDITORIAL · PRODUCT ANALYTICS\u003c/div>\n \u003c/div>\n\n \u003c!-- L17 · calendar heat -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">LUPI 编辑型 · 日历热力\u003c/span>\n \u003ch2>A year of deploys, day by day\u003c/h2>\n \u003cdiv class=\"sub\">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025\u003c/div>\n \u003csvg id=\"calheat\" viewBox=\"0 0 840 232\" preserveAspectRatio=\"xMidYMid meet\" style=\"max-height:260px\">\u003c/svg>\n \u003cdiv class=\"src\">CALENDAR HEAT · MONO-EDITORIAL · DEPLOY LOG\u003c/div>\n \u003c/div>\n\n \u003c!-- L19 · ridgeline -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 编辑型 · RIDGELINE\u003c/span>\n \u003ch2>Five pipelines, five tempos\u003c/h2>\n \u003cdiv class=\"sub\">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak\u003c/div>\n \u003csvg id=\"ridge\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">RIDGELINE · MONO-EDITORIAL · DATA PLATFORM\u003c/div>\n \u003c/div>\n\n \u003c!-- L20 · parallel coordinates -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">LUPI 编辑型 · 平行坐标\u003c/span>\n \u003ch2>Twelve products, four dimensions\u003c/h2>\n \u003cdiv class=\"sub\">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere\u003c/div>\n \u003csvg id=\"parallel\" viewBox=\"0 0 400 320\" preserveAspectRatio=\"xMidYMid meet\">\u003c/svg>\n \u003cdiv class=\"src\">PARALLEL COORDS · MONO-EDITORIAL · PRODUCT PANEL\u003c/div>\n \u003c/div>\n\n\u003c/div>\n\u003cscript>\n// ── shared Mono tokens ──\nconst INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic\nconst D2R=Math.PI/180;\nconst pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];\nconst sect=(cx,cy,r0,r1,a0,a1)=>{\n const big=a1-a0>180?1:0;\n const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);\n const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);\n return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;\n};\n// unified reveal: draw when scrolled into view; click replays\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>{n.innerHTML='';fn(n)};\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ 1 · launch fan ════\n// spokes sorted by launch week → the big dots cascade along a diagonal arc\n(()=>{\nconst FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];\nconst LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19]; // week of launch\nconst CX=40,CY=304,rOf=w=>56+w*9.7;\nobsReveal('fan',s=>{\n const ang=i=>-84+i*(76/11);\n // dotted week guides\n [5,10,15,20].forEach((w,gi)=>{\n const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);\n el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',\n stroke:'#D8D6CE','stroke-width':1,'stroke-dasharray':'2 5',\n class:'fade',style:`animation-delay:${gi*.1}s`});\n const [lx,ly]=pol(CX,CY,r,-87.5);\n txt(s,{x:lx,y:ly,'font-size':6.5,fill:'#B0AFA9','text-anchor':'middle',\n class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);\n });\n FEAT.forEach((name,i)=>{\n const a=ang(i),L=LAUNCH[i];\n const mau=Math.round(48-i*3+rnd(i+1,3)*10);\n // hairline spoke from launch to rim\n const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);\n el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:'#E3E2DB','stroke-width':.7,\n class:'fade',style:`animation-delay:${.15+i*.05}s`});\n // weekly texture dots\n for(let w=L+1;w\u003c=20;w++){\n const [x,y]=pol(CX,CY,rOf(w),a);\n el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:'#B0AFA9',opacity:.8,\n class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});\n }\n // launch dot, sized by MAU\n const [bx,by]=pol(CX,CY,rOf(L),a);\n const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:INK,\n class:'pop',style:`animation-delay:${.15+i*.07}s`});\n tip(big,`${name} — launched W${L} · ${mau}k MAU`);\n // rim label along the spoke\n const [tx,ty]=pol(CX,CY,rOf(20)+9,a);\n txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:'#6A6963',\n transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',\n class:'fade',style:`animation-delay:${.3+i*.05}s`},name);\n });\n});\n})();\n\n// ════ 4 · dot cascade on dark ════\n(()=>{\nconst CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];\nconst COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];\nobsReveal('cascade',s=>{\n const x=i=>34+i*22.2, base=i=>272-i*7.5;\n // dotted diagonal baseline\n el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,\n stroke:'#4A4944','stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});\n CAUSE.forEach((name,i)=>{\n const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);\n for(let k=0;k\u003cn-1;k++)\n el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:'#B3B0A4',opacity:.9,\n class:'pop',style:`animation-delay:${i*.05+k*.03}s`});\n const topY=by-12-(n-1)*8.8;\n const top=el(s,'circle',{cx:bx,cy:topY,r:4.6,fill:PAPER,\n class:'pop',style:`animation-delay:${i*.05+n*.03}s`});\n tip(top,`${name} — ${COUNT[i]} incidents`);\n txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:PAPER,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);\n txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:'#6A6963','text-anchor':'end',\n transform:`rotate(-90 ${bx} ${by+12})`,\n class:'fade',style:`animation-delay:${.1+i*.05}s`},name);\n });\n});\n})();\n\n// ════ 2 · barcode lollipop ════\n(()=>{\nconst N=90;\nconst val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;\nobsReveal('barcode',s=>{\n // the barcode: one hairline per day, full height\n for(let d=0;d\u003cN;d++){\n const x=18+d*8.53;\n el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,\n class:'fade',style:`animation-delay:${d*.004}s`});\n }\n const vs=Array.from({length:N},(_,d)=>val(d));\n // top-3 peaks, kept at least 6 days apart so labels never collide\n const top3=[];\n for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){\n if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);\n if(top3.length===3)break;\n }\n for(let d=0;d\u003cN;d++){\n const x=18+d*8.53,v=vs[d],y=252-v*.75;\n const weekend=d%7===5||d%7===6;\n const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);\n el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:INK,'stroke-width':1.1,\n class:'fade',style:`animation-delay:${.3+d*.008}s`});\n const big=top3.includes(d);\n const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,\n fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1.2:0,\n class:'pop',style:`animation-delay:${.3+d*.008}s`});\n tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);\n if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:INK,'text-anchor':'middle',\n class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');\n }\n [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>\n txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUTED,\n 'letter-spacing':'.12em',class:'fade'},m));\n});\n})();\n\n// ════ 3 · arc bubble matrix ════\n(()=>{\nconst PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];\nconst CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];\nconst W=[9,8,7,7,6,5,5,4,4,3,3,2]; // market weight\nobsReveal('arcmatrix',s=>{\n const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);\n const v=(i,j)=>{\n if(rnd(i*13+1,j*7+3)\u003c.09)return 0; // a few true absences\n const age=1-i*.085; // older rows reach further\n return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));\n };\n // find top-4 cells for labels\n const all=[];\n PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));\n const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);\n PROD.forEach((p,i)=>{\n // the horizon arc\n const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');\n el(s,'path',{d,fill:'none',stroke:'#E3E2DB','stroke-width':1,pathLength:1,\n class:'draw',style:`animation-delay:${i*.08}s`});\n txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.08}s`},p);\n CITY.forEach((c,j)=>{\n const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);\n if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:'#D8D6CE',\n class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}\n const fill=vv>=25?INK:vv>=12?'#6A6963':'#B0AFA9';\n const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,\n class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});\n tip(dot,`${p} · ${c} — ${vv} accounts`);\n if(top.includes(i*100+j))\n txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:INK,\n 'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);\n });\n });\n CITY.forEach((c,j)=>{\n const x=colX(j),y=74+dy(j)-24;\n txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.08em',\n transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);\n });\n});\n})();\n\n// ════ 5 · radial convergence ════\n(()=>{\nconst N=48,CX=200,CY=162,R=116;\nconst THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];\nobsReveal('converge',s=>{\n // hub assignment: contiguous blocks with a little organic leakage\n const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));\n const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];\n const hubPt=h=>pol(CX,CY,34,THEME[h][2]);\n const counts=[0,0,0,0,0];\n for(let i=0;i\u003cN;i++)counts[hubOf(i)]++;\n for(let i=0;i\u003cN;i++){\n const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);\n const h=hubOf(i),[hx,hy]=hubPt(h);\n const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;\n const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;\n el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,\n fill:'none',stroke:'#A8A7A0','stroke-width':.7,opacity:.55,pathLength:1,\n class:'draw',style:`animation-delay:${i*.018}s`});\n el(s,'circle',{cx:px,cy:py,r:1.6,fill:'#6A6963',\n class:'pop',style:`animation-delay:${i*.018}s`});\n // tiny rim code, flipped on the left half so it stays readable\n const flip=deg>90&°\u003c270,[lx,ly]=pol(CX,CY,R+7,deg);\n txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUTED,\n 'text-anchor':flip?'end':'start','dominant-baseline':'middle',\n transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,\n class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));\n }\n THEME.forEach(([name,,deg],h)=>{\n const [hx,hy]=hubPt(h);\n const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:INK,\n class:'pop',style:`animation-delay:${.5+h*.08}s`});\n tip(hub,`${name} — ${counts[h]} requests`);\n // theme label parked outside the rim, past the code ring\n const [tx,ty]=pol(CX,CY,R+34,deg);\n txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n 'dominant-baseline':'middle','letter-spacing':'.08em',\n style:`animation-delay:${.6+h*.08}s`,\n class:'fade'},`${name} · ${counts[h]}`);\n // hairline tying the label to its hub, skimming past the rim\n const [gx,gy]=pol(CX,CY,R+22,deg);\n el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:'#C6C5BF','stroke-width':.7,\n 'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});\n });\n});\n})();\n\n// ════ 6 · cluster field ════\n(()=>{\nconst SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];\nobsReveal('clusters',s=>{\n const HX=300,HY=172;\n // dotted cross-contribution paths, hub → each island\n SAT.forEach(([x,y],ci)=>{\n const mx=(HX+x)/2,my=(HY+y)/2-30;\n el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',\n stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,\n class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});\n });\n // main hub: 80 golden-angle spokes\n for(let i=0;i\u003c80;i++){\n const a=i*137.508,len=28+rnd(i+1,7)*88;\n const [x,y]=pol(HX,HY,len,a);\n el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:'#C6C5BF','stroke-width':.6,pathLength:1,\n class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:INK,opacity:.85,\n class:'pop',style:`animation-delay:${.15+i*.012}s`});\n tip(dot,`core contributor #${i+1}`);\n }\n el(s,'circle',{cx:HX,cy:HY,r:6.5,fill:INK,class:'pop'});\n txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n 'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px`,\n class:'fade'},'CORE · 80');\n // satellite islands\n SAT.forEach(([x,y,name,n],ci)=>{\n el(s,'circle',{cx:x,cy:y,r:3.5,fill:'#4A4944',\n class:'pop',style:`animation-delay:${.9+ci*.12}s`});\n for(let k=0;k\u003cn;k++){\n const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;\n const [dx,dy]=pol(x,y,rr,a);\n if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:'#D8D6CE','stroke-width':.5,\n class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});\n el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:'#6A6963',\n class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});\n }\n txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n 'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px`,\n class:'fade'},`${name} · ${n}`);\n });\n});\n})();\n\n// ════ 1 · brand spectrum ════\n// paired opposites; the pale ribbon threads our position down the page\n(()=>{\nconst ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];\nconst US=[.74,.80,.40,.22]; // 0 = left trait, 1 = right trait\nconst COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];\nconst X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);\nobsReveal('spectrum',s=>{\n // the ribbon first, underneath everything\n let d=`M${px(US[0])} ${Y(0)}`;\n for(let i=1;i\u003c4;i++)\n d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;\n el(s,'path',{d,fill:'none',stroke:'#F0EFEB','stroke-width':30,'stroke-linecap':'round',\n 'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',\n style:'animation-duration:1.4s'});\n ROWS.forEach(([a,b],i)=>{\n const y=Y(i);\n el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:'#B0AFA9','stroke-width':1,\n class:'fade',style:`animation-delay:${i*.08}s`});\n [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:'#B0AFA9','stroke-width':1,\n class:'fade',style:`animation-delay:${i*.08}s`}));\n txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',\n 'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);\n txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963',\n 'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);\n COMP[i].forEach((t,k)=>{\n const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:'#8F8E88',\n class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});\n tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);\n });\n const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:INK,\n class:'pop',style:`animation-delay:${.5+i*.1}s`});\n tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);\n });\n txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.12em',\n class:'fade',style:'animation-delay:.9s'},'● US ● COMPETITORS A · B · C');\n});\n})();\n\n// ════ 2 · dotty matrix — isometric stacked dot planes ════\n// pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:\n// each plane reads as its own quiet grid instead of a smudge\n(()=>{\nconst SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];\nconst SHADE=['#C6C5BF','#A8A7A0','#6A6963',INK];\nconst P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];\nobsReveal('dotty',s=>{\n for(let k=0;k\u003c4;k++){\n // plane slab: paper fill so upper decks occlude lower ones\n const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];\n el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',\n fill:PAPER,'fill-opacity':.96,stroke:'#DEDDD6','stroke-width':.9,\n class:'fade',style:`animation-delay:${k*.16}s`});\n for(let r=0;r\u003c6;r++)for(let c=0;c\u003c6;c++){\n const load=rnd(k*37+r*6+c+1,k+2);\n const v=load\u003c.3?0:Math.round(load*12); // sparser: silence is part of the texture\n const [x,y]=P(c,r,k);\n if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:'#D8D6CE',\n class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}\n const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],\n class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});\n tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);\n }\n // label off the plane's right corner, tied by a short hairline\n const [cxr,cyr]=P(5.8,-.8,k);\n el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:'#C6C5BF','stroke-width':.8,\n class:'fade',style:`animation-delay:${.2+k*.16}s`});\n txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?INK:'#8F8E88',\n 'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);\n }\n});\n})();\n\n// ════ 3 · bubble almanac ════\n// the vintage-almanac feel lives in three things: ledger-paper hairlines\n// under everything, bubbles big enough to collide, and marginalia. do all three.\n(()=>{\nconst AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];\nconst BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025}; // beta until this year\nconst 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};\nobsReveal('almanac',s=>{\n const rowY=i=>62+i*34,colX=j=>132+j*57;\n // ledger paper: hairlines every 6.8px, edge to edge, under everything\n for(let y=40;y\u003c=318;y+=6.8)\n el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:'#E4E3DC','stroke-width':.5,\n class:'fade',style:`animation-delay:${(y-40)*.001}s`});\n const vals=[];\n for(let i=0;i\u003c8;i++){\n const yr=2019+i;\n el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:'#D3D2CA','stroke-width':.9,\n class:'fade',style:`animation-delay:${i*.04}s`});\n txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:'#8F8E88','text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.04}s`},yr);\n AREAS.forEach((a,j)=>{\n if(yr\u003cBORN[a])return;\n const age=yr-BORN[a];\n // steep spread: quiet areas stay pinpricks, busy ones balloon\n const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));\n vals.push([Math.max(1,v),i,j,a,yr]);\n });\n }\n // draw small over large so the giants sit underneath as washes\n vals.sort((a,b)=>b[0]-a[0]);\n const top3=vals.slice(0,3);\n // hand-drawn circle: radius wobbles around the rim on two slow sine waves,\n // so every bubble is its own slightly-off print — never a compass circle\n const blob=(x,y,r,seed)=>{\n const n=Math.max(14,Math.round(r*1.6)),pts=[];\n for(let t=0;t\u003cn;t++){\n const a=t/n*Math.PI*2;\n const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;\n pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);\n }\n let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;\n for(let t=0;t\u003cn;t++){\n const p=pts[t],q=pts[(t+1)%n];\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)}`;\n }\n return d+' Z';\n };\n vals.forEach(([v,i,j,a,yr],k)=>{\n const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10, // jitter off the gridline\n y=rowY(i),r=Math.sqrt(v)*3.8; // big: rows are 34 apart, giants reach ~28 and collide\n const beta=BETA[a]&&yr\u003c=BETA[a];\n const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});\n const seed=i*12+j;\n const dot=el(g,'path',beta\n ?{d:blob(x,y,r,seed),fill:'none',stroke:'#8F8E88','stroke-width':1,'stroke-dasharray':'3 3'}\n :{d:blob(x,y,r,seed),fill:INK,'fill-opacity':.09+rnd(i+2,j+4)*.1,\n stroke:INK,'stroke-opacity':.3+rnd(i+5,j+7)*.35,'stroke-width':.6+rnd(i+3,j+11)*.9});\n tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);\n // escalation core: small wobbly blot, nudged off-center like a press mark\n if(!beta){\n const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;\n el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:INK});\n }\n if(top3.includes(vals[k]))\n txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:INK,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},v*10);\n });\n AREAS.forEach((a,j)=>{\n const x=colX(j);\n txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',\n transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);\n });\n // marginalia: two annotations with pointer hairlines, like the original's notes\n 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}`,\n fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});\n txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:'#6A6963','font-style':'italic',\n class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');\n 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}`,\n fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});\n txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:'#6A6963','font-style':'italic',\n class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');\n // footer strip: important events, the almanac's timeline shelf\n const evY=356;\n el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:'#C6C5BF','stroke-width':.8,\n class:'fade',style:'animation-delay:1.2s'});\n txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:'#8F8E88','letter-spacing':'.16em',\n class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');\n [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]\n .forEach(([yr,note],k)=>{\n const x=90+k*196;\n el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:'#8F8E88','stroke-width':1,\n class:'fade',style:`animation-delay:${1.3+k*.08}s`});\n txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);\n txt(s,{x,y:evY+15,'font-size':7,fill:'#8F8E88',\n class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);\n });\n});\n})();\n\n// ════ 4 · radial patchwork ════\n(()=>{\nconst CX=200,CY=168,N=46;\nobsReveal('patchwork',s=>{\n // 24h rim ticks\n for(let h=0;h\u003c96;h++){\n const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);\n el(s,'line',{x1,y1,x2,y2,stroke:'#C6C5BF','stroke-width':h%4===0?1:.5,\n class:'fade',style:`animation-delay:${h*.006}s`});\n }\n [0,6,12,18].forEach(h=>{\n const [x,y]=pol(CX,CY,157,-90+h*15);\n txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUTED,'text-anchor':'middle',\n class:'fade'},String(h).padStart(2,'0'));\n });\n // the patchwork: translucent wedges pile into ink\n const wedges=[];\n for(let i=0;i\u003cN;i++){\n // deploys cluster around 11h and 16h, thin overnight\n const peak=rnd(i+1,2)>.5?11:16;\n const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;\n const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;\n const r1=34+rnd(i+1,5)*100;\n wedges.push([a0,sw,r1,hour,i]);\n }\n wedges.forEach(([a0,sw,r1,hour,i])=>{\n const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:INK,\n 'fill-opacity':.07+rnd(i+1,6)*.09,class:'fade',\n style:`animation-delay:${.2+i*.022}s`});\n 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`);\n });\n // three outlined wedges: the ones that paged somebody\n [4,17,31].forEach((i,k)=>{\n const [a0,sw,r1]=wedges[i];\n const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',\n stroke:INK,'stroke-width':1.1,class:'fade',style:`animation-delay:${1.2+k*.15}s`});\n tip(w,`deploy #${i+1} — triggered an incident`);\n });\n el(s,'circle',{cx:CX,cy:CY,r:3,fill:INK,class:'pop',style:'animation-delay:1.3s'});\n txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');\n});\n})();\n\n// ════ 6 · trend lineage ════\n(()=>{\n// [name, events [year, type f|o], aliveToday]\nconst TR=[\n ['Dark mode',[[2017,'f'],[2022,'o']],true],\n ['Kanban',[[2016,'f']],true],\n ['Wikis',[[2016,'f'],[2019,'o']],false],\n ['Slash cmds',[[2018,'f']],true],\n ['Templates',[[2019,'f'],[2024,'o']],true],\n ['Inbox',[[2020,'f']],false],\n ['Live cursors',[[2020,'f'],[2023,'o']],true],\n ['AI drafts',[[2023,'f'],[2025,'o']],true],\n ['Voice notes',[[2021,'f']],false],\n ['Offline',[[2018,'f'],[2026,'o']],true],\n];\nconst Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;\nobsReveal('lineage',s=>{\n for(let yr=Y0;yr\u003c=2026;yr++){\n el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:'#E3E2DB','stroke-width':.8,\n class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});\n if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUTED,'text-anchor':'end',\n class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);\n }\n TR.forEach(([name,ev,alive],c)=>{\n const x=colX(c);\n // segments between events: solid while fresh, dashed once dormant\n for(let k=0;k\u003cev.length-1;k++){\n const gap=ev[k+1][0]-ev[k][0];\n el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,\n stroke:'#B0AFA9','stroke-width':1,\n 'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,\n class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});\n }\n const last=ev[ev.length-1][0];\n if(alive)\n el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:'#B0AFA9','stroke-width':1,\n pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});\n ev.forEach(([yr,t])=>{\n const node=el(s,'circle',t==='f'\n ?{cx:x,cy:yY(yr),r:5.5,fill:INK}\n :{cx:x,cy:yY(yr),r:5.5,fill:PAPER,stroke:INK,'stroke-width':1.4});\n node.setAttribute('class','pop');\n node.style.animationDelay=(.35+c*.07)+'s';\n tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);\n });\n // terminal dot: alive lands on today's baseline, dead stops where it stopped\n el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,\n fill:alive?INK:'#C6C5BF',class:'pop',style:`animation-delay:${.6+c*.07}s`});\n const ly=yY(2026)+26;\n txt(s,{x,y:ly,'font-size':6.8,'font-weight':600,fill:alive?'#4A4944':'#B0AFA9',\n transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',\n class:'fade',style:`animation-delay:${.7+c*.05}s`},name);\n });\n});\n})();\n\n// ════ 8 · type colonnade ════\n(()=>{\nconst PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];\nconst SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];\nconst TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];\nobsReveal('colonnade',s=>{\n const N=44;\n const teamY=j=>36+j*28,repoY=i=>16+i*6.7;\n const counts=Array(10).fill(0);\n const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard\n for(let i=0;i\u003cN;i++)counts[owner(i)]++;\n for(let i=0;i\u003cN;i++){\n const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');\n const yi=repoY(i),j=owner(i),yj=teamY(j);\n txt(s,{x:112,y:yi+2,'font-size':5,fill:'#8F8E88','text-anchor':'end',\n class:'fade',style:`animation-delay:${i*.012}s`},name);\n el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:'#B0AFA9',\n class:'fade',style:`animation-delay:${i*.012}s`});\n el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',\n stroke:'#A8A7A0','stroke-width':.6,opacity:.6,pathLength:1,\n class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});\n }\n TEAM.forEach((t,j)=>{\n const y=teamY(j);\n el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:counts[j]>=7?INK:counts[j]>=4?'#6A6963':'#B0AFA9',\n class:'pop',style:`animation-delay:${.6+j*.05}s`});\n const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:'#4A4944',\n class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);\n tip(lab,`${t} owns ${counts[j]} repos`);\n });\n});\n})();\n\n// ════ 9 · hourglass stream ════\n(()=>{\nconst STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];\nobsReveal('hourglass',s=>{\n const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;\n STAGE.forEach(([name,c],k)=>{\n const y=sy(k),hw=w(c)/2;\n // barcode strip: one tick ≈ 40 people\n const n=Math.round(c/40);\n for(let t=0;t\u003cn;t++){\n const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;\n el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:INK,'stroke-width':.8,\n opacity:.45+rnd(t+2,k+5)*.5,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});\n }\n // threads trickling to the next strip\n if(k\u003c4){\n const hw1=w(STAGE[k+1][1])/2;\n for(let t=0;t\u003c34;t++){\n const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;\n const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;\n el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,\n fill:'none',stroke:'#B0AFA9','stroke-width':.5,opacity:.32,pathLength:1,\n class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});\n }\n // conversion between stages, parked in the left margin\n const pct=Math.round(STAGE[k+1][1]/c*100);\n txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':8.5,'font-weight':800,fill:'#8F8E88',\n class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');\n txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:'#C6C5BF',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');\n }\n // stage label, tied to the strip edge\n el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:'#DEDDD6','stroke-width':.8,\n class:'fade',style:`animation-delay:${.3+k*.12}s`});\n txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:'#4A4944',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);\n txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:INK,\n class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());\n });\n});\n})();\n\n// ════ 14 · hundred field ════\n// small data, unit-decomposed: four shares of 100% become 100 people.\n// phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.\n(()=>{\nconst SEG=[['CHARGED',41,INK],['TORN',35,'#55554F'],['ADRIFT',12,'#8F8E88'],['AVERSE',12,'#B0AFA9']];\nconst POS=[[132,140],[276,116],[186,252],[322,238]];\nobsReveal('hundredfield',s=>{\n // dashed constellation hairlines tying the cluster cores\n [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{\n el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],\n stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',\n class:'fade',style:`animation-delay:${.9+k*.1}s`});\n });\n SEG.forEach(([name,v,shade],ci)=>{\n const [cx,cy]=POS[ci];\n let edge=0;\n for(let k=0;k\u003cv;k++){\n const a=k*137.508+ci*55;\n const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;\n edge=Math.max(edge,rr);\n const [x,y]=pol(cx,cy,rr,a);\n // hairline spoke for every 5th person, core to dot\n if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:'#CDCCC5','stroke-width':.6,\n class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});\n const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,\n class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});\n tip(dot,`${name} — one of ${v} in 100`);\n }\n el(s,'circle',{cx,cy,r:2.4,fill:INK,class:'pop',style:`animation-delay:${ci*.14}s`});\n txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',\n 'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+ci*.12}s`,\n class:'fade'},`${name} · ${v}`);\n });\n txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');\n});\n})();\n\n// ════ 15 · ballot tally ════\n// multi-select survey (each option independent, 0–100): every option is a row of\n// 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.\n(()=>{\nconst ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];\nobsReveal('ballottally',s=>{\n ASK.forEach(([name,v],i)=>{\n const base=78+i*62;\n txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:'#6A6963',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);\n el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,\n class:'fade',style:`animation-delay:${i*.1}s`});\n for(let k=0;k\u003c100;k++){\n const x=28+k*3.44,picked=k\u003cv;\n const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;\n el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,\n stroke:picked?INK:'#CFCEC7','stroke-width':picked?.9:.55,\n class:'fade',style:`animation-delay:${i*.1+k*.006}s`});\n if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',\n class:'fade',style:`animation-delay:${i*.1+k*.006}s`});\n }\n const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':11,'font-weight':800,fill:INK,\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+i*.1}s`,\n class:'fade'},v);\n tip(lab,`${v} of 100 picked this — they could pick several`);\n });\n txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');\n});\n})();\n\n\n// ── New chart-family helpers (scoped to the added templates) ──\n(()=>{\nconst INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';\nconst NS='http://www.w3.org/2000/svg';\nconst 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};\nconst txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};\nconst tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};\nconst rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;\n\n// ════ L16 · matrix heat ════\n// 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,\n// 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。\n// 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,\n// 值是占比/强度而不是可数件数。\n(()=>{\nconst FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];\nconst v=(i,j)=>{\n if(i===j)return -1;\n const a=Math.min(i,j),b=Math.max(i,j);\n const pop=(8-a)/8*(8-b)/8; // 头部功能重合度高\n const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);\n return Math.round(raw);\n};\nconst shade=t=>t>36?INK:t>24?'#4A4944':t>14?'#8F8E88':t>6?'#B0AFA9':'#D8D7D1';\nobsReveal('matheat',s=>{\n const X0=118,Y0=70,P=26,C=22;\n let max=0,mi=0,mj=0;\n for(let i=0;i\u003c8;i++)for(let j=0;j\u003c8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}\n FEAT.forEach((f,i)=>{\n txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'#6A6963','text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);\n const cy=Y0-10;\n txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',\n transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);\n });\n for(let i=0;i\u003c8;i++)for(let j=0;j\u003c8;j++){\n const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;\n if(t\u003c0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'#D8D6CE',\n class:'fade',style:`animation-delay:${d}s`});continue}\n if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'#D8D6CE',\n class:'pop',style:`animation-delay:${d}s`});continue}\n const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),\n class:'pop',style:`animation-delay:${d}s`});\n tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);\n if(i===mi&&j===mj){\n el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',\n stroke:INK,'stroke-width':1,'stroke-dasharray':'2 3',\n class:'fade',style:'animation-delay:.9s'});\n txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',\n class:'fade',style:'animation-delay:1s'},t);\n }\n }\n // shade legend, countable buckets\n const LG=[['#D8D7D1','1–6'],['#B0AFA9','7–14'],['#8F8E88','15–24'],['#4A4944','25–36'],[INK,'37+']];\n LG.forEach(([c,lab],k)=>{\n const x=118+k*54;\n el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});\n txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#8F8E88',\n class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');\n });\n txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.1em',\n class:'fade',style:'animation-delay:1s'},'SHADE = %');\n});\n})();\n\n// ════ L17 · calendar heat ════\n// 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,\n// 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,\n// 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。\n(()=>{\nconst MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];\nconst DAY=['MON','WED','FRI','SUN'];\nconst val=(w,d)=>{\n const wd=d\u003c5;\n const season=1+.55*Math.sin((w-8)/9); // 春秋两个发布季\n if(!wd)return rnd(w*7+d+1,3)>.82?1:0; // 周末基本静默\n const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);\n return Math.round(raw);\n};\nobsReveal('calheat',s=>{\n const X0=56,Y0=42,P=14.8;\n let max=0,mw=0,md=0;\n for(let w=0;w\u003c52;w++)for(let d=0;d\u003c7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}\n DAY.forEach((lab,k)=>{\n 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',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);\n });\n MON.forEach((m,k)=>{\n const x=X0+Math.round(k*52/12)*P;\n txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',\n class:'fade',style:`animation-delay:${k*.03}s`},m);\n el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'#C6C5BF','stroke-width':.7,\n class:'fade',style:`animation-delay:${k*.03}s`});\n });\n for(let w=0;w\u003c52;w++)for(let d=0;d\u003c7;d++){\n const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;\n if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'#D8D6CE',\n class:'pop',style:`animation-delay:${delay}s`});continue}\n const r=1.1+Math.sqrt(t)*1.55;\n const dot=el(s,'circle',{cx:x,cy:y,r,\n fill:t>max*.66?INK:t>max*.33?'#6A6963':'#B0AFA9',\n class:'pop',style:`animation-delay:${delay}s`});\n tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);\n }\n // peak: dashed ring + marginalia hairline\n const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;\n el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:INK,'stroke-width':1,\n 'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});\n el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,\n fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});\n txt(s,{x:px+78,y:py+39,'font-size':7,fill:'#6A6963','font-style':'italic',\n class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);\n txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');\n});\n})();\n\n// ════ L19 · ridgeline ════\n// 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成\n// (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上\n// 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。\n(()=>{\nconst 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]];\nobsReveal('ridge',s=>{\n const X0=92,X1=372,MAXH=24,N=72,LIFT=54;\n const rowY=i=>96+i*44;\n ROWS.forEach(([name,c,sp],i)=>{\n const y0=rowY(i);\n const dens=Array.from({length:N},(_,k)=>{\n const h=k/(N-1)*MAXH;\n return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));\n });\n const dmax=Math.max(...dens);\n const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);\n const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');\n el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,\n class:'fade',style:`animation-delay:${i*.1}s`});\n pts.forEach(([px,py],k)=>{\n if(k%2)return;\n el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:'#8F8E88','stroke-width':.5,\n opacity:.28+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});\n });\n el(s,'path',{d:crest,fill:'none',stroke:INK,'stroke-width':.9,pathLength:1,\n class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});\n el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':.7,\n class:'fade',style:`animation-delay:${i*.1}s`});\n const pk=dens.indexOf(dmax),[px,py]=pts[pk];\n const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:INK,class:'pop',style:`animation-delay:${.6+i*.1}s`});\n tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);\n txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:INK,\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,\n class:'fade'},c.toFixed(1)+'h');\n txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',\n 'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);\n });\n const yb=rowY(4);\n [0,6,12,18,24].forEach(h=>{\n const x=X0+h/MAXH*(X1-X0);\n el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'#CFCEC7','stroke-width':.6,\n class:'fade',style:'animation-delay:.8s'});\n txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',\n class:'fade',style:'animation-delay:.85s'},h+'h');\n });\n txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},\n 'EACH CREST = ONE PIPELINE · THE AREA IS MADE OF HAIRLINES · DOT = THE PEAK');\n});\n})();\n\n// ════ L20 · parallel coordinates ════\n// 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。\n// 线束的形状就是数据结构(哪两维相关、谁全线领先)。\n// 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。\n(()=>{\nconst DIMS=[['PRICE

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…

,8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];\nconst P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],\n ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],\n ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],\n ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];\nobsReveal('parallel',s=>{\n const AX=[64,156,248,340],TOP=64,BOT=252;\n const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};\n // hero: judged on csat+retention+growth (price is a fact, not a virtue)\n const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;\n const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);\n DIMS.forEach(([name,lo,hi],di)=>{\n el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#B0AFA9','stroke-width':1,\n class:'fade',style:`animation-delay:${di*.06}s`});\n [[TOP-8],[BOT+8]].forEach(([yy])=>\n el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#B0AFA9','stroke-width':1,\n class:'fade',style:`animation-delay:${di*.06}s`}));\n txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'#6A6963','text-anchor':'middle',\n 'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);\n txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'#C6C5BF',\n class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);\n txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'#C6C5BF',\n class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);\n });\n P.forEach((p,i)=>{\n const isHero=p===hero;\n const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];\n let d=`M${AX[0]} ${ys[0].toFixed(1)}`;\n for(let k=0;k\u003c3;k++){\n const mx=(AX[k]+AX[k+1])/2;\n d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;\n }\n const ln=el(s,'path',{d,fill:'none',\n stroke:isHero?INK:'#8F8E88','stroke-width':isHero?2:.65,\n opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,\n class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});\n tip(ln,`${p[0]} — ${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);\n ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,\n fill:isHero?INK:'#8F8E88',opacity:isHero?1:.7,\n class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));\n if(isHero)\n txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:INK,\n style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,\n class:'fade'},p[0]);\n });\n txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',\n 'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},\n 'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');\n});\n})();\n})();\n\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":58153,"content_sha256":"158f9258a111f299f18774dc0b2d48834f0c3c49bf77aafbbf57380cdc407bac"},{"filename":"templates/maps-gallery.html","content":"\u003c!doctype html>\n\u003chtml lang=\"zh\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Lieflat Charts · Maps · 2 张参考实现\u003c/title>\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n:root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}\n *{margin:0;padding:0;box-sizing:border-box}\n body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}\n .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}\n .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}\n .card.wide{grid-column:1/-1}\n h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}\n .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}\n .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}\n .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;\n padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #B0AFA9;color:#8F8E88}\n svg{width:100%;max-height:330px;display:block;margin:0 auto}\n svg text{font-family:'Inter',sans-serif}\n .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}\n @keyframes pop{from{transform:scale(0)}to{transform:none}}\n .fade{animation:fade .9s ease both}\n @keyframes fade{from{opacity:0}}\n .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}\n @keyframes draw{to{stroke-dashoffset:0}}\n @media (prefers-reduced-motion:reduce){\n .pop,.fade{animation:none}\n .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}\n }\n .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}\n .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}\n .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"pagehead\">\n \u003ch1>Maps · 2 张参考实现\u003c/h1>\n \u003cp>仅在用户明确要求地图或地域分布时使用。M1 为美国州级,M2 为世界国家级;地图数据与 ECharts 通过网络加载。\u003c/p>\n\u003c/div>\n\u003cdiv class=\"grid2\">\n \u003c!-- M1 · US Choropleth -->\n \u003cdiv class=\"card\">\n \u003cspan class=\"badge\">MAPS · US CHOROPLETH\u003c/span>\n \u003ch2>Sign-ups across the states\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapus\" style=\"height:340px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · MONO-MAP · GROWTH GEO\u003c/div>\n \u003c/div>\n\n \u003c!-- M2 · World Choropleth -->\n \u003cdiv class=\"card wide\">\n \u003cspan class=\"badge\">MAPS · WORLD CHOROPLETH\u003c/span>\n \u003ch2>Where the users are\u003c/h2>\n \u003cdiv class=\"sub\">real outlines · shade = monthly active accounts · gray = markets we haven't opened\u003c/div>\n \u003cdiv class=\"ch\" id=\"mapworld\" style=\"height:400px\">\u003c/div>\n \u003cdiv class=\"src\">CHOROPLETH · MONO-MAP · GLOBAL OPS\u003c/div>\n \u003c/div>\n\u003c/div>\n\u003cscript>\nconst INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';\nconst obsReveal=(id,fn)=>{\n const n=document.getElementById(id);\n const go=()=>fn(n);\n const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});\n io.observe(n);\n n.style.cursor='pointer';\n n.addEventListener('click',go);\n};\n\n// ════ M1 · US choropleth ════\n// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。\n// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角\n// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。\n// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、\n// 纸色描边当州界发丝、无数据州留 #E4E3DC(沉默可见)。\n// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。\n// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线\n// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。\nconst MAP_LEGEND={\n itemWidth:11,itemHeight:11,itemSymbol:'rect',\n textStyle:{fontFamily:'Inter',fontSize:9,color:'#8F8E88'},\n inRange:{color:['#D8D7D1','#B0AFA9','#8F8E88','#4A4944','#1C1C1A']},\n};\n(()=>{\nconst D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,\n 'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,\n 'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,\n 'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,\n 'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,\n 'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,\n 'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};\nfetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-usa',geo,{\n Alaska:{left:-131,top:25,width:15},\n Hawaii:{left:-110,top:28,width:5},\n 'Puerto Rico':{left:-76,top:26,width:2},\n });\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},\n visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-usa',top:10,bottom:48,\n itemStyle:{areaColor:'#E4E3DC',borderColor:PAPER,borderWidth:1},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,\n fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),\n }],\n };\n obsReveal('mapus',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\n// ════ M2 · World choropleth ════\n// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,\n// 关键市场(≥39k)直接把数值标在版图上。\n(()=>{\nconst D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,\n 'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,\n 'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,\n 'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,\n 'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,\n 'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,\n 'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,\n 'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};\nconst BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],\n 'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};\nfetch('https://cdn.jsdelivr.net/npm/[email protected]/map/json/world.json')\n .then(r=>r.json()).then(geo=>{\n echarts.registerMap('mono-world',geo);\n const opt={\n animationDuration:900,animationEasing:'quarticOut',\n tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],\n textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},\n formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},\n visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',\n pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},\n {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],\n ...MAP_LEGEND},\n series:[{type:'map',map:'mono-world',top:4,bottom:36,\n boundingCoords:[[-170,76],[190,-58]], // crop antarctica\n itemStyle:{areaColor:'#E4E3DC',borderColor:PAPER,borderWidth:.7},\n emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,\n textBorderColor:PAPER,textBorderWidth:3},\n itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.1}},\n select:{disabled:true},\n data:Object.entries(D).map(([n,v])=>({name:n,value:v,\n label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,\n fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),\n }],\n };\n obsReveal('mapworld',elDom=>{\n const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);\n g.clear();g.setOption(opt);\n });\n });\n})();\n\naddEventListener('resize',()=>{\n ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());\n});\n\u003c/script>\n\u003c/body>\n\u003c/html>\n","content_type":"text/html; charset=utf-8","language":"markup","size":9786,"content_sha256":"984728e65def8c0e087ec4bede2e6bb6a73f3497a022eed3969be7efdf2c05d2"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Lieflat Charts — 图表品味法典","type":"text"}]},{"type":"paragraph","content":[{"text":"Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。","type":"text"},{"text":"默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 ","type":"text","marks":[{"type":"strong"}]},{"text":"presentation-catalog.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 与 ","type":"text","marks":[{"type":"strong"}]},{"text":"references/presentation.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":",生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取 ","type":"text","marks":[{"type":"strong"}]},{"text":"report-catalog.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":"。","type":"text","marks":[{"type":"strong"}]},{"text":" 图表模式下","type":"text"},{"text":"必须先从 Lupi Editorial 和 Lupi Basics 中选型;只有两者都没有合适模板,或用户明确要求 Glance / dashboard / 三秒快读时,才允许使用 Glance。Maps 只有在用户明确说要地图或地域分布时才启用。","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"颜色无需用户显式触发。按第六点五节在 Mono 与 ","type":"text"},{"text":"color-presets.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的三套内置方案之间自动判断;用户明确给出品牌色或自定义色值时,可以建立一套交付内的 custom 色板。同一份 HTML 或同一组交付只能锁定一种色彩系统,不得混用。换肤样张在 ","type":"text"},{"text":"templates/color/","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]},{"type":"paragraph","content":[{"text":"署名提示:","type":"text","marks":[{"type":"strong"}]},{"text":" 每次成功生成图表或报告并向用户交付成品后,在回复末尾主动提示:“本 skill 由‘躺在废墟里’开发,若公开分发内容请署名或@开发者。”这只是建议,不要强行写入用户的图表或报告内容,也不要在 README 中重复这条提示。","type":"text"}]},{"type":"paragraph","content":[{"text":"怎么查一张图的参考代码","type":"text","marks":[{"type":"strong"}]},{"text":":catalog 查到图型 → 打开对应 gallery 文件 → 按卡内标题找到 ","type":"text"},{"text":"\u003cdiv class=\"card\">","type":"text","marks":[{"type":"code_inline"}]},{"text":" 块看结构 → 在 ","type":"text"},{"text":"\u003cscript>","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里搜同名 ","type":"text"},{"text":"// ════","type":"text","marks":[{"type":"code_inline"}]},{"text":" 注释块拿渲染代码。不要整页照抄——gallery 是多卡合页,交付给用户的永远是按第九节骨架组装的单图文件。","type":"text"}]},{"type":"paragraph","content":[{"text":"目标用户是非程序员","type":"text","marks":[{"type":"strong"}]},{"text":"(写作者、运营、做 PPT 的人)。他们说人话(\"帮我把这季度转化画一下,发公众号用\"),不说图型名。你的职责是把人话翻译成正确的图,而且好看到能直接发出去。","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"零、输出模式与模板优先硬约束","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"0.1 先判输出模式","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"默认图表模式","type":"text","marks":[{"type":"strong"}]},{"text":":用户只给数据,或说“可视化这组数据 / 分析数据 / 做一张图 / 做几张图 / 做个图表页 / 做 PPT 插图”,都必须生成图表或图表页。没有报告关键词时,不得自行套用 R01–R12,也不得因为数据很丰富、结论很多或用户说“分析”就升级成报告。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"报告模式","type":"text","marks":[{"type":"strong"}]},{"text":":只有用户明确说“生成报告 / 报告模板 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付,才读取 ","type":"text"},{"text":"report-catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":",按报告类型、内容结构、版心、密度、阅读速度和语言选择一个 R01–R12。目录里的场景只是召回线索,不是对模板的限制。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"有歧义时仍选图表模式","type":"text","marks":[{"type":"strong"}]},{"text":":如果用户同时说了“分析”但没有明确要求报告,先交付最强单图或 2–3 张证据图;可以在文字中提示“如需完整报告可再切换”,但不能直接生成报告。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"演示模式边界","type":"text","marks":[{"type":"strong"}]},{"text":":用户说“做 PPT 插图 / 放进 PPT”仍是图表模式;用户说“做一套 PPT”才是演示模式。演示模式交付为 HTML 或 PDF,不得未经确认把 HTML/PDF 称为可编辑 ","type":"text"},{"text":".pptx","type":"text","marks":[{"type":"code_inline"}]},{"text":"。**用户提供的 PPT、截图或 HTML 只能作为画布、字体、留白、页眉页脚和页面编排的参考,不是数据来源,也不能决定图型。数据、单位、时间范围、来源、标题结论和无障碍描述必须来自用户材料或明确标注的计算;每页图表仍按 ","type":"text"},{"text":"catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的数据形状选择。**不能因为演示版式而默认 Glance、照搬模板示例数字或发明新图型。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不要把报告模式当成“多画几张图”","type":"text","marks":[{"type":"strong"}]},{"text":"。报告模板决定整页结构;页内图表仍必须遵守 ","type":"text"},{"text":"catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 和本文件的图表规则。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"报告模板提供 ","type":"text"},{"text":".zh.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" 与 ","type":"text"},{"text":".en.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" 两个版本。用户没有指定语言时,跟随输入语言;中英文内容不能混在同一版本中。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"以下规则不是建议,违反任意一条都必须返工:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"必须从仓库模板生成。","type":"text","marks":[{"type":"strong"}]},{"text":" 每张成品图先在 ","type":"text"},{"text":"catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 锁定图型编号,再打开对应 gallery 的真实实现:Lupi 使用 ","type":"text"},{"text":"templates/lupi-gallery.html","type":"text","marks":[{"type":"code_inline"}]},{"text":",基础型使用 ","type":"text"},{"text":"templates/basics-gallery.html","type":"text","marks":[{"type":"code_inline"}]},{"text":",Glance 使用 ","type":"text"},{"text":"templates/glance-gallery.html","type":"text","marks":[{"type":"code_inline"}]},{"text":",地图使用 ","type":"text"},{"text":"templates/maps-gallery.html","type":"text","marks":[{"type":"code_inline"}]},{"text":",交互大图使用 ","type":"text"},{"text":"templates/big-*.html","type":"text","marks":[{"type":"code_inline"}]},{"text":"。彩色图仍以这些原版模板为结构正本,","type":"text"},{"text":"templates/color/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 只用于查配色。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"必须沿用所选模板的代码骨架。","type":"text","marks":[{"type":"strong"}]},{"text":" 从卡内标题对应的 ","type":"text"},{"text":"\u003cdiv class=\"card\">","type":"text","marks":[{"type":"code_inline"}]},{"text":" 和同名 ","type":"text"},{"text":"// ════ 图型名 ════","type":"text","marks":[{"type":"code_inline"}]},{"text":" 注释块出发,保留其核心 SVG / Canvas / ECharts 结构、数据编码方式、比例关系和动画节奏。允许替换数据、标题、旁注、来源与必要布局;禁止脱离模板另画一张“看起来差不多”的图,禁止拼接多个模板造混合图型,禁止退回图表库默认样式。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"默认选型顺序固定。","type":"text","marks":[{"type":"strong"}]},{"text":" 先完整比较 Lupi Editorial(L1–L19),再比较 Lupi Basics(F1–F17)。只要其中存在能诚实承载数据、容纳标签且可读的模板,就必须从这两组中选择。Maps 不进入这条默认链,只有用户明确要求地图时才单独检查。 3.1 ","type":"text"},{"text":"两组之内还分主力与后备。","type":"text","marks":[{"type":"strong"}]},{"text":" 主力是 ","type":"text"},{"text":"L1–L15 与 F1–F13","type":"text","marks":[{"type":"strong"}]},{"text":",默认从这里出图。后备是 ","type":"text"},{"text":"L16–L20、F14–F17、G19–G22","type":"text","marks":[{"type":"strong"}]},{"text":":只有主力里没有能诚实编码这份数据的模板时才用,且必须写明主力的哪几张、因为什么不适配。理由不得只写“新图更好看”或“更专业”。唯一例外见第 3.2 条。 3.2 ","type":"text"},{"text":"五种数据形状允许直接用后备图,不必先证伪主力。","type":"text","marks":[{"type":"strong"}]},{"text":" 因为主力里不存在对应的诚实编码,硬套只会画出错的图:OHLC 四值行情 → F17 Candlestick;五数概括 + 异常值 → F15 Tick Box;同一实体跨 3–6 个连续维度 → L20 Parallel Coordinates;整年 52 周 × 7 天日期热力 → L17 Calendar Heat;多系列构成随连续时间变化且要同时看总量 → F16 Stream Ribbon。除这五种,后备图一律走 3.1 条。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Glance 是默认降级方案,不是并列首选。","type":"text","marks":[{"type":"strong"}]},{"text":" 只有 Lupi Editorial 与 Lupi Basics 都不适配,或用户明确要求 Glance、dashboard、监控、周报、三秒快读时,才使用 Glance。降级前必须写明 Lupi / Basics 不适配的具体原因。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"库外新造是最后手段。","type":"text","marks":[{"type":"strong"}]},{"text":" Lupi、Basics、Glance 和交互模板都无法承载时,才能走第六节翻译流程;新图仍必须继承最接近 gallery 模板的视觉语法和代码结构。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"一、工作流程(每次请求都走这六步)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"判数据形状。","type":"text","marks":[{"type":"strong"}]},{"text":" 别问用户要什么图,看他的数据长什么样:几个类目的比较?带时间的序列?占比?带正负?多对一归属?网络?逐条记录的分布?形状是选图的主键。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先审计主力,再考虑后备。","type":"text","marks":[{"type":"strong"}]},{"text":" 按数据形状先扫描主力 L1–L15 与 F1–F13 的候选,至少比较 3 个;不足 3 个就列出全部。比较语义契合、单位诚实、标签容纳、阅读速度、叙事张力和本批次是否重复。主力全部不适配时,才扫描后备 L16–L20 / F14–F17,并写明主力哪几张为什么不行(第零节 3.1)。数据形状命中第零节 3.2 那五种时,直接用对应后备图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"必要时才检查 Glance。","type":"text","marks":[{"type":"strong"}]},{"text":" 只有 Lupi 与 Basics 候选全部失败,或用户明确要求 Glance / dashboard / 监控 / 周报 / 三秒快读,才扫描 20 张 Glance 候选(G3–G22)。选择 Glance 时记录为什么 Lupi 与 Basics 无法承载,不得只写“Glance 更直观”。用户明确要求地图时跳到 Maps(M1–M2),不把地图混入普通候选。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"锁定真实模板,再组织整页。","type":"text","marks":[{"type":"strong"}]},{"text":" 每张图必须记录体系、图型编号、gallery 文件和卡内标题,并以该卡的真实结构与渲染代码为骨架。禁止先想好“这一页要讲六件事”,再临时发明图型;整页叙事只能在模板锁定之后组织。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"按图数规则组成批次。","type":"text","marks":[{"type":"strong"}]},{"text":" 一张图只承担一个独立结论;去掉重复结论后,再按第 1.2 节的默认区间决定数量。模板需要全局分配:不重复、不堆同一种轮廓、不为了凑数硬加图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"按模板渲染并自检","type":"text","marks":[{"type":"strong"}]},{"text":"(第零、二、三、八节)。逐项核对成品是否仍能对应到所选 gallery 实现;不能因为改数据而换掉模板的核心几何、编码或动效。按第六点五节为整份交付选择 Mono 或一套彩色预设;库外图型走翻译流程(第六节)。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1.1 报告模式工作流","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"读取 ","type":"text"},{"text":"report-catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":",至少比较 3 个报告候选并记录淘汰理由。按内容结构、信息密度、版心和阅读速度选模板,不把模板名称当成行业限制;例如“旅行手记”也可以承载运动或个人生活数据,“月度运营”也可以承载财报或业务数据报告。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"锁定一个语言版本和一个报告模板文件;不得拼接两个报告模板的版式。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先提炼整页的主结论、证据结论、上下文说明和来源,再把它们分配到模板已有的标题、导语、KPI、图表、旁注和结尾槽位。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"每个图表槽位单独按 ","type":"text"},{"text":"catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 选型,优先复用 Lupi / Basics 的真实实现;报告模板的布局不能成为绕过图表数据契约的理由。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"复制对应报告模板的整份 HTML 作为起点,只替换数据、文案、来源、图例、语言和必要模块。不得保留演示数据、演示来源、Moxt 链接或模板原始结论。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"报告页的色彩系统锁定为模板当前色系;如用户明确要求改色,整页统一替换为 Mono、单一内置预设或完整 custom 色板,不得局部混搭。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"报告模式的交付前额外检查:版心尺寸未漂移、章节顺序仍成立、页内图表数量有证据支撑、定尺模板没有溢出、中文字体和英文 fallback 可读。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1.2 演示模式工作流","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先确认主题、听众、语言、用途和材料范围;明确交付为 HTML 或 PDF 格式的 PPT,若用户要求 ","type":"text"},{"text":".pptx","type":"text","marks":[{"type":"code_inline"}]},{"text":" 先确认替代格式。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"写逐页提纲:每页一个主要结论,记录来源、数据口径、图型编号、版式编号和待检查项。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"在 ","type":"text"},{"text":"catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 先审计 Lupi Editorial 与 Lupi Basics,再把已锁定的真实图型放进 ","type":"text"},{"text":"presentation-catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的 P04–P08 页面槽位。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"按已锁定的全局色系选模板:Mono 复制 ","type":"text"},{"text":"templates/presentations/presentation.zh.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或 ","type":"text"},{"text":".en.html","type":"text","marks":[{"type":"code_inline"}]},{"text":";彩色预设复制对应的 ","type":"text"},{"text":"complete-porcelain","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"complete-palm","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"complete-wire","type":"text","marks":[{"type":"code_inline"}]},{"text":"(各 19 页、中英双版、带入场动效),替换所有示例内容;不拼接报告模板。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"验收导航、字体回退、动画生命周期、无障碍描述、打印分页和 1280×720 显示。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"演示模式规则详见 ","type":"text"},{"text":"references/presentation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1.3 图数规则","type":"text"}]},{"type":"paragraph","content":[{"text":"图的数量由","type":"text"},{"text":"独立结论数","type":"text","marks":[{"type":"strong"}]},{"text":"决定,不由“有多少数据列”决定,也不固定要求 5 张或 6 张:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"请求类型","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"默认成品数量","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"规则","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"单个问题 / 单张表 / 一个指标","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"只交付最强的一张,不为了展示模板而扩展","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"两到三个明确结论","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"2–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"每张图承担一个不同结论,可共享同一数据源","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"一篇文章、论文或完整案例","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"4–6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"覆盖总览、构成、比较、关系或变化等不同数据形状","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户明确要求数量","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"按用户要求","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"仍然删除重复图;不足以支撑时说明并少做","type":"text"}]}]}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"默认单页上限为 ","type":"text"},{"text":"6 张","type":"text","marks":[{"type":"strong"}]},{"text":";超过 6 张就拆成多页或按章节交付。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"候选方案、Glance/Lupi 对照稿不计入最终图数;它们是选择过程,不是成品批次。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"一页多图至少保留一个总览结论,其余图必须提供新的比较维度、关系、时间变化或细节证据。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"如果两个图表达同一个结论,只保留阅读场合更合适、数据契约更诚实的一张。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"二、Mono 语法 · 硬规则(违反即返工)","type":"text"}]},{"type":"paragraph","content":[{"text":"默认引用 ","type":"text"},{"text":"mono-tokens.js","type":"text","marks":[{"type":"code_inline"}]},{"text":"(开源分发时把内容内联进 HTML)。与 token 冲突的取值一律以 token 为准。走彩色时只替换颜色 token,字体、圆角、布局和动画仍以 ","type":"text"},{"text":"mono-tokens.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" 为准。","type":"text"}]},{"type":"paragraph","content":[{"text":"颜色","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"默认只有纸灰 ","type":"text"},{"text":"#F0EFEB","type":"text","marks":[{"type":"code_inline"}]},{"text":" 和炭黑 ","type":"text"},{"text":"#1C1C1A","type":"text","marks":[{"type":"code_inline"}]},{"text":" 两极,中间 7 级灰阶 ladder。彩色例外按第六点五节从预设起步。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"明度即数据","type":"text","marks":[{"type":"strong"}]},{"text":":最重要 = 最黑(暗卡上反转为最亮)。多系列按重要性沿 ladder 分配,不按顺序随便拿。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"一律实心","type":"text","marks":[{"type":"strong"}]},{"text":":不透明材质、不发光、不渐变滤镜、无阴影。质感全靠明度对比和形状。唯一例外:叠加型图(Radial Patchwork)里透明度本身编码密度——那是数据,不是装饰。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"暗卡(","type":"text"},{"text":".card.dark","type":"text","marks":[{"type":"code_inline"}]},{"text":")只给两种图:必须暗底衬托的形(花瓣、发光感丝线/网络)。默认浅卡;每屏(4 卡)最多 1 张暗卡。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"排版","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inter 全家。标题 700 / 图内数值 800 / 轴标签 600。卡片结构固定四件套:结论式标题(h2)+ 副标题(图例和时间范围写在这里,用 ","type":"text"},{"text":"·","type":"text","marks":[{"type":"code_inline"}]},{"text":" 分隔)+ 图 + 来源行(全大写、加字距)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"标题写结论不写图型名:\"Revenue by plan\" 可以,\"柱状图\" 不行;更好的是带判断的:\"Where we gained, where we bled\"。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SVG 最小字号:半宽卡 6.5px、通栏 5.5px。装不下的信息改 hover 出,不许缩小字号硬塞。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"形状","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"卡片圆角 24px,无边框无阴影,靠留白分卡。柱端胶囊圆角(竖柱圆上端、横柱圆外端)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"柱状图不断轴。","type":"text","marks":[{"type":"strong"}]},{"text":" 柱的契约是长度∝数值,断轴毁契约。极端值场景的正确做法:①让极端值冲天(最诚实)②主图+放大镜小图 ③撕柱不撕轴(明说画不下)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"花瓣皮(粗黑缝+圆角扇瓣)只适用等分或近似等分的径向图。宽窄悬殊的扇区会相互遮蔽,降低可读性。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"动画","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"入场动画默认开,","type":"text"},{"text":"quarticOut","type":"text","marks":[{"type":"code_inline"}]},{"text":" 快进快停,不弹跳(波浪入场可用 elasticOut)。点阵 stagger 8–15ms/个,条形 80–130ms/根。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"统一 reveal 机制:滚入视野才播 + 点击重播(用 token 里的 ","type":"text"},{"text":"obsReveal","type":"text","marks":[{"type":"code_inline"}]},{"text":",带 timer 清理)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"必须带 ","type":"text"},{"text":"prefers-reduced-motion","type":"text","marks":[{"type":"code_inline"}]},{"text":" 降级(token 的 CSS 已含)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"动画不可优于结构","type":"text","marks":[{"type":"strong"}]},{"text":":一个效果如果需要发明新布局才能安放,不配存在(effectScatter 教训)。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"数据","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"演示数据用 token 的 ","type":"text"},{"text":"rnd(i,k)","type":"text","marks":[{"type":"code_inline"}]},{"text":" 确定性伪随机,禁用 ","type":"text"},{"text":"Math.random()","type":"text","marks":[{"type":"code_inline"}]},{"text":"——刷新必须长一样。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"数值和视觉严格成正比。面积编码用 ","type":"text"},{"text":"Math.sqrt(v)","type":"text","marks":[{"type":"code_inline"}]},{"text":" 换算半径,不许拿数值直接当半径。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"三、两个风格系的分工","type":"text"}]},{"type":"paragraph","content":[{"text":"同一份 mono 色板下有两种世界观,选哪个看","type":"text"},{"text":"场合","type":"text","marks":[{"type":"strong"}]},{"text":"和","type":"text"},{"text":"读者愿意花几秒","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Glance 系(20 张)","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lupi 系(细读,19 张)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"基本单元","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"形状(粗柱、大弧、色块)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"记录(一个点 = 一条数据)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"线条","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2px+,理直气壮","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0.5–0.7px 发丝","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"聚合","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"提前聚合好,给结论","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"拒绝聚合,摊开原材料","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"读法","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"扫一眼(\u003c10s)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"凑近读(30s+)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"场合","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"周报、dashboard、随手贴","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"年报、对外故事页、海报","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"引擎","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Chart.js / ECharts","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"手写 SVG","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"默认策略:Lupi Editorial → Lupi Basics → Glance。","type":"text","marks":[{"type":"strong"}]},{"text":" 没有明确场合时,禁止默认 Glance。放年报、公众号长文、海报、开源 README、作品集时先选 Lupi Editorial;数据较少或适合熟悉图型时再选 Lupi Basics。只有前两组都没有合适模板,或用户明确要 dashboard、监控、周报、三秒快读时,才进入 Glance 候选池。","type":"text"}]},{"type":"paragraph","content":[{"text":"全量候选审计不是“把所有图都做一遍”。","type":"text","marks":[{"type":"strong"}]},{"text":" 先用数据形状筛出能编码同一本体的候选,再把候选分成:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"语义合适","type":"text","marks":[{"type":"strong"}]},{"text":":每个点、线、面、串珠背后都有真实单位或明确聚合口径。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"视觉合适","type":"text","marks":[{"type":"strong"}]},{"text":":标签装得下,密度足够,读者能按预期时间读完。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"叙事合适","type":"text","marks":[{"type":"strong"}]},{"text":":图形本身能承载这批数据要讲的判断,而不只是把数字摆出来。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"最终选择是三项的交集,不是“哪个模板文件最容易复制”。默认审计先在 Lupi Editorial 与 Lupi Basics 内完成;只有两组都失败才扩展到 Glance。若同一批要做多张图,再对候选做一次全局分配:模板不重复,形状轮换,最多一张暗卡,避免整页变成六张相似的环或横条。","type":"text"}]},{"type":"paragraph","content":[{"text":"数据少(只有几个百分比)不等于只能 Glance。","type":"text","marks":[{"type":"strong"}]},{"text":" Lupi 化的正道是单位分解:把聚合数摊回可数单位(1 点 = 1 人 / 1 百分点),密度来自单位而不是记录数。单位含义写进副标题,只摊诚实单位(占比加总 100 → 100 个点),不编造不存在的个体记录。取整时如果加总不足 100(如 49.0+27.4+13.9+5.0+3.2 → 98),在底注写明「另外 N 人被四舍五入吃掉了」,不凑假单位。","type":"text"}]},{"type":"paragraph","content":[{"text":"小数据走 Lupi 的路径优先级(经多轮案例验证)","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先全量扫描,再选代码骨架","type":"text","marks":[{"type":"strong"}]},{"text":"——基础型组 F1–F13(","type":"text"},{"text":"templates/basics-gallery.html","type":"text","marks":[{"type":"code_inline"}]},{"text":")是稀疏数据的 Lupi 词汇:柱/折线/面积/环形/横条/分组/堆叠/散点/瀑布/热力/进度/哑铃/矩形树图;编辑型 L1–L15 则提供逐记录、单位分解、关系与旁注语法。这两组是主力,小数据几乎总能在这里落地。F14–F17 与 L16–L20 是后备,按第零节 3.1 / 3.2 判断。不要因为 ","type":"text"},{"text":"basics-gallery.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里有现成代码就直接选 F 图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用最接近本体的模板起步","type":"text","marks":[{"type":"strong"}]},{"text":"——多选题百分比优先比较 L15、F5、L2;100% 构成优先比较 L14、L5、F4、G4;漏斗优先比较 L13、F1/F5 的降级表达。选择依据是数据编码方式,不是文件顺序。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"库里真没有对应形状才新造","type":"text","marks":[{"type":"strong"}]},{"text":",且新造必须从 gallery 现有语法延伸(发丝 tick、确定性 rnd 抖动、paint-order 光晕、全大写注记),不引入库外参照——“Lupi 风”指 gallery 那批图的视觉语法,不是 Giorgia Lupi 本人的手绘风。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"新造的小数据图要配满","type":"text"},{"text":"环境结构层","type":"text","marks":[{"type":"strong"}]},{"text":":gallery 好看有一半靠无数据的家具(账本纸横线、虚线导轨、rim 刻度、每 10 单位的立柱网格、旁注引线)。数据层诚实稀疏,密度预算花在家具上。只画数据 + 一根底线的小数据图必然寒酸。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"同一批产出(一页多图)里模板不重复。","type":"text","marks":[{"type":"strong"}]},{"text":" 19 张 Lupi 系够轮换;同一数据能被多个模板承接时,选这一批里还没用过的。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"四、图型决策树(数据形状 → 候选)","type":"text"}]},{"type":"paragraph","content":[{"text":"编号对应 ","type":"text"},{"text":"catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"。这些只是","type":"text"},{"text":"候选召回表","type":"text","marks":[{"type":"strong"}]},{"text":",箭头和书写顺序不代表优先级。实际选择必须遵守:先检查全部 Lupi Editorial 与 Lupi Basics 候选,确认无合适模板后,才允许采用 Glance 候选。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"少类目比较(≤8)","type":"text","marks":[{"type":"strong"}]},{"text":" → G3 Chunky Bars ⇄ 竖排 F1 Rung Bars / 横排 F5 Tick Rows ⇄ L2 Dot Cascade(⚠️ cascade 类目名竖排,仅当名称 ≤4 字或短缩写时可用;中文长类目换 F5/L5/L12)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多选题百分比(各项独立 0–100,加总可超 100)","type":"text","marks":[{"type":"strong"}]},{"text":" → G3 Chunky Bars ⇄ L15 Ballot Tally","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多类目分布(30–60 根)","type":"text","marks":[{"type":"strong"}]},{"text":" → G12 Stagger Wave","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"带正负的分类数值","type":"text","marks":[{"type":"strong"}]},{"text":" → G10 Diverging Bar","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"占比 / 100% 构成","type":"text","marks":[{"type":"strong"}]},{"text":" → G4 Dot Waffle ⇄ L14 Hundred Field ⇄ F4 Tick Donut(饼图的默认替代);占比×强度双编码 → G13 Big Slice;按类堆叠 → F7 Stacked Rungs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"两时点对比(前后 / 今昔)","type":"text","marks":[{"type":"strong"}]},{"text":" → 类目级(≤6 类)→ F12 Dumbbell Queue(横向,串珠 = 真单位)⇄ F6 Paired Rungs(并肩双梯);只有 2–4 条序列要画趋势线 → Glance chunky slope(粗线、大数、结论进标题)。交叉过多的斜率图会显著降低可读性,不要为了 Lupi 化增加装饰结构。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"日序列(≤30 天,逐日读数)","type":"text","marks":[{"type":"strong"}]},{"text":" → F2 Hairline Line ⇄(90 天级、要肌理)L3 Barcode Lollipop;30–60 天看形态 → F3 Hairline Area","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"累计增长","type":"text","marks":[{"type":"strong"}]},{"text":" → G18 Draw-in + Counter","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"双序列因果(投入 vs 产出)","type":"text","marks":[{"type":"strong"}]},{"text":" → G8 Rainfall","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"实时数据","type":"text","marks":[{"type":"strong"}]},{"text":" → G17 Dynamic Stream","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"排名随时间变","type":"text","marks":[{"type":"strong"}]},{"text":" → 需要动态演示用 G16 Bar Race;静态见下方「排名随离散时间变化」一条","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"星期×小时×量","type":"text","marks":[{"type":"strong"}]},{"text":" → G14 Single Axis ⇄ F10 Dot Heat","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"瀑布 / 增减分解(≤6 级)","type":"text","marks":[{"type":"strong"}]},{"text":" → F9 Rung Waterfall","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"单值进度(0–100%)","type":"text","marks":[{"type":"strong"}]},{"text":" → F11 Tick Gauge ⇄ G18 Draw-in + Counter","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"二维散点(≤20 点)","type":"text","marks":[{"type":"strong"}]},{"text":" → F8 Plumb Scatter;几百点分布 → G15 Jitter Strip","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"单变量分箱频数","type":"text","marks":[{"type":"strong"}]},{"text":" → 先看 F1 Rung Bars 能不能直接承接(bin 当类目);确实需要分箱语义时用 F14 Rung Histogram。bin 边界必须有业务含义;不要为了好看任意切箱。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"分组连续分布","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"五数概括 + 异常值直接用 F15 Tick Box","type":"text","marks":[{"type":"strong"}]},{"text":"(主力无对应编码)。只看密度轮廓时先比较 G15 Jitter Strip;G19 Violin 与 L19 Ridgeline 属后备,要写明为什么 Jitter / F15 不够。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"逐条记录分布","type":"text","marks":[{"type":"strong"}]},{"text":" → G15 Jitter Strip。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"分类×分类+量(矩阵)","type":"text","marks":[{"type":"strong"}]},{"text":" → 主力优先:轻量 L4 Arc Matrix,跨年份带旁注 L9 Bubble Almanac。两者都撑不住(格子多到气泡互相挤、必须靠明度读)才用 L16 Matrix Heat;要快读且每格直接标数才用 G20。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"全年日期×数量","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"整年 52 周 × 7 天直接用 L17 Calendar Heat","type":"text","marks":[{"type":"strong"}]},{"text":"(主力无对应编码,L3 只到 90 天级)。星期×小时的重复周期仍用 F10 / G14。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多对一归属","type":"text","marks":[{"type":"strong"}]},{"text":" → 造型强 L5 Radial Convergence ⇄ 带名单 L12 Type Colonnade","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"漏斗 / 分阶段递减","type":"text","marks":[{"type":"strong"}]},{"text":" → L13 Hourglass Stream","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"层级结构","type":"text","marks":[{"type":"strong"}]},{"text":" → G7 Tree LR","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"层级 + 份额 / 权重(两层、值为正数)","type":"text","marks":[{"type":"strong"}]},{"text":" → F13 Nested Treemap;只看谁属于谁、不比较大小 → G7 Tree LR","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多系列构成随连续时间变化","type":"text","marks":[{"type":"strong"}]},{"text":" → 静态类目构成先用 F7 Stacked Rungs;","type":"text"},{"text":"要同时看总量与构成的连续时间流,直接用 F16 Stream Ribbon","type":"text","marks":[{"type":"strong"}]},{"text":"(主力无对应编码)。只有单序列总量用 F3 / G17。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"同一实体跨 3–6 个连续维度","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"直接用 L20 Parallel Coordinates","type":"text","marks":[{"type":"strong"}]},{"text":"(主力无对应编码)。维度超过 6 个先筛选或拆图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OHLC 行情","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"直接用 F17 Candlestick","type":"text","marks":[{"type":"strong"}]},{"text":"(主力无对应编码)。只有 min–max 区间而无开收盘值时用 G1。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"两端聚合流量","type":"text","marks":[{"type":"strong"}]},{"text":" → 先看 L5 Radial Convergence / L12 Type Colonnade 能否承接归属关系;确实要看流量宽度才用 G22 Aggregate Sankey。要查每条真实路径用 B3 Threads。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"排名随离散时间变化(静态)","type":"text","marks":[{"type":"strong"}]},{"text":" → 先比较 L11 Trend Lineage / L2 Dot Cascade;确实要逐期格子对位才用 G21 Rank Strip。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"事件序列生命史","type":"text","marks":[{"type":"strong"}]},{"text":" → L11 Trend Lineage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多实体出生时间+现状","type":"text","marks":[{"type":"strong"}]},{"text":" → L1 Launch Fan","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"逐事件时刻分布(一天内)","type":"text","marks":[{"type":"strong"}]},{"text":" → L10 Radial Patchwork","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"双极量表(两端都合法)","type":"text","marks":[{"type":"strong"}]},{"text":" → L7 Brand Spectrum。注意和单极打分(雷达)区分:单极用 ECharts 原生雷达","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"网络","type":"text","marks":[{"type":"strong"}]},{"text":" → ≤15 节点 G6/G11 小图;>15 或要查数 → B1 环形 / B2 力导向;多段路径流向 → B3 Threads","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多组×网格(要装饰感)","type":"text","marks":[{"type":"strong"}]},{"text":" → L8 Dotty Matrix;要读数 → 摊平用 G14","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"同一实体集多维度轮播(演示)","type":"text","marks":[{"type":"strong"}]},{"text":" → G9 Scatter Morph","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"地图显式触发规则","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"只有用户明确说“地图”“地域分布”“按国家/州着色”或点名 choropleth,才检查 Maps。数据表里仅出现地区字段不得自动触发。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"美国州级区域数值用 M1 US Choropleth;世界国家级区域数值用 M2 World Choropleth。地图面积是地理面积,不代表数值,副标题必须说明 ","type":"text"},{"text":"shade = value","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"M1/M2 依赖 ECharts 与在线 GeoJSON;交付时说明需要联网,或在用户明确要求离线时再内联合法来源的地图数据。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"中国地图不从 M1/M2 改名套用。只有拿到符合当前地图合规要求的完整数据源和审图信息后才制作。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"F13 Nested Treemap 硬规则","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"只接收层级数据和非负权重;父级值由子级求和,禁止父子总数互相矛盾。面积直接交给 ECharts treemap 布局,不再对值开方。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"默认展示两层(父组 + 叶子)。超过两层且读者需要逐层查询时才开启 drill-down;普通静态交付保持 ","type":"text"},{"text":"nodeClick:false","type":"text","marks":[{"type":"code_inline"}]},{"text":",避免误触改变视图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mono 的灰度只表示层级,不给每个叶子随机分配深浅;父组靠标题带、组间留白和边界区分。去掉颜色后仍能看清分组。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"porcelain 用单色相明度表示有序的父组份额或层级;palm 用色相对应不超过 4 个顶层类目;wire 保持灰阶,只允许一个明确主角使用 ","type":"text"},{"text":"HERO","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"面积已经表达数值时,颜色不得再次无说明地重复同一数值。副标题必须写清 ","type":"text"},{"text":"area = ...","type":"text","marks":[{"type":"code_inline"}]},{"text":",使用彩色时再写清 ","type":"text"},{"text":"color = ...","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"叶子超过 30 个、标签大面积省略,或最小矩形小到无法形成稳定热区时,先合并长尾为 Other、拆成多张,或改用 G7 / L12 / B 系关系图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tooltip 至少显示完整层级路径、原始值和总量占比;卡内标签放不下就隐藏,不缩到低于最小字号。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"五、交互三问(决定静态还是可交互)","type":"text"}]},{"type":"paragraph","content":[{"text":"按顺序问:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"这根线/点背后有没有一条真实记录?","type":"text","marks":[{"type":"strong"}]},{"text":" 没有(纯肌理装饰,如 Cluster Field 的辐条、Hourglass 的淌线)→ 禁止加交互,给没有内容的元素加 hover 是欺骗。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"有记录 → ","type":"text"},{"text":"不点能不能读出来?","type":"text","marks":[{"type":"strong"}]},{"text":" 元素 \u003c50 且两端有标注(如 Colonnade)→ 静态够用,hover 是锦上添花。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"元素 >50 根或多段路径","type":"text","marks":[{"type":"strong"}]},{"text":"(如 Threads)→ 必须 hover/pin,否则只是氛围图。交互实现参考 ","type":"text"},{"text":"templates/big-threads.html","type":"text","marks":[{"type":"code_inline"}]},{"text":":可见线下垫 9px 透明孪生线当热区,hover 单线亮整条路径、hover 标签拉整束、点击钉住。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"六、库外图型 · 翻译流程","type":"text"}]},{"type":"paragraph","content":[{"text":"用户要的图不在 63 张里(或发来一张参考图)时,","type":"text"},{"text":"不是做不了,是现场造句","type":"text","marks":[{"type":"strong"}]},{"text":"。流程四步,第一步不许跳:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先回答本体","type":"text","marks":[{"type":"strong"}]},{"text":":这个图型编码了什么?每个视觉通道(位置/长度/角度/面积/明度/密度)分别对应哪个数据维度?答不出来就问用户拿数据结构。抄形不抄魂 = 白做(pictorial bar 曾把\"符号计数\"错做成\"树长高\",返工)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"找最近的亲戚","type":"text","marks":[{"type":"strong"}]},{"text":":在 catalog 里找数据形状最像的一张当起点,继承它的布局骨架和动画节奏。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用 token 造句","type":"text","marks":[{"type":"strong"}]},{"text":":色板、字体、圆角、动画参数全部从 ","type":"text"},{"text":"mono-tokens.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" 拿,不许发明新颜色新字号。产出必须和库里的图\"一眼一家人\"。走彩色时颜色改从 ","type":"text"},{"text":"color-presets.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的单一预设中取,其他 token 仍以 ","type":"text"},{"text":"mono-tokens.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" 为准。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"过第二节硬规则和第八节自检","type":"text","marks":[{"type":"strong"}]},{"text":",和库内图同一标准。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"参考图翻译的附加规则:识别参考图属于哪个流派(Glance or 编辑部),编辑部风格的密度、旁注、手绘感(","type":"text"},{"text":"blob","type":"text","marks":[{"type":"code_inline"}]},{"text":")是本体的一部分,别做收敛了——almanac 曾因\"不敢挤\"返工。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"六点五、彩色(按场景自动选择,单一色系)","type":"text"}]},{"type":"paragraph","content":[{"text":"Mono 是保底方案,不是未触发彩色时的强制答案。","type":"text","marks":[{"type":"strong"}]},{"text":" 用户没有提颜色时也要根据数据语义、类目数量、阅读场景和内容气质评估是否适合彩色;适配关系不明确,或颜色没有带来真实信息价值时,回到 Mono。","type":"text"}]},{"type":"paragraph","content":[{"text":"彩色初稿从三套预设中选择一套,色值在 ","type":"text"},{"text":"color-presets.js","type":"text","marks":[{"type":"code_inline"}]},{"text":",样张在 ","type":"text"},{"text":"templates/color/","type":"text","marks":[{"type":"code_inline"}]},{"text":"。预设用于快速得到稳定、统一的第一版,不是锁死的色板。用户要求\"这个蓝再深一点\"时,在当前色系内继续调整;用户明确给出品牌色、色值或完整色板时,按下方规则建立 custom 色板。调整后必须重新检查对比度、视觉主次和数据语义。","type":"text"}]},{"type":"paragraph","content":[{"text":"同一份 HTML 或同一组交付必须先锁定一个全局色彩系统:Mono、porcelain、palm、wire 或一套 custom。","type":"text","marks":[{"type":"strong"}]},{"text":" 所有图共享该选择。若其中某张图不支持已选系统,不得单独换色;改选一套全局兼容的系统,或整组退回 Mono。","type":"text"}]},{"type":"paragraph","content":[{"text":"结构正本永远是原版 gallery。","type":"text","marks":[{"type":"strong"}]},{"text":" 查渲染代码去 ","type":"text"},{"text":"templates/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 根目录的 gallery,","type":"text"},{"text":"templates/color/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 只查配色。先按第零节选定结构,再决定是否换肤。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"用不用彩色","type":"text"}]},{"type":"paragraph","content":[{"text":"按以下优先级判断:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用户明确指定。","type":"text","marks":[{"type":"strong"}]},{"text":" 用户要求某种颜色、气质或预设时优先服从,但仍要通过容量、对比度和数据语义检查。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent 自动选择。","type":"text","marks":[{"type":"strong"}]},{"text":" 即使用户没提颜色,只要数据形状和场景与某套预设有清楚的对应关系,就可以自动使用:有序单序列适合 porcelain;少量无序类目适合 palm;需要一个受控视线落点适合 wire。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"关系不明确就用 Mono。","type":"text","marks":[{"type":"strong"}]},{"text":" 类目过多、高密度记录、颜色没有稳定含义,或整组图无法共享同一预设时,使用 Mono。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"彩色不是升级版,而是 Agent 可以主动选择的语义工具。交付时用一句话说明所选色系及其承担的数据含义。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"自定义色板(只在用户明确要求时启用)","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent 不得自行发明第五套默认审美。只有用户给出明确色值、品牌色、品牌规范或\"沿用这张参考图的配色\"时,才建立 custom 色板;仅说\"好看一点\"仍从三套内置预设选择。","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先确定颜色逻辑,再分配色值。","type":"text","marks":[{"type":"strong"}]},{"text":" 有序数据用单色相明度梯;无序类目用分类色;需要聚焦时用中性色 + 一个强调色。禁止先拿到五个颜色,再平均撒到所有元素上。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"建立完整角色,不直接散落 hex。","type":"text","marks":[{"type":"strong"}]},{"text":" custom 至少定义 ","type":"text"},{"text":"BG","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"TXT","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"MUT","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"GRID","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"DATA","type":"text","marks":[{"type":"code_inline"}]},{"text":";需要重点时再定义 ","type":"text"},{"text":"HERO","type":"text","marks":[{"type":"code_inline"}]},{"text":",有序数据定义 ","type":"text"},{"text":"RAMP","type":"text","marks":[{"type":"code_inline"}]},{"text":",分类数据定义 ","type":"text"},{"text":"CAT","type":"text","marks":[{"type":"code_inline"}]},{"text":"。成品内联一个 ","type":"text"},{"text":"CUSTOM","type":"text","marks":[{"type":"code_inline"}]},{"text":" 对象,所有图从角色取色。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"按输入数量解释。","type":"text","marks":[{"type":"strong"}]},{"text":" 1 个颜色生成同色相明度梯;2 个颜色默认分为主数据色 + 强调色,不自动当作两个平等类目;3–6 个颜色只有在数据确有对应类目时才进入 ","type":"text"},{"text":"CAT","type":"text","marks":[{"type":"code_inline"}]},{"text":"。超过 6 个先合并类目、改用位置/标签,或退回 Mono。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"允许派生明暗,不允许偷渡新色相。","type":"text","marks":[{"type":"strong"}]},{"text":" 浅色由用户色与 ","type":"text"},{"text":"BG","type":"text","marks":[{"type":"code_inline"}]},{"text":" 混合,深色由用户色与 ","type":"text"},{"text":"TXT","type":"text","marks":[{"type":"code_inline"}]},{"text":" 混合;不得借用 porcelain、palm、wire 的色值补齐 custom。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"对比度是硬门。","type":"text","marks":[{"type":"strong"}]},{"text":" 正文和小标签相对背景至少 4.5:1,大字至少 3:1;关键边界、数据形状和交互状态相对相邻颜色至少 3:1。不达标时优先调整明度,不擅自改变用户指定的主色相。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"颜色不能成为唯一线索。","type":"text","marks":[{"type":"strong"}]},{"text":" 分类仍带标签,序数仍保留位置/长度/面积,强调仍有标题或旁注说明。色觉异常用户去掉颜色后,图的结构仍应读得懂。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"一次只锁定这一套 custom。","type":"text","marks":[{"type":"strong"}]},{"text":" custom 不与任何内置预设混用;多图交付共享同一个 ","type":"text"},{"text":"CUSTOM","type":"text","marks":[{"type":"code_inline"}]},{"text":"。除非用户明确要求把品牌色固化为项目级预设,否则 custom 只内联在当前成品,不回写 ","type":"text"},{"text":"color-presets.js","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"用户后续要求\"再深一点\"、\"背景暖一点\"或替换某个色值时,修改对应角色并重新检查整套对比度和语义映射,不只改被点名的那一个元素。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"选哪套:先看数据形状,再看气质","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"数据形状","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"允许","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"原因","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"有序 / 单序列(进度、热力、时间序列、单指标、排名)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"porcelain、wire","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"明度即数据的契约仍然成立","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无序类目 ≤4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"palm","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"色相对应类目","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无序类目 5–6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"palm(勉强)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"依靠明度和饱和度辅助区分","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"类目 >6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mono 灰阶","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"彩色预设容量不足","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"需要克制但要一个视线落点","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"wire","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"灰阶承载数据,橙色只标主角","type":"text"}]}]}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户会说","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"预设","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"气质","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"蓝、冷、学术、理性、严肃、科技感","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"porcelain","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"单色相蓝阶","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"绿、暖、自然、温柔、复古、莫兰迪","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"palm","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"绿黄低饱和,琥珀点睛","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"黑白、克制、杂志感、编辑部","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"wire","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"黑灰加一点荧光橙","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"彩色硬规则","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"同一交付只用一种色彩系统。","type":"text","marks":[{"type":"strong"}]},{"text":" 一张图、同一份 HTML 或同一组姊妹图只能使用 Mono、","type":"text"},{"text":"color-presets.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的一套预设,或一套用户明确要求的 custom。禁止预设彼此混用,也禁止 custom 借用内置预设补色。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"线宽 ×1.8、透明度地板 .85。","type":"text","marks":[{"type":"strong"}]},{"text":" 灰阶下的发丝线换成淡色后容易消失;","type":"text"},{"text":"dot heat","type":"text","marks":[{"type":"code_inline"}]},{"text":" 除外。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"每张图至少 3 个色阶或色位。","type":"text","marks":[{"type":"strong"}]},{"text":" 只用 1–2 个颜色时退回灰阶。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"颜色必须连接真实维度。","type":"text","marks":[{"type":"strong"}]},{"text":" 序数梯接数值,分段色接月份或区段,分类色接类目;底注写清颜色在编码什么。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"强调色只给一个主角。","type":"text","marks":[{"type":"strong"}]},{"text":" 第二个主角会消解强调。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"暗卡数据色必须亮于卡底。","type":"text","marks":[{"type":"strong"}]},{"text":" 暗底大图使用 preset 的 ","type":"text"},{"text":"DARK","type":"text","marks":[{"type":"code_inline"}]},{"text":" 块。","type":"text"},{"text":"big-circular","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"big-force","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"big-threads","type":"text","marks":[{"type":"code_inline"}]},{"text":" 只有 porcelain 和 palm;wire 使用原版 Mono 大图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"palm 中 ","type":"text","marks":[{"type":"strong"}]},{"text":"DATA","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 和 ","type":"text","marks":[{"type":"strong"}]},{"text":"HERO","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 必须不同。","type":"text","marks":[{"type":"strong"}]},{"text":" 琥珀和橄榄明度接近,不要把两者当作平等类目色。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"七、什么时候说不","type":"text"}]},{"type":"paragraph","content":[{"text":"敢拒绝比什么都接更可信。以下情况不做,并给替代:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"断轴柱状图","type":"text","marks":[{"type":"strong"}]},{"text":" → 拒绝,给三个诚实方案(冲天 / 放大镜 / 撕柱不撕轴)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"发光 / 玻璃拟态 / 3D","type":"text","marks":[{"type":"strong"}]},{"text":" → 拒绝,保持现有视觉语法。自定义色值按 custom 规则映射;跨色彩系统混用拒绝。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"拿多色相画单序列","type":"text","marks":[{"type":"strong"}]},{"text":" → 拒绝。给 porcelain、wire 或退回 Mono。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"类目 >6 还要彩色","type":"text","marks":[{"type":"strong"}]},{"text":" → 拒绝,退回 Mono 灰阶。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用户未明确要求地图","type":"text","marks":[{"type":"strong"}]},{"text":" → 不因数据里有地区字段自动使用 M1/M2;继续按普通比较、排名或构成选图。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"地图范围超出 M1/M2","type":"text","marks":[{"type":"strong"}]},{"text":"(中国省级、城市点位、轨迹图等)→ 不套用错误边界;确认数据源、投影、合规和交互要求后再现场制作。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"给纯装饰元素加交互","type":"text","marks":[{"type":"strong"}]},{"text":"(第五节第 1 问)→ 拒绝并解释。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"雷达图重构","type":"text","marks":[{"type":"strong"}]},{"text":" → 不重构,用 ECharts 原生雷达 + Mono token 换肤(实测过:展示场景里\"认得出的图型\"有价值)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"数据太少撑不起所选图型(如 3 个节点要力导向)→ 降级到更简单的图并说明。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"八、交付前自检清单","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"数值和视觉成正比?(面积用了 sqrt?柱没断轴?)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"色板是否受控?整份 HTML 或同一组交付是否只使用 Mono、单一内置预设或单一 custom?Mono 中出现任何非 ladder 颜色即返工;内置彩色出现另一套预设颜色即返工;custom 是否只从 ","type":"text"},{"text":"CUSTOM","type":"text","marks":[{"type":"code_inline"}]},{"text":" 角色取色并通过对比度检查?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"标签会不会重叠?(barcode 教训:邻近峰值标签要强制最小间距)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"最小字号踩线没有?(半宽 6.5 / 通栏 5.5)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"演示数据是 ","type":"text"},{"text":"rnd","type":"text","marks":[{"type":"code_inline"}]},{"text":" 确定性的?刷新两次长得一样?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reveal 正常?(滚入播、点击重播、timer 不叠加、reduced-motion 降级在)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"node --check","type":"text","marks":[{"type":"code_inline"}]},{"text":" 过语法(把 ","type":"text"},{"text":"\u003cscript>","type":"text","marks":[{"type":"code_inline"}]},{"text":" 内容抽出来查)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"卡片四件套齐全?标题是结论不是图型名?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"副标题把图例说清了?(读者不看代码只看这一行)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"是否完成全量候选审计?至少写下 3 个候选和淘汰理由,而不是只记录“采用了哪个模板”。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"如果是一页多图,模板是否全局分配过?有没有相似轮廓重复、暗卡过量、所有图都变成同一种径向图?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"每张图是否记录了图型编号、gallery 文件和卡内标题?成品的核心结构是否确实来自该模板,而不是重新发明?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"如果使用 Glance,是否写明 Lupi Editorial 与 Lupi Basics 为什么都不适配?如果没有,返工回到 Lupi / Basics。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"最后一问:这张图放回所选 gallery 的对应卡片旁边,是不是同一个模板家族,而不只是“风格有点像”?","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"九、单文件模板骨架","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"html"},"content":[{"text":"\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n\u003ctitle>Mono — {图名}\u003c/title>\n\u003c!-- 需要 ECharts 时: -->\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js\">\u003c/script>\n\u003c!-- 需要 Chart.js 时(G1 / G3): -->\n\u003c!-- \u003cscript src=\"https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js\">\u003c/script> -->\n\u003clink rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\u003clink href=\"{MONO.FONT.link}\" rel=\"stylesheet\">\n\u003cstyle>/* 内联 MONO.CARD_CSS */\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"grid2\">\n \u003cdiv class=\"card\">\u003c!-- 或 card dark / card wide -->\n \u003ch2>{结论式标题}\u003c/h2>\n \u003cdiv class=\"sub\">{说明} · {图例} · {时间范围}\u003c/div>\n \u003c!-- 图容器按引擎三选一: -->\n \u003cdiv class=\"ch\" id=\"ch\">\u003c/div>\u003c!-- ECharts -->\n \u003c!-- 手写 SVG: \u003csvg id=\"ch\" viewBox=\"0 0 400 320\">\u003c/svg> -->\n \u003c!-- Chart.js(G1/G3): \u003cdiv class=\"wrap\">\u003ccanvas id=\"ch\">\u003c/canvas>\u003c/div>,配 .wrap{position:relative;height:320px} -->\n \u003c!-- ⚠️ Chart.js 必须挂 \u003ccanvas>,套 \u003cdiv class=\"ch\"> 会报 can't acquire context -->\n \u003cdiv class=\"src\">{图型名} · {系列} · {数据来源}\u003c/div>\n \u003c/div>\n\u003c/div>\n\u003cscript>\n// 内联 mono-tokens.js 全文\n// ── 数据(用户只需要改这里)──\nconst DATA = [ /* ... */ ];\n// ── 渲染 ──\nMONO.obsReveal('ch', el => { /* ... */ });\n\u003c/script>\n\u003c/body>\n\u003c/html>","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"十、报告与演示模式骨架","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"report-catalog.md # 先按场景选 R01–R12\npresentation-catalog.md # 演示模式 P01–P10\nreferences/presentation.md # 演示模式完整规则\ntemplates/presentations/index.html # 演示模板索引\ntemplates/presentations/presentation.zh.html # 中文 16:9 演示正本(Mono)\ntemplates/presentations/presentation.en.html # English 16:9 deck source (Mono)\ntemplates/presentations/complete-{porcelain,palm,wire}{,-zh}.html # 彩色完整版,19 页中英双版\ntemplates/reports/report-NN.zh.html # 中文整页正本\ntemplates/reports/report-NN.en.html # English full-page source\ntemplates/reports/index.html # 本地可打开的模板索引\ndocs/assets/reports/report-NN.png # 中文版静态预览","type":"text"}]},{"type":"paragraph","content":[{"text":"报告交付仍然是单文件 HTML;","type":"text"},{"text":"templates/reports/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 中的文件是可复制的起始骨架,不是需要逐字照抄的内容样例。模板内的演示数据只用于展示结构,生成最终报告时必须全部替换。","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"lieflat-charts","author":"@skillopedia","source":{"stars":6625,"repo_name":"lieflat-charts","origin_url":"https://github.com/larashero3-dotcom/lieflat-charts/blob/HEAD/SKILL.md","repo_owner":"larashero3-dotcom","body_sha256":"8bbb3eced0a0f72a5470af5eef5bba160412ca46f5a35eed938efc8c63084b0e","cluster_key":"3218c004a90fc775d45b53684352577533d2b09a0d67c5b9387f180f275a202d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"larashero3-dotcom/lieflat-charts/SKILL.md","text_bytes":1046371,"source_commit":"e5a59ee498fe2f938e6d82bdc39f4b1db682e3a3","skills_sh_rank":8923,"attachment_count":35,"text_attachments":35,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"data-analytics","category_label":"Data"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"data-analytics","import_tag":"skills-sh-top20k-2026-11","description":"一套模板驱动的数据可视化、PPT 演示与整页报告生成 skill,既能严格从 Lupi、Basics、Glance、Maps 与 Interactive gallery 的真实实现生成 HTML 图表,也能从 Presentation Mode 生成 HTML/PDF 演示,或从报告模板生成整页 HTML 报告;以 Mono 为保底,能按数据语义自动选择内置彩色预设,也支持用户明确提供的自定义色板。地图仅在用户明确要求时启用,同一交付禁止混用色系。"}},"renderedAt":1793666652701}

Lieflat Charts — 图表品味法典 Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化、PPT 演示与整页报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 同时提供图表模式、Presentation Mode 和 Report Mode,但默认产出图表。用户明确要求一套 PPT、幻灯片或可翻页汇报时,进入 Presentation Mode,生成 HTML 或 PDF 格式的 PPT;用户明确要求报告、年报、月报、白皮书、海报或 brief 时,进入 Report Mode,生成整页 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。 默认输出是图表:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式。只有用户明确要求“一套 PPT / 幻灯片 / 演示文稿 / 可翻页汇报”时,才进入演示模式并读取 与 ,生成 HTML 或 PDF 格式的 PPT;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式并读取…