工作台视频列表页新增点赞/评论趋势折线图(lieflat-charts G8 骨架)
- 引入 lieflat-charts 第三方图表技能到 subskills/(SKILL.md+catalog+templates,已删嵌套 .git) - ECharts 6.1.0 本地化至 mcn-work-shop/public/vendor/(零 CDN 依赖,离线可用) - 账号详情视频列表 tab 上方新增趋势图:X=发布时间升序,点赞/评论双序列面积折线 (G8 Rainfall 骨架:双 grid 独立量级、同 X 联动 pointer、tipLight tooltip、quarticOut 入场) - 全量视频数据单独拉取(pageSize=500,不随分页变化);numOf 解析 'x.x万' 文本 - V1.0/SKILL.md 登记 lieflat-charts 外部技能依赖
This commit is contained in:
1 parent
d43578e820
commit
cad356047c
129 files changed
+26762
-1
No files matched your search
@@ -0,0 +1,35 @@
|
||||
# Examples · 真实数据成品案例
|
||||
|
||||
templates/ 里的 gallery 是「例句」(演示数据、多卡合页);这里是「成文」——拿一篇真实文章的公开数据,从判形状到出图走完整个 skill 流程的成品。拿不准交付物该长什么样,先看这里。
|
||||
|
||||
## r04-financial-report.zh.html
|
||||
|
||||
这是 R04「月度运营」模板的一份财务场景示例:保留原有版心和四个图表槽位,将内容改为月度经营财报,包括收入、毛利、经营现金流、毛利率、现金可支撑月数、每日收入、回款时段、收入构成和业务线毛利贡献。数据为虚构演示数据,用来说明同一模板如何适配金融 / 经济类报告。
|
||||
|
||||
预览图:[R04 月度经营财报](../docs/assets/examples/r04-financial-report.zh.png)
|
||||
|
||||
## lenny-2026-survey.html
|
||||
|
||||
数据来源:Lenny's Newsletter《How tech workers are feeling in 2026》(2026-07-07,年度科技从业者调研)。一篇文章 → 8 张图,全英文,8 个模板不重复:
|
||||
|
||||
这是图数规则确定前保留的早期展示案例,用 8 张图覆盖更多模板。当前 skill 处理同类文章时,默认会筛选为 4–6 张,并在超过 6 张时拆页。
|
||||
|
||||
| # | 模板 | 数据 |
|
||||
|---|------|------|
|
||||
| 1 | Type Colonnade (L12) | AI 身份认同 49/27/14/5/3 |
|
||||
| 2 | Ballot Tally (L15) | 矛盾三联:享受工作 79 · 显著倦怠 56 · 乐观 49 |
|
||||
| 3 | Hundred Faces(语义单位·通栏) | 裁员担忧 28/31/21/12/8,嘴角弧度=担忧程度 |
|
||||
| 4 | Tick Rows (F5) | 四大恐惧 51/46/41/22 |
|
||||
| 5 | Hourglass Stream (L13) | 矛盾收窄 100 → 77 → 51 |
|
||||
| 6 | Brand Spectrum (L7) | 分层 NPS −49/−23/−5 |
|
||||
| 7 | Radial Convergence (L5) | 行业一句话:混乱 30/快 17/泡沫 12/兴奋 11/其他 30 |
|
||||
| 8 | Ballot Rings(表盘 tick·通栏) | AI 生产力 82 → 49 |
|
||||
|
||||
这个文件顺便示范了几条容易忘的规矩:
|
||||
|
||||
- **取整要认账**:身份认同取整后只有 98,底注写「rounding ate the other two」,不凑假线。
|
||||
- **没有的数据不编**:Brand Spectrum 模板原有竞品对照点,Lenny 没这个数据,删空。
|
||||
- **精确份额关掉随机泄漏**:Radial Convergence 模板的 rnd 泄漏逻辑(约 8% 汇错 hub 制造有机感)在调研数据上要关。
|
||||
- **同一批多图模板不重复**:8 张图 8 个模板。
|
||||
- **cascade 竖排类目名只配短标签**——本例曾用 Dot Cascade 画恐惧数据,两轮都被评「看不清」,换成横排 Tick Rows 后解决:标签保持水平永远是更稳的选择。
|
||||
- **reduced-motion 降级不能漏**(无头截图验证时可用 `--force-prefers-reduced-motion` 绕过动画时序)。
|
||||
@@ -0,0 +1,445 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>How Tech Workers Are Feeling in 2026 · 8 charts, Mono</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
|
||||
.pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
|
||||
.pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
|
||||
.pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
|
||||
.card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
|
||||
.card.dark{background:var(--dark);color:#F0EFEB}
|
||||
.card.dark .sub{color:#8F8E88}
|
||||
.card.dark .src{color:#55554F}
|
||||
.card.wide{grid-column:1/-1}
|
||||
h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
|
||||
.sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
|
||||
.src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
|
||||
svg{width:100%;max-height:330px;display:block;margin:0 auto}
|
||||
svg text{font-family:'Inter',sans-serif}
|
||||
.pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
|
||||
@keyframes pop{from{transform:scale(0)}to{transform:none}}
|
||||
.fade{animation:fade .9s ease both}
|
||||
@keyframes fade{from{opacity:0}}
|
||||
.draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
|
||||
@keyframes draw{to{stroke-dashoffset:0}}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.pop,.fade{animation:none}
|
||||
.draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pagehead">
|
||||
<h1>How tech workers are feeling in 2026 · eight charts</h1>
|
||||
<p>Data: Lenny's Newsletter annual tech worker sentiment survey (Jul 2026). Every chart is a gallery template with only its DATA swapped — one unit is always one real person or one real answer. Scroll to draw; click any chart to replay.</p>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
|
||||
<!-- 1 · type colonnade ← AI identity -->
|
||||
<div class="card">
|
||||
<h2>What AI did to a hundred identities</h2>
|
||||
<div class="sub">one hairline = one person in a hundred · each thread joins the identity they chose · rounding ate 2 people</div>
|
||||
<svg id="colonnade" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="src">TYPE COLONNADE · AI IDENTITY, 5 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
<!-- 2 · ballot tally ← the contradiction -->
|
||||
<div class="card">
|
||||
<h2>Love the work, worn out, losing faith</h2>
|
||||
<div class="sub">one tick = one person in a hundred · same respondents, three different questions</div>
|
||||
<svg id="tally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="src">BALLOT TALLY · ENJOYMENT / BURNOUT / OPTIMISM, 3 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
<!-- 3 · hundred faces ← layoff worry, wide -->
|
||||
<div class="card wide">
|
||||
<h2>How worried is the room</h2>
|
||||
<div class="sub">one face = one person in a hundred · the mouth is the data · dark ink piles up where worry does</div>
|
||||
<svg id="faces" viewBox="0 0 720 340" preserveAspectRatio="xMidYMid meet" style="max-height:380px"></svg>
|
||||
<div class="src">HUNDRED FACES · LAYOFF WORRY, 5 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
<!-- 4 · tick rows ← fears -->
|
||||
<div class="card">
|
||||
<h2>What they fear, tick by tick</h2>
|
||||
<div class="sub">one tick = one person in a hundred who picked it · they could pick several · sorted by dread</div>
|
||||
<svg id="fears" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="src">TICK ROWS · WORKPLACE FEARS, 4 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
<!-- 5 · hourglass stream ← ambivalence narrowing -->
|
||||
<div class="card">
|
||||
<h2>Ambivalence, layer by layer</h2>
|
||||
<div class="sub">one tick = one person in a hundred · everyone, then both-feelings, then the openly conflicted</div>
|
||||
<svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="src">HOURGLASS STREAM · FEELINGS ABOUT AI, 3 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
<!-- 6 · brand spectrum ← NPS by seniority -->
|
||||
<div class="card">
|
||||
<h2>Who would still recommend it</h2>
|
||||
<div class="sub">each rank sits between NEVER and GLADLY · the pale ribbon slides left as you climb down</div>
|
||||
<svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="src">BRAND SPECTRUM · NPS BY SENIORITY, 3 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
<!-- 7 · radial convergence ← industry in one sentence -->
|
||||
<div class="card">
|
||||
<h2>A hundred answers, five ways to say it</h2>
|
||||
<div class="sub">every rim node = one answer · threads bundle into the theme it landed in · chaos pulls hardest</div>
|
||||
<svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="src">RADIAL CONVERGENCE · "THE INDUSTRY IN A SENTENCE", 5 THEMES</div>
|
||||
</div>
|
||||
|
||||
<!-- 8 · ballot rings ← AI helping at work, wide finale -->
|
||||
<div class="card wide">
|
||||
<h2>Is AI actually making them better?</h2>
|
||||
<div class="sub">one tick = one person in a hundred · inked = better at their job because of AI · the right dial lives inside the left</div>
|
||||
<svg id="rings" viewBox="0 0 720 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
|
||||
<div class="src">BALLOT RINGS · AI PRODUCTIVITY, 2 NUMBERS</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
// ── shared Mono tokens ──
|
||||
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
|
||||
const NS='http://www.w3.org/2000/svg';
|
||||
const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
|
||||
const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
|
||||
const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
|
||||
const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
|
||||
const D2R=Math.PI/180;
|
||||
const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
|
||||
const obsReveal=(id,fn)=>{
|
||||
const n=document.getElementById(id);
|
||||
const go=()=>{n.innerHTML='';fn(n)};
|
||||
const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
|
||||
io.observe(n);
|
||||
n.style.cursor='pointer';
|
||||
n.addEventListener('click',go);
|
||||
};
|
||||
|
||||
// ════ 1 · type colonnade ════ AI identity: 49/27.4/13.9/5/3.2 → 49+27+14+5+3 = 98,
|
||||
// rounding ate 2 people — the footer says so instead of inventing threads.
|
||||
(()=>{
|
||||
const TEAM=[['AMPLIFIED','I can do more, and better',49],['REDEFINED','changing shape, not clearly good or bad',27],['DESTABILIZED','less sure what is still mine',14],['DIMINISHED','I feel less essential',5],['UNCHANGED','',3]];
|
||||
obsReveal('colonnade',s=>{
|
||||
const N=98;
|
||||
const teamY=j=>60+j*56,repoY=i=>14+i*2.9;
|
||||
const blocks=TEAM.flatMap(([name,,n],h)=>Array(n).fill(h));
|
||||
for(let i=0;i<N;i++){
|
||||
const yi=repoY(i),j=blocks[i],yj=teamY(j);
|
||||
el(s,'rect',{x:64,y:yi-1,width:4,height:2,fill:'#B0AFA9',
|
||||
class:'fade',style:`animation-delay:${i*.006}s`});
|
||||
if(i%10===0)txt(s,{x:58,y:yi+2.5,'font-size':5.5,fill:'#C6C5BF','text-anchor':'end',
|
||||
class:'fade',style:`animation-delay:${i*.006}s`},i+1);
|
||||
el(s,'path',{d:`M70 ${yi} C 160 ${yi} 190 ${yj} 262 ${yj}`,fill:'none',
|
||||
stroke:'#A8A7A0','stroke-width':.6,opacity:.6,pathLength:1,
|
||||
class:'draw',style:`animation-delay:${.2+i*.008}s;animation-duration:.7s`});
|
||||
}
|
||||
TEAM.forEach(([name,desc,n],j)=>{
|
||||
const y=teamY(j);
|
||||
const hub=el(s,'circle',{cx:267,cy:y,r:Math.sqrt(n)*1.7,fill:n>=27?INK:n>=14?'#6A6963':'#B0AFA9',
|
||||
class:'pop',style:`animation-delay:${.6+j*.05}s`});
|
||||
tip(hub,`${name} — ${n} in 100`);
|
||||
txt(s,{x:280+Math.sqrt(n)*1.2,y:y+1,'font-size':8,'font-weight':800,fill:'#1C1C1A',
|
||||
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${.65+j*.05}s`},`${name} · ${n}`);
|
||||
if(desc)txt(s,{x:280+Math.sqrt(n)*1.2,y:y+11,'font-size':5.8,'font-weight':500,fill:'#8F8E88',
|
||||
class:'fade',style:`animation-delay:${.7+j*.05}s`},desc);
|
||||
});
|
||||
txt(s,{x:200,y:322,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.2s'},
|
||||
'ONE THREAD = ONE PERSON · 49+27+14+5+3 = 98 · ROUNDING ATE THE OTHER TWO');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 2 · ballot tally ════ same respondents, three questions:
|
||||
// still enjoy the work 79 · significantly burned out 56 · optimistic 49.
|
||||
(()=>{
|
||||
const ASK=[['STILL ENJOY THE WORK (MODERATELY+)',79],['SIGNIFICANTLY BURNED OUT',56],['OPTIMISTIC ABOUT THEIR CAREER',49]];
|
||||
obsReveal('tally',s=>{
|
||||
ASK.forEach(([name,v],i)=>{
|
||||
const base=88+i*78;
|
||||
txt(s,{x:28,y:base-30,'font-size':7.5,'font-weight':700,fill:'#6A6963',
|
||||
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
|
||||
el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
|
||||
class:'fade',style:`animation-delay:${i*.1}s`});
|
||||
for(let k=0;k<100;k++){
|
||||
const x=28+k*3.44,picked=k<v;
|
||||
const h=picked?14+rnd(k+1,i+2)*6:5+rnd(k+1,i+5)*2.5;
|
||||
el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
|
||||
stroke:picked?INK:'#CFCEC7','stroke-width':picked?.9:.55,
|
||||
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
|
||||
if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',
|
||||
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
|
||||
}
|
||||
const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-13,'font-size':11.5,'font-weight':800,fill:INK,
|
||||
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+i*.1}s`,
|
||||
class:'fade'},v);
|
||||
tip(lab,`${v} in 100`);
|
||||
});
|
||||
txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
|
||||
'ONE TICK = ONE PERSON · SAME RESPONDENTS · ALL THREE CAN BE TRUE AT ONCE');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 3 · hundred faces ════ layoff worry 28/31/21/12/8 = 100. One face = one person;
|
||||
// mouth curve deepens with worry, grey ladder fills in step — ink piles up where worry does.
|
||||
(()=>{
|
||||
const SEG=[
|
||||
['NOT AT ALL',28,PAPER,INK,-2.6], // fill, feature color, mouth curve
|
||||
['SLIGHTLY',31,'#DEDDD6',INK,-.6],
|
||||
['MODERATELY',21,'#B0AFA9',INK,1.4],
|
||||
['VERY',12,'#55554F',PAPER,2.6],
|
||||
['EXTREMELY',8,INK,PAPER,3.6],
|
||||
];
|
||||
obsReveal('faces',s=>{
|
||||
const COLS=20,cell=26,R=8.6,X0=98,Y0=52;
|
||||
let idx=0;
|
||||
SEG.forEach(([name,n,fill,fg,curve],si)=>{
|
||||
for(let k=0;k<n;k++,idx++){
|
||||
const col=idx%COLS,row=Math.floor(idx/COLS);
|
||||
const x=X0+col*cell,y=Y0+row*cell;
|
||||
const g=el(s,'g',{class:'pop',style:`animation-delay:${idx*.008}s`});
|
||||
el(g,'circle',{cx:x,cy:y,r:R,fill,stroke:INK,'stroke-width':1});
|
||||
el(g,'circle',{cx:x-3,cy:y-2.4,r:1,fill:fg});
|
||||
el(g,'circle',{cx:x+3,cy:y-2.4,r:1,fill:fg});
|
||||
if(si===4){
|
||||
el(g,'line',{x1:x-4.4,y1:y-5.6,x2:x-1.8,y2:y-4.4,stroke:fg,'stroke-width':.9});
|
||||
el(g,'line',{x1:x+4.4,y1:y-5.6,x2:x+1.8,y2:y-4.4,stroke:fg,'stroke-width':.9});
|
||||
}
|
||||
const my=y+3.2;
|
||||
el(g,'path',{d:`M ${x-3.4} ${my} Q ${x} ${my+curve} ${x+3.4} ${my}`,
|
||||
fill:'none',stroke:fg,'stroke-width':1});
|
||||
tip(g,`${name} — one of ${n} in 100`);
|
||||
}
|
||||
});
|
||||
// top legend: shares with names
|
||||
let lx=98;
|
||||
SEG.forEach(([name,n],si)=>{
|
||||
txt(s,{x:lx,y:24,'font-size':8,'font-weight':800,fill:INK,
|
||||
class:'fade',style:`animation-delay:${.2+si*.06}s`},n+'%');
|
||||
txt(s,{x:lx+26,y:24,'font-size':6.6,'font-weight':600,fill:MUTED,
|
||||
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.2+si*.06}s`},name);
|
||||
lx+=Math.max(88,name.length*5.4+40);
|
||||
});
|
||||
// bracket: everyone past the first 28 carries some worry
|
||||
const bx=98+20*26-6;
|
||||
el(s,'line',{x1:bx+14,y1:Y0+26-13,x2:bx+14,y2:Y0+4*26+13,stroke:'#8F8E88','stroke-width':1,
|
||||
class:'fade',style:'animation-delay:1s'});
|
||||
[[Y0+26-13],[Y0+4*26+13]].forEach(([yy])=>
|
||||
el(s,'line',{x1:bx+9,y1:yy,x2:bx+14,y2:yy,stroke:'#8F8E88','stroke-width':1,
|
||||
class:'fade',style:'animation-delay:1s'}));
|
||||
txt(s,{x:bx+22,y:Y0+2.5*26-6,'font-size':10.5,'font-weight':800,fill:INK,
|
||||
class:'fade',style:'animation-delay:1.1s'},'72%');
|
||||
txt(s,{x:bx+22,y:Y0+2.5*26+6,'font-size':6.6,'font-weight':600,fill:MUTED,
|
||||
'letter-spacing':'.06em',class:'fade',style:'animation-delay:1.1s'},'CARRY SOME');
|
||||
txt(s,{x:bx+22,y:Y0+2.5*26+15,'font-size':6.6,'font-weight':600,fill:MUTED,
|
||||
'letter-spacing':'.06em',class:'fade',style:'animation-delay:1.1s'},'WORRY');
|
||||
txt(s,{x:360,y:330,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
|
||||
'ONE FACE = ONE PERSON IN A HUNDRED · THE MOUTH IS THE DATA · 28+31+21+12+8 = 100');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 4 · tick rows ════ fears (multi-select): one tick = one person who picked it.
|
||||
// Horizontal queues, labels stay level — readable at any size.
|
||||
(()=>{
|
||||
const D=[['MORE FOR SAME PAY',51],['UNSUSTAINABLE PACE',46],['QUALITY SLIPPING',41],['REPLACED BY AI',22]];
|
||||
obsReveal('fears',s=>{
|
||||
const X0=160,PX=3.9,y0=i=>66+i*62;
|
||||
D.forEach(([name,v],i)=>{
|
||||
const y=y0(i);
|
||||
txt(s,{x:150,y:y+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
|
||||
'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.08}s`},name);
|
||||
el(s,'line',{x1:X0,y1:y+9,x2:X0+55*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
|
||||
class:'fade',style:`animation-delay:${i*.08}s`});
|
||||
for(let k=0;k<v;k++){
|
||||
const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
|
||||
el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:INK,'stroke-width':.9,
|
||||
opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
|
||||
if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:'#C6C5BF',
|
||||
class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
|
||||
}
|
||||
const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:INK,
|
||||
class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
|
||||
tip(lab,`${name} — ${v} of 100 · they could pick several`);
|
||||
});
|
||||
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
|
||||
'ONE TICK = ONE PERSON IN A HUNDRED · MULTI-SELECT · DOT MARKS EVERY FIFTH');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 5 · hourglass stream ════ ambivalence narrowing: 100 → 77 → 51, one tick = one person.
|
||||
(()=>{
|
||||
const STAGE=[['ALL RESPONDENTS',100],['PICKED POSITIVE AND NEGATIVE',77],['SAID "CONFLICTED" OUTRIGHT',51]];
|
||||
obsReveal('hourglass',s=>{
|
||||
const CXm=175,sy=k=>50+k*104,w=c=>c/100*300;
|
||||
STAGE.forEach(([name,c],k)=>{
|
||||
const y=sy(k),hw=w(c)/2;
|
||||
for(let t=0;t<c;t++){
|
||||
const x=CXm-hw+(t+.5)/c*hw*2+(rnd(t+1,k+3)-.5)*2.4;
|
||||
el(s,'line',{x1:x,y1:y-7,x2:x,y2:y+7,stroke:INK,'stroke-width':.8,
|
||||
opacity:.45+rnd(t+2,k+5)*.5,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
|
||||
}
|
||||
if(k<2){
|
||||
const hw1=w(STAGE[k+1][1])/2;
|
||||
for(let t=0;t<34;t++){
|
||||
const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
|
||||
const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
|
||||
el(s,'path',{d:`M${xt} ${y+9} C${xt} ${y+44} ${xb} ${sy(k+1)-44} ${xb} ${sy(k+1)-9}`,
|
||||
fill:'none',stroke:'#B0AFA9','stroke-width':.5,opacity:.32,pathLength:1,
|
||||
class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
|
||||
}
|
||||
const pct=Math.round(STAGE[k+1][1]/c*100);
|
||||
txt(s,{x:16,y:(y+sy(k+1))/2+3,'font-size':9,'font-weight':800,fill:'#8F8E88',
|
||||
class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
|
||||
txt(s,{x:16,y:(y+sy(k+1))/2+14,'font-size':6,'font-weight':600,fill:'#C6C5BF',
|
||||
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'STAY IN');
|
||||
}
|
||||
el(s,'line',{x1:CXm+hw+6,y1:y,x2:332,y2:y,stroke:'#DEDDD6','stroke-width':.8,
|
||||
class:'fade',style:`animation-delay:${.3+k*.12}s`});
|
||||
txt(s,{x:336,y:y-2,'font-size':6.8,'font-weight':700,fill:'#4A4944',
|
||||
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
|
||||
txt(s,{x:336,y:y+11,'font-size':11,'font-weight':800,fill:INK,
|
||||
class:'fade',style:`animation-delay:${.4+k*.12}s`},c);
|
||||
});
|
||||
txt(s,{x:200,y:322,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
|
||||
'ONE TICK = ONE PERSON · THE AVERAGE PERSON PICKED 5+ EMOTIONS · ONE PICKED 13');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 6 · brand spectrum ════ NPS by seniority: −49 / −23 / −5.
|
||||
// The template's competitor dots are dropped — Lenny has no such data, we don't invent it.
|
||||
(()=>{
|
||||
const ROWS=[['NEVER AGAIN','GLADLY'],['NEVER AGAIN','GLADLY'],['NEVER AGAIN','GLADLY']];
|
||||
const RANK=['SENIOR / STAFF IC','VP','FOUNDER'];
|
||||
const NPS=[-49,-23,-5];
|
||||
const US=NPS.map(v=>(v+60)/80); // scale −60 … +20
|
||||
const X0=118,X1=342,Y=i=>66+i*72,px=t=>X0+t*(X1-X0);
|
||||
obsReveal('spectrum',s=>{
|
||||
let d=`M${px(US[0])} ${Y(0)}`;
|
||||
for(let i=1;i<3;i++)
|
||||
d+=` C${px(US[i-1])} ${Y(i-1)+36} ${px(US[i])} ${Y(i)-36} ${px(US[i])} ${Y(i)}`;
|
||||
el(s,'path',{d,fill:'none',stroke:'#F0EFEB','stroke-width':30,'stroke-linecap':'round',
|
||||
'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
|
||||
style:'animation-duration:1.4s'});
|
||||
ROWS.forEach(([a,b],i)=>{
|
||||
const y=Y(i);
|
||||
el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:'#B0AFA9','stroke-width':1,
|
||||
class:'fade',style:`animation-delay:${i*.08}s`});
|
||||
[X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:'#B0AFA9','stroke-width':1,
|
||||
class:'fade',style:`animation-delay:${i*.08}s`}));
|
||||
txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',
|
||||
'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
|
||||
txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963',
|
||||
'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
|
||||
const xz=px((0+60)/80);
|
||||
el(s,'line',{x1:xz,y1:y-5,x2:xz,y2:y+5,stroke:'#C6C5BF','stroke-width':1,
|
||||
'stroke-dasharray':'2 2',class:'fade',style:`animation-delay:${.2+i*.08}s`});
|
||||
const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:INK,
|
||||
class:'pop',style:`animation-delay:${.5+i*.1}s`});
|
||||
tip(us,`${RANK[i]} — NPS ${NPS[i]}`);
|
||||
txt(s,{x:px(US[i]),y:y-14,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
|
||||
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.6+i*.1}s`,
|
||||
class:'fade'},NPS[i]);
|
||||
txt(s,{x:px(US[i]),y:y+22,'font-size':7,'font-weight':700,fill:MUTED,'text-anchor':'middle',
|
||||
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.65+i*.1}s`},RANK[i]);
|
||||
});
|
||||
txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.12em',
|
||||
class:'fade',style:'animation-delay:.9s'},'● NPS BY RANK ┊ ZERO — WHERE YES BALANCES NO');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 7 · radial convergence ════ "the industry in a sentence": chaos 30 / too fast 17 /
|
||||
// bubble 12 / excitement 11 / everything else 30. Leakage off — shares are exact.
|
||||
(()=>{
|
||||
const N=100,CX=200,CY=162,R=116;
|
||||
const THEME=[['CHAOS · UNCERTAIN',30,-75],['TOO FAST TO KEEP UP',17,-3],['AI BUBBLE TALK',12,69],['EXCITEMENT',11,141],['EVERYTHING ELSE',30,213]];
|
||||
obsReveal('converge',s=>{
|
||||
const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
|
||||
const hubOf=i=>blocks[i];
|
||||
const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
|
||||
const counts=[0,0,0,0,0];
|
||||
for(let i=0;i<N;i++)counts[hubOf(i)]++;
|
||||
for(let i=0;i<N;i++){
|
||||
const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
|
||||
const h=hubOf(i),[hx,hy]=hubPt(h);
|
||||
const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
|
||||
const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
|
||||
el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
|
||||
fill:'none',stroke:'#A8A7A0','stroke-width':.7,opacity:.55,pathLength:1,
|
||||
class:'draw',style:`animation-delay:${i*.01}s`});
|
||||
el(s,'circle',{cx:px,cy:py,r:1.6,fill:'#6A6963',
|
||||
class:'pop',style:`animation-delay:${i*.01}s`});
|
||||
}
|
||||
THEME.forEach(([name,,deg],h)=>{
|
||||
const [hx,hy]=hubPt(h);
|
||||
const grey=name==='EVERYTHING ELSE';
|
||||
const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,
|
||||
fill:grey?'#B0AFA9':INK,
|
||||
class:'pop',style:`animation-delay:${.5+h*.08}s`});
|
||||
tip(hub,`${name} — ${counts[h]} of 100 answers`);
|
||||
const [tx,ty]=pol(CX,CY,R+26,deg);
|
||||
txt(s,{x:tx,y:ty,'font-size':7.5,'font-weight':800,fill:grey?'#8F8E88':INK,'text-anchor':'middle',
|
||||
'dominant-baseline':'middle','letter-spacing':'.06em',
|
||||
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.6+h*.08}s`,
|
||||
class:'fade'},`${name} · ${counts[h]}`);
|
||||
const [gx,gy]=pol(CX,CY,R+14,deg);
|
||||
el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:'#C6C5BF','stroke-width':.7,
|
||||
'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
|
||||
});
|
||||
txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.2s'},
|
||||
'ONE RIM NODE = ONE ANSWER · THE CHAOS QUOTES SPLIT 37 POSITIVE / 37 NEGATIVE / 26 NEUTRAL');
|
||||
});
|
||||
})();
|
||||
|
||||
// ════ 8 · ballot rings ════ AI productivity: 82 say at least moderately better,
|
||||
// 49 of those say very or extremely. Two dials, one story.
|
||||
(()=>{
|
||||
const D=[['AT LEAST MODERATELY BETTER',82],['VERY OR EXTREMELY BETTER',49]];
|
||||
obsReveal('rings',s=>{
|
||||
D.forEach(([name,v],i)=>{
|
||||
const cx=214+i*292,cy=134,R0=64;
|
||||
for(let k=0;k<100;k++){
|
||||
const a=k*3.6-90,picked=k<v;
|
||||
const len=picked?12+rnd(k+1,i+2)*6:4.5+rnd(k+1,i+5)*2;
|
||||
const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
|
||||
el(s,'line',{x1,y1,x2,y2,stroke:picked?INK:'#CFCEC7','stroke-width':picked?.95:.55,
|
||||
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
|
||||
if(k%10===0){
|
||||
const [dx,dy]=pol(cx,cy,R0-5,a);
|
||||
el(s,'circle',{cx:dx,cy:dy,r:.8,fill:'#C6C5BF',
|
||||
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
|
||||
}
|
||||
}
|
||||
const aEnd=v*3.6-90,[ex,ey]=pol(cx,cy,R0+18,aEnd);
|
||||
el(s,'circle',{cx:ex,cy:ey,r:2.2,fill:INK,class:'pop',style:`animation-delay:${.5+i*.1}s`});
|
||||
const num=txt(s,{x:cx,y:cy+2,'font-size':24,'font-weight':800,fill:INK,'text-anchor':'middle',
|
||||
class:'fade',style:`animation-delay:${.4+i*.1}s`},v);
|
||||
tip(num,`${name} — ${v} of 100`);
|
||||
txt(s,{x:cx,y:cy+18,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:`animation-delay:${.4+i*.1}s`},'OF 100');
|
||||
txt(s,{x:cx,y:cy+R0+42,'font-size':7.5,'font-weight':700,fill:'#55554F','text-anchor':'middle',
|
||||
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.5+i*.1}s`},name);
|
||||
});
|
||||
txt(s,{x:360,y:290,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
|
||||
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
|
||||
'ONE TICK = ONE PERSON · TWELVE O’CLOCK IS ZERO · THE RIGHT DIAL LIVES INSIDE THE LEFT');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>七月经营财报 — Lieflat R04 Demo</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Oswald:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
LIEFLAT 报告模板 04 · 月度经营财报 Demo(中文版)
|
||||
场景:月度经营 / 投资人月更 / 财务复盘
|
||||
排版参考:"2017 Website Traffic" 浅蓝海报(左侧竖排压缩大写
|
||||
书脊、右上角对齐节标题 + 细规线、底部 NOTABLE
|
||||
STATISTICS 大数统计行)—— 只学排版和字体,浅蓝不学。
|
||||
中西混排:数字走 Oswald(保留压缩感),汉字走 Noto Sans SC 黑体。
|
||||
汉字没有压缩字形,书脊改重黑体 + 收紧字距。
|
||||
色板正本 = color-presets.js 的 PORCELAIN 一套(青瓷蓝,明度即数据)。
|
||||
彩色加墨规则(INK_BOOST):数据发丝线宽 ×1.8,透明度地板 .85。
|
||||
图表正本 = lieflat gallery:
|
||||
图A F2 hairline line · basics-gallery.html
|
||||
图B F10 dot heat · basics-gallery.html
|
||||
图C F4 tick donut · basics-gallery.html
|
||||
图D F5 tick rows · basics-gallery.html
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
:root{
|
||||
/* PORCELAIN 角色 token(结构灰一律为文字色的透明档)*/
|
||||
--bg:rgb(247,242,235); --ink:rgb(8,31,92); --muted:rgba(8,31,92,.60); --faint:rgba(8,31,92,.32); --grid:rgba(8,31,92,.16);
|
||||
--num:'Oswald','Noto Sans SC',sans-serif;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--ink);font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
-webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
|
||||
display:flex;justify-content:center;padding:56px 24px}
|
||||
.sheet{width:1080px;max-width:1080px;display:grid;grid-template-columns:110px 1fr}
|
||||
|
||||
/* ── 书脊:重黑体竖排(学参考图4 的压缩大写)── */
|
||||
.spine{position:relative;border-right:1px solid var(--ink)}
|
||||
.spine .t{position:absolute;top:0;left:14px;writing-mode:vertical-rl;
|
||||
font-family:'Noto Sans SC',sans-serif;font-weight:900;font-size:56px;line-height:1.1;
|
||||
letter-spacing:.06em;white-space:nowrap}
|
||||
.spine .b{position:absolute;bottom:0;left:48px;writing-mode:vertical-rl;
|
||||
font-size:9px;font-weight:700;letter-spacing:.26em;color:var(--muted)}
|
||||
|
||||
.content{padding-left:40px}
|
||||
|
||||
/* ── 顶注 ── */
|
||||
.tophead{display:flex;gap:18px;align-items:baseline;border-bottom:1px solid var(--ink);
|
||||
padding-bottom:10px;margin-bottom:40px}
|
||||
.tophead .site{font-family:var(--num);font-weight:600;font-size:13px;
|
||||
letter-spacing:.08em;text-transform:uppercase}
|
||||
.tophead .sep{color:var(--faint)}
|
||||
.tophead .tag{font-size:10px;font-weight:600;letter-spacing:.12em;color:var(--muted)}
|
||||
|
||||
/* ── 节标题:右对齐 + 引线(学参考图4)── */
|
||||
.secthead{display:flex;align-items:baseline;gap:14px;margin:44px 0 8px}
|
||||
.secthead .line{flex:1;border-top:1px solid var(--ink)}
|
||||
.secthead .t{font-family:'Noto Sans SC',sans-serif;font-weight:700;font-size:15px;
|
||||
letter-spacing:.2em}
|
||||
.secthead.first{margin-top:0}
|
||||
|
||||
.claim{font-size:13px;font-weight:700;color:var(--ink);margin:4px 0 2px}
|
||||
.fig svg{width:100%;height:auto;display:block}
|
||||
svg text{font-family:'Inter','Noto Sans SC',sans-serif}
|
||||
.srcline{font-size:9px;font-weight:600;letter-spacing:.1em;
|
||||
color:var(--faint);margin-top:8px}
|
||||
|
||||
.duo{display:grid;grid-template-columns:1.25fr 1fr;gap:44px}
|
||||
|
||||
/* ── 底部统计行(学参考图4 的 NOTABLE STATISTICS)── */
|
||||
.stats{display:grid;grid-template-columns:1fr 1fr 1.2fr 1fr;gap:0;margin-top:14px;
|
||||
border-top:1px solid var(--ink)}
|
||||
.stats>div{padding:22px 20px 6px;border-right:1px dotted var(--faint)}
|
||||
.stats>div:first-child{padding-left:0}
|
||||
.stats>div:last-child{border-right:0;padding-right:0}
|
||||
.stats .kpi{margin-bottom:18px}
|
||||
.stats .kpi .v{font-family:var(--num);font-weight:700;font-size:38px;line-height:1}
|
||||
.stats .kpi .r{font-size:10px;font-weight:700;color:var(--muted);margin-top:4px}
|
||||
.stats .kpi .l{font-size:9.5px;font-weight:600;letter-spacing:.12em;
|
||||
color:var(--faint);margin-top:3px}
|
||||
.stats h4{font-family:'Noto Sans SC',sans-serif;font-weight:700;font-size:12.5px;
|
||||
letter-spacing:.14em;margin-bottom:10px}
|
||||
.ranked .row{display:flex;gap:12px;align-items:baseline;padding:8px 0;
|
||||
border-bottom:1px dotted var(--faint)}
|
||||
.ranked .row:last-child{border-bottom:0}
|
||||
.ranked .n{font-family:var(--num);font-weight:700;font-size:24px;min-width:38px}
|
||||
.ranked .d{font-size:10px;line-height:1.7;color:var(--muted)}
|
||||
.ranked .d b{color:var(--ink)}
|
||||
|
||||
.foot{margin-top:48px;border-top:1px solid var(--ink);padding-top:12px;
|
||||
display:flex;justify-content:space-between;font-size:9px;font-weight:600;
|
||||
letter-spacing:.12em;color:var(--muted)}
|
||||
|
||||
/* ── 动画(mono-tokens MOTION.css 原样)── */
|
||||
.pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
|
||||
@keyframes pop{from{transform:scale(0)}to{transform:none}}
|
||||
.fade{animation:fade .9s ease both}
|
||||
@keyframes fade{from{opacity:0}}
|
||||
.draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
|
||||
@keyframes draw{to{stroke-dashoffset:0}}
|
||||
@media(max-width:640px){body{padding:24px 10px}}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.pop,.fade{animation:none}
|
||||
.draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sheet">
|
||||
|
||||
<!-- ══ 书脊 ══ -->
|
||||
<div class="spine">
|
||||
<div class="t">七月经营财报</div>
|
||||
<div class="b">财务 · 2026</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tophead">
|
||||
<span class="site">YOUR COMPANY</span><span class="sep">|</span>
|
||||
<span class="tag">2026 年 7 月财务数据 · 全部指标环比上月</span>
|
||||
</div>
|
||||
|
||||
<!-- ══ 图A · F2 hairline line:本月逐日 ══
|
||||
模板:basics-gallery.html · "Thirty days of sign-ups" -->
|
||||
<div class="secthead first"><span class="line"></span><span class="t">每日收入</span></div>
|
||||
<div class="claim">收入在月中和月末各出现一次结算高峰,基本盘保持稳定</div>
|
||||
<div class="fig"><svg id="dayline" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||||
<div class="srcline">HAIRLINE LINE · 一点 = 一天 · 空心 = 周末 · 单位:万元</div>
|
||||
|
||||
<!-- ══ 图B · F10 dot heat:一周节奏 ══
|
||||
模板:basics-gallery.html · "When support gets loud" -->
|
||||
<div class="secthead"><span class="line"></span><span class="t">回款节奏</span></div>
|
||||
<div class="duo">
|
||||
<div>
|
||||
<div class="claim">周二上午十点,是本月回款最集中的时段</div>
|
||||
<div class="fig"><svg id="dotheat" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||||
<div class="srcline">DOT HEAT · 7 天 × 12 小时 · 点面积 = 回款笔数</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- ══ 图C · F4 tick donut:来源分布 ══
|
||||
模板:basics-gallery.html · "Where the traffic comes from" -->
|
||||
<div class="claim">订阅收入仍是最大来源,服务收入贡献第二增长曲线</div>
|
||||
<div class="fig"><svg id="donut" viewBox="0 0 330 300" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||||
<div class="srcline">TICK DONUT · 一格 = 收入构成的 1%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ 值得记住的数字 ══ -->
|
||||
<div class="secthead"><span class="line"></span><span class="t">值得记住的数字</span></div>
|
||||
<div class="stats">
|
||||
<div>
|
||||
<div class="kpi"><div class="v">¥2.84M</div><div class="r">环比 +12.6%</div><div class="l">营业收入</div></div>
|
||||
<div class="kpi"><div class="v">¥1.94M</div><div class="r">毛利率 68.4%</div><div class="l">毛利</div></div>
|
||||
<div class="kpi"><div class="v">¥0.62M</div><div class="r">连续第三个月为正</div><div class="l">经营现金流</div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kpi"><div class="v">68.4%</div><div class="r">较 6 月的 65.2% 上升</div><div class="l">毛利率</div></div>
|
||||
<div class="kpi"><div class="v">14.2</div><div class="r">按当前现金消耗估算</div><div class="l">现金可支撑月数</div></div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>各业务线毛利贡献</h4>
|
||||
<!-- ══ 图D · F5 tick rows(迷你)══
|
||||
模板:basics-gallery.html · "Six teams, shipped and counted" -->
|
||||
<div class="fig"><svg id="rows" viewBox="0 0 260 190" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||||
<div class="srcline" style="text-align:left">TICK ROWS · 一格 = 10 万元毛利</div>
|
||||
</div>
|
||||
<div class="ranked">
|
||||
<h4>本月收入贡献最高</h4>
|
||||
<div class="row"><div class="n">46%</div><div class="d"><b>企业订阅</b> —— 占全月收入的 46%</div></div>
|
||||
<div class="row"><div class="n">31%</div><div class="d"><b>实施服务</b> —— 连续第二个月增长</div></div>
|
||||
<div class="row"><div class="n">15%</div><div class="d"><b>API 用量</b> —— 受大客户扩容带动</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<span>月度经营财报 Demo · 2026 年 7 月</span>
|
||||
<span>演示数据 · 管理账 + Billing + Cash Flow · 8 月 1 日拉取</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
|
||||
const BG='rgb(247,242,235)',TXT='rgb(8,31,92)',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.32)';
|
||||
const FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',GRID='rgba(8,31,92,.16)';
|
||||
const DATA='rgb(51,78,172)',DATA2='rgb(112,150,209)',HERO='rgb(8,31,92)',FAINTDATA='rgb(186,214,235)';
|
||||
const CAT4=['rgb(8,31,92)','rgb(51,78,172)','rgb(112,150,209)','rgb(186,214,235)'],HEAT=['rgb(51,78,172)','rgb(112,150,209)','rgb(186,214,235)'];
|
||||
const NS='http://www.w3.org/2000/svg';
|
||||
const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
|
||||
const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
|
||||
const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
|
||||
const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
|
||||
const D2R=Math.PI/180;
|
||||
const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
|
||||
const obsReveal=(id,fn)=>{
|
||||
const n=document.getElementById(id);
|
||||
const go=()=>{n.innerHTML='';fn(n)};
|
||||
const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
|
||||
io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
|
||||
};
|
||||
|
||||
/* ════ 图A · F2 hairline line ════
|
||||
结构照搬 basics-gallery "hairline line":barcode 日历地板 +
|
||||
发丝折线 + 逐日圆点(周末空心),结算高峰放大标数。 */
|
||||
(()=>{
|
||||
const N=31;
|
||||
const val=d=>{
|
||||
const spike=(d===5||d===6)?38:(d===19?30:0); // 两次结算高峰
|
||||
return 58+10*Math.sin(d/5.2)+rnd(d+1,5)*10+spike;
|
||||
};
|
||||
obsReveal('dayline',s=>{
|
||||
const x=d=>26+d*11.4,base=254,map=v=>base-v*2.1;
|
||||
for(let d=0;d<N;d++)
|
||||
el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
|
||||
class:'fade',style:`animation-delay:${d*.008}s`});
|
||||
el(s,'line',{x1:20,y1:base,x2:380,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
|
||||
const vs=Array.from({length:N},(_,d)=>val(d));
|
||||
const top=[];
|
||||
for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
|
||||
if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
|
||||
if(top.length===2)break;
|
||||
}
|
||||
const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
|
||||
el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
|
||||
class:'draw',style:'animation-duration:1.2s'});
|
||||
vs.forEach((v,d)=>{
|
||||
const weekend=d%7===3||d%7===4,big=top.includes(d);
|
||||
const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
|
||||
fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
|
||||
class:'pop',style:`animation-delay:${.2+d*.03}s`});
|
||||
tip(dot,`7 月 ${d+1} 日 —— ${Math.round(v)} 万元收入`);
|
||||
if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
|
||||
style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
|
||||
class:'fade'},Math.round(v));
|
||||
});
|
||||
[[0,'7月1日'],[14,'7月15日'],[30,'7月31日']].forEach(([d,m])=>
|
||||
txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
|
||||
'letter-spacing':'.06em',class:'fade'},m));
|
||||
txt(s,{x:200,y:294,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
|
||||
'一点 = 一天 · 空心 = 周末 · 标数 = 结算高峰');
|
||||
});
|
||||
})();
|
||||
|
||||
/* ════ 图B · F10 dot heat ════
|
||||
结构照搬 basics-gallery "dot heat":7×12 网格,点面积 = 回款笔数,
|
||||
静默格留一粒极小点,最热格虚线圈标数。 */
|
||||
(()=>{
|
||||
const DAY=['周一','周二','周三','周四','周五','周六','周日'];
|
||||
obsReveal('dotheat',s=>{
|
||||
const x0=j=>64+j*27,y0=i=>50+i*30;
|
||||
let max=0,mi=0,mj=0;
|
||||
const v=(i,j)=>{
|
||||
const day=i<5?(i===1?1.25:1):.32;
|
||||
const hour=Math.exp(-((j-3.4)**2)/6)+.6*Math.exp(-((j-8.6)**2)/5);
|
||||
return Math.round(day*hour*20*(0.6+rnd(i*12+j+1,j+3)*.8));
|
||||
};
|
||||
for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
|
||||
for(let i=0;i<7;i++){
|
||||
txt(s,{x:50,y:y0(i)+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',
|
||||
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
|
||||
for(let j=0;j<12;j++){
|
||||
const t=v(i,j),x=x0(j),y=y0(i);
|
||||
if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,
|
||||
class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
|
||||
const hero=i===mi&&j===mj;
|
||||
const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
|
||||
fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],
|
||||
class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
|
||||
tip(dot,`${DAY[i]} ${8+j}:00 —— ${t} 笔回款`);
|
||||
if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
|
||||
stroke:TXT,'stroke-width':1,'stroke-dasharray':'2 3',
|
||||
class:'fade',style:'animation-delay:1s'});
|
||||
}
|
||||
}
|
||||
for(let j=0;j<12;j+=2)
|
||||
txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
|
||||
class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
|
||||
txt(s,{x:200,y:296,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
|
||||
`点面积 = 回款笔数 · 虚线圈 = 峰值 ${max} 笔 · 极小点 = 安静时段`);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ════ 图C · F4 tick donut ════
|
||||
结构照搬 basics-gallery "tick donut":1 tick = 1%,
|
||||
四段来源用灰阶 ladder 分段。 */
|
||||
(()=>{
|
||||
const D=[['订阅',46,CAT4[0]],['服务',31,CAT4[1]],['API',15,CAT4[2]],['其他',8,CAT4[3]]];
|
||||
obsReveal('donut',s=>{
|
||||
const cx=170,cy=140,R0=58;
|
||||
let k0=0;
|
||||
D.forEach(([name,v,shade],si)=>{
|
||||
for(let k=0;k<v;k++){
|
||||
const idx=k0+k,a=idx*3.6-90;
|
||||
const len=9+rnd(idx+1,si+2)*5.5;
|
||||
const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
|
||||
el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
|
||||
class:'fade',style:`animation-delay:${idx*.012}s`});
|
||||
if(idx%10===0){
|
||||
const [dx,dy]=pol(cx,cy,R0-5,a);
|
||||
el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
|
||||
class:'fade',style:`animation-delay:${idx*.012}s`});
|
||||
}
|
||||
}
|
||||
const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+34,mid),[gx,gy]=pol(cx,cy,R0+18,mid);
|
||||
el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
|
||||
'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
|
||||
const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
|
||||
const lab=txt(s,{x:lx,y:ly+3,'font-size':8.5,'font-weight':800,fill:shade,'text-anchor':anchor,
|
||||
'letter-spacing':'.04em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
|
||||
class:'fade'},`${name} · ${v}`);
|
||||
tip(lab,`${name}流量 —— 占 ${v}%`);
|
||||
k0+=v;
|
||||
});
|
||||
txt(s,{x:cx,y:cy-2,'font-size':20,'font-weight':800,fill:TXT,'text-anchor':'middle',
|
||||
class:'fade',style:'animation-delay:.9s'},'100');
|
||||
txt(s,{x:cx,y:cy+13,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
|
||||
'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},'格 · 一格 = 1%');
|
||||
txt(s,{x:170,y:288,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
|
||||
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
|
||||
'每十格一个圆点 · 顺时针读');
|
||||
});
|
||||
})();
|
||||
|
||||
/* ════ 图D · F5 tick rows(迷你版)════
|
||||
结构照搬 basics-gallery "tick rows":1 tick = 10 万元毛利。 */
|
||||
(()=>{
|
||||
const D=[['订阅',13],['服务',9],['API',6],['渠道',4],['其他',2]];
|
||||
obsReveal('rows',s=>{
|
||||
const y0=i=>28+i*34,X0=72,PX=4.6;
|
||||
D.forEach(([name,v],i)=>{
|
||||
const y=y0(i);
|
||||
txt(s,{x:64,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
|
||||
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
|
||||
el(s,'line',{x1:X0,y1:y+8,x2:X0+34*PX,y2:y+8,stroke:GRID,'stroke-width':.6,
|
||||
class:'fade',style:`animation-delay:${i*.08}s`});
|
||||
for(let k=0;k<v;k++){
|
||||
const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*5;
|
||||
el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:DATA,'stroke-width':1.6,
|
||||
opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
|
||||
if(k%5===4)el(s,'circle',{cx:x,cy:y+11.5,r:.7,fill:FAINT,
|
||||
class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
|
||||
}
|
||||
const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,fill:TXT,
|
||||
class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
|
||||
tip(lab,`${name} —— ${v * 10} 万元毛利`);
|
||||
});
|
||||
txt(s,{x:130,y:186,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
|
||||
'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},
|
||||
'一格 = 10 万元毛利 · 每五格一个圆点');
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script>
|
||||
/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
|
||||
报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
|
||||
设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
|
||||
机制同模板 11。 */
|
||||
(()=>{
|
||||
const sheet=document.querySelector('.sheet');
|
||||
const W=1080;
|
||||
const fit=()=>{
|
||||
const cs=getComputedStyle(document.body);
|
||||
const avail=document.documentElement.clientWidth
|
||||
-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
|
||||
sheet.style.zoom=Math.min(1,avail/W);
|
||||
};
|
||||
fit();
|
||||
addEventListener('resize',fit);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user