G18 · 一笔绘制与计数器
累计增长(一条线+一个大数)的 Glance 真实参考实现。示例结论为“上半年收入,一笔画出”,副标题已经把图例、单位或时间口径写在图旁:累计年度经常性收入 · 曲线绘制的同时数字递增 · 点击重播
技术档案
| 项目 | 约定 |
|---|---|
| 数据形状 | 累计增长(一条线+一个大数) |
| 场合 | 短视频/汇报开场 |
| 读者时间 | 动画 |
| 渲染引擎 | ECharts |
| 交互与动效 | 滚入视野初始化,点击图表重播;保留原 gallery 的 tooltip 与动画 |
| 示例来源行 | DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE |
实现与使用边界
- 页面直接复用 gallery 中 “H1 revenue, drawn in one stroke” 的卡片结构和同名渲染块,没有按截图重新绘制。
- 标题、副标题、图形、来源行四件套保持原样;数据编码、比例关系、灰阶和动画节奏由原实现决定。
- 使用时只替换数据与文案,并保持“累计增长(一条线+一个大数)”这一数据契约;若数据本体改变,应回到总目录重新选型。
- 颜色限于 Mono 灰阶,面积编码继续使用平方根换算;柱长、位置与角度编码不得断轴或视觉夸大。
模板来源:Lieflat Charts
G18 Draw-in + Counter,真实骨架取自templates/glance-gallery.html的 “H1 revenue, drawn in one stroke” 卡片与对应渲染块。
g18-draw-in-plus-counter.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 — G18 一笔绘制与计数器</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
<link rel="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}
h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
.sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
.src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
.ch{height:320px}
.wrap{position:relative;height:320px}
svg text{font-family:'Inter',sans-serif}
.pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
@keyframes pop{from{transform:scale(0)}to{transform:none}}
.pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
.pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
.pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
.pagehead .badge{margin:0 2px}
.badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
padding:3px 10px;border-radius:99px;margin-bottom:10px}
.badge.del{background:#1C1C1A;color:#F0EFEB}
.badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}
.badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}
.card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}
.card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}
.card.dark .badge.pair{border-color:#55554F;color:#8F8E88}
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">GLANCE 快读型 · G18</span>
<h2>上半年收入,一笔画出</h2>
<div class="sub">累计年度经常性收入 · 曲线绘制的同时数字递增 · 点击重播</div>
<div class="ch" id="stroke"></div>
<div class="src">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE</div>
</div>
</main>
<script>
// ── shared Mono tokens ──
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
const L=['#1C1C1A','#4A4944','#6A6963','#8F8E88','#B0AFA9','#C6C5BF'];
const LAD=['#1C1C1A','#4A4944','#8F8E88','#B0AFA9','#D8D7D1'];
const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
// ── unified reveal: every chart waits to be scrolled into view; click replays.
// fn charts manage their own timers via keep(); they're cleared on replay.
const timers={};
const obsReveal=(id,fn)=>{
const el=document.getElementById(id);
const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
const io=new IntersectionObserver(es=>{
if(es[0].isIntersecting){go();io.disconnect()}
},{threshold:.35});
io.observe(el);
el.style.cursor='pointer';
el.addEventListener('click',go);
};
const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
const eReveal=(id,opt)=>obsReveal(id,el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();g.setOption(opt);
});
const cjs={};
const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
cjs[id]?.destroy();
cjs[id]=new Chart(el,mkCfg());
});
Chart.defaults.color=MUTED;
Chart.defaults.font.family='Inter';
Chart.defaults.font.size=10.5;
Chart.defaults.borderColor=GRID;
Chart.defaults.plugins.legend.display=false;
Chart.defaults.plugins.tooltip.backgroundColor=INK;
Chart.defaults.plugins.tooltip.titleColor='#F0EFEB';
Chart.defaults.plugins.tooltip.bodyColor='#A5A49E';
Chart.defaults.plugins.tooltip.padding=12;
Chart.defaults.plugins.tooltip.cornerRadius=12;
Chart.defaults.plugins.tooltip.displayColors=false;
// ════ mono-fancy4 · 3: draw-in + counter ════
(()=>{
const days=Array.from({length:180},(_,i)=>i);
const daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);
const cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);
const TOTAL=cum[cum.length-1];
const DUR=2600;
let gen=0; // replay guard: only the latest counter loop may write
obsReveal('stroke',el=>{
const g=echarts.getInstanceByDom(el)||echarts.init(el);
g.clear();
gen++;const my=gen;
g.setOption({
animationDuration:DUR,animationEasing:'cubicOut',
tooltip:{show:false},
grid:{left:14,right:20,top:64,bottom:24},
xAxis:{type:'category',data:days,boundaryGap:false,
axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
axisLabel:{color:'#C6C5BF',fontFamily:'Inter',fontSize:9,fontWeight:600,
interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},
yAxis:{show:false,max:TOTAL*1.06},
series:[{
type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',
lineStyle:{color:INK,width:2.6},
areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
colorStops:[{offset:0,color:'rgba(28,28,26,.14)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
}],
});
const t0=performance.now();
const tick=()=>{
if(my!==gen)return;
const p=Math.min(1,(performance.now()-t0)/DUR);
const eased=1-Math.pow(1-p,3);
const val=(TOTAL*eased/1000).toFixed(2);
g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[
{type:'text',style:{text:'$'+val+'M',font:'800 32px Inter',fill:INK}},
{type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:'#8F8E88'}},
]}]});
if(p<1)requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
})();
addEventListener('resize',()=>{
['rose','forest','circular','tree','rainfall','morph','negbar','force','wave',
'custompie','singleaxis','jitter','race','stream','stroke']
.forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
});
</script>
</body>
</html>