跳转至

L11 · 趋势谱系图

事件序列生命史(首发/重做/休眠/存活)的 Lupi Editorial 真实参考实现。示例结论为“功能兴起、沉寂,再度归来”,副标题已经把图例、单位或时间口径写在图旁:● 已发布 · ○ 已重做 · 虚线代表沉寂年份 · 延伸至今的实线表示仍然存续

技术档案

项目 约定
数据形状 事件序列生命史(首发/重做/休眠/存活)
场合 产品复盘
读者时间 >30s
渲染引擎 SVG
交互与动效 滚入视野播放,点击图表重播;数据点带浏览器原生提示
示例来源行 TREND LINEAGE · MONO-EDITORIAL2 · PRODUCT HISTORY

实现与使用边界

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

模板来源:Lieflat Charts L11 Trend Lineage,真实骨架取自 templates/lupi-gallery.html 的 “Features rise, fall, come back” 卡片与对应渲染块。

l11-trend-lineage.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 — L11 趋势谱系图</title>
<link rel="stylesheet" href="../../../../vendor/fonts/fonts.css">
<style>
  :root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
  *{margin:0;padding:0;box-sizing:border-box}
  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
  .card.dark{background:var(--dark);color:#F0EFEB}
  .card.dark .sub{color:#8F8E88}
  .card.dark .src{color:#55554F}
  .card.wide{grid-column:1/-1}
  .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}
  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
  .note{font-size:11px;color:#6A6963;line-height:1.65;margin-bottom:16px}
  .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}
  svg{width:100%;max-height:330px;display:block;margin:0 auto}
  svg text{font-family:'Inter',sans-serif}
  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
  @keyframes pop{from{transform:scale(0)}to{transform:none}}
  .fade{animation:fade .9s ease both}
  @keyframes fade{from{opacity:0}}
  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
  @keyframes draw{to{stroke-dashoffset:0}}
  @media (prefers-reduced-motion:reduce){
    .pop,.fade{animation:none}
    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
  }

  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
  .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
  .pagehead .badge{margin:0 2px}
  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
    padding:3px 10px;border-radius:99px;margin-bottom:10px}
  .badge.del{background:#1C1C1A;color:#F0EFEB}
  .badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}
  .badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}
  .card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}
  .card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}
  .card.dark .badge.pair{border-color:#55554F;color:#8F8E88}

  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 编辑型 · L11</span>
    <h2>功能兴起、沉寂,再度归来</h2>
    <div class="sub">● 已发布 · ○ 已重做 · 虚线代表沉寂年份 · 延伸至今的实线表示仍然存续</div>
    <svg id="lineage" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg>
    <div class="src">TREND LINEAGE · MONO-EDITORIAL2 · PRODUCT HISTORY</div>
  </div>
</main>
<script>
// ── shared Mono tokens ──
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
const NS='http://www.w3.org/2000/svg';
const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
const D2R=Math.PI/180;
const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
const sect=(cx,cy,r0,r1,a0,a1)=>{
  const big=a1-a0>180?1:0;
  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
};
// unified reveal: draw when scrolled into view; click replays
const obsReveal=(id,fn)=>{
  const n=document.getElementById(id);
  const go=()=>{n.innerHTML='';fn(n)};
  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
  io.observe(n);
  n.style.cursor='pointer';
  n.addEventListener('click',go);
};

// ════ 6 · trend lineage ════
(()=>{
// [name, events [year, type f|o], aliveToday]
const TR=[
  ['Dark mode',[[2017,'f'],[2022,'o']],true],
  ['Kanban',[[2016,'f']],true],
  ['Wikis',[[2016,'f'],[2019,'o']],false],
  ['Slash cmds',[[2018,'f']],true],
  ['Templates',[[2019,'f'],[2024,'o']],true],
  ['Inbox',[[2020,'f']],false],
  ['Live cursors',[[2020,'f'],[2023,'o']],true],
  ['AI drafts',[[2023,'f'],[2025,'o']],true],
  ['Voice notes',[[2021,'f']],false],
  ['Offline',[[2018,'f'],[2026,'o']],true],
];
const Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;
obsReveal('lineage',s=>{
  for(let yr=Y0;yr<=2026;yr++){
    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:'#E3E2DB','stroke-width':.8,
      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
    if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUTED,'text-anchor':'end',
      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
  }
  TR.forEach(([name,ev,alive],c)=>{
    const x=colX(c);
    // segments between events: solid while fresh, dashed once dormant
    for(let k=0;k<ev.length-1;k++){
      const gap=ev[k+1][0]-ev[k][0];
      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
        stroke:'#B0AFA9','stroke-width':1,
        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
    }
    const last=ev[ev.length-1][0];
    if(alive)
      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:'#B0AFA9','stroke-width':1,
        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
    ev.forEach(([yr,t])=>{
      const node=el(s,'circle',t==='f'
        ?{cx:x,cy:yY(yr),r:5.5,fill:INK}
        :{cx:x,cy:yY(yr),r:5.5,fill:PAPER,stroke:INK,'stroke-width':1.4});
      node.setAttribute('class','pop');
      node.style.animationDelay=(.35+c*.07)+'s';
      tip(node,`${name}${t==='f'?'shipped':'reworked'} ${yr}`);
    });
    // terminal dot: alive lands on today's baseline, dead stops where it stopped
    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
      fill:alive?INK:'#C6C5BF',class:'pop',style:`animation-delay:${.6+c*.07}s`});
    const ly=yY(2026)+26;
    txt(s,{x,y:ly,'font-size':6.8,'font-weight':600,fill:alive?'#4A4944':'#B0AFA9',
      transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',
      class:'fade',style:`animation-delay:${.7+c*.05}s`},name);
  });
});
})();
</script>
</body>
</html>