跳转至

F2 · 发丝折线图

日序列(≤30 天,逐日读数)的 Lupi Basics 真实参考实现。示例结论为“三十天注册量,一天不落”,副标题已经把图例、单位或时间口径写在图旁:一个圆点代表一天 · 空心圆点代表周末 · 底部条码保留完整日历

技术档案

项目 约定
数据形状 日序列(≤30 天,逐日读数)
场合 年报/故事页
读者时间 ~30s
渲染引擎 SVG
交互与动效 滚入视野播放,点击图表重播;数据点带浏览器原生提示
示例来源行 HAIRLINE LINE · MONO-BASIC · GROWTH

实现与使用边界

  • 页面直接复用 gallery 中 “Thirty days of sign-ups” 的卡片结构和同名渲染块,没有按截图重新绘制。
  • 标题、副标题、图形、来源行四件套保持原样;数据编码、比例关系、灰阶和动画节奏由原实现决定。
  • 使用时只替换数据与文案,并保持“日序列(≤30 天,逐日读数)”这一数据契约;若数据本体改变,应回到总目录重新选型。
  • 颜色限于 Mono 灰阶,面积编码继续使用平方根换算;柱长、位置与角度编码不得断轴或视觉夸大。

模板来源:Lieflat Charts F2 Hairline Line,真实骨架取自 templates/basics-gallery.html 的 “Thirty days of sign-ups” 卡片与对应渲染块。

f2-hairline-line.html —— 自包含单页源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lieflat Charts — F2 发丝折线图</title>
<link rel="stylesheet" href="../../../../vendor/fonts/fonts.css">
<style>
  :root{--bg:#F0EFEB;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
  *{margin:0;padding:0;box-sizing:border-box}
  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #B0AFA9;color:#8F8E88}
  svg{width:100%;max-height:330px;display:block;margin:0 auto}
  svg text{font-family:'Inter',sans-serif}
  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
  @keyframes pop{from{transform:scale(0)}to{transform:none}}
  .fade{animation:fade .9s ease both}
  @keyframes fade{from{opacity:0}}
  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
  @keyframes draw{to{stroke-dashoffset:0}}

  .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
  .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}

  html,body{min-height:100%;margin:0}
  body{display:grid;place-items:center;padding:28px}
  .pagehead{display:none}
  .grid2{display:block;width:min(100%,920px);max-width:920px;margin:0 auto}
  .card,.card.wide{display:block;width:100%;min-height:520px;padding:38px 42px 24px}
  .card .split{grid-template-columns:minmax(190px,250px) 1fr}
  .wiki-code{display:inline-block;margin-bottom:12px;padding:4px 11px;border:1px dashed #B0AFA9;border-radius:99px;color:#8F8E88;font-size:10px;font-weight:700;letter-spacing:.08em}
  .card.dark .wiki-code{border-color:#55554F}
  @media(max-width:620px){
    body{padding:10px}
    .card,.card.wide{min-height:500px;padding:26px 18px 20px}
    .card .split{display:block}
    .card .note,.card .legend{display:none}
  }
</style>

</head>
<body>
<main class="grid2">
<div class="card">
    <span class="wiki-code">LUPI 基础型 · F2</span>
    <span class="badge">LUPI 基础型 · 折线</span>
    <h2>三十天注册量,一天不落</h2>
    <div class="sub">一个圆点代表一天 · 空心圆点代表周末 · 底部条码保留完整日历</div>
    <svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
    <div class="src">HAIRLINE LINE · MONO-BASIC · GROWTH</div>
  </div>
</main>
<script>
// ── shared Mono tokens(与 lupi-gallery 同源) ──
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
const NS='http://www.w3.org/2000/svg';
const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
const D2R=Math.PI/180;
const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
const obsReveal=(id,fn)=>{
  const n=document.getElementById(id);
  const go=()=>{n.innerHTML='';fn(n)};
  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
  io.observe(n);
  n.style.cursor='pointer';
  n.addEventListener('click',go);
};

// ════ B2 · hairline line ════
// 折线图的 Lupi 化:barcode 的日历地板(每天一根发丝,无论有没有事)+
// 发丝折线 + 逐日圆点(周末空心)。峰值两个点放大标数。
(()=>{
const N=30;
const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
obsReveal('dayline',s=>{
  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
  // calendar floor
  for(let d=0;d<N;d++)
    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:'#CFCEC7','stroke-width':.6,
      class:'fade',style:`animation-delay:${d*.008}s`});
  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
  const vs=Array.from({length:N},(_,d)=>val(d));
  // top-2 peaks, apart
  const top=[];
  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
    if(top.length===2)break;
  }
  // hairline path
  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
  el(s,'path',{d:'M'+pts,fill:'none',stroke:INK,'stroke-width':1,pathLength:1,
    class:'draw',style:'animation-duration:1.2s'});
  vs.forEach((v,d)=>{
    const weekend=d%7===5||d%7===6,big=top.includes(d);
    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
      fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1:0,
      class:'pop',style:`animation-delay:${.2+d*.03}s`});
    tip(dot,`Day ${d+1}${Math.round(v)} sign-ups`);
    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${1+d*.01}s`,
      class:'fade'},Math.round(v));
  });
  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
      'letter-spacing':'.1em',class:'fade'},m));
  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
});
})();
</script>
</body>
</html>