feat: 构建泡泡玛特相关图表

This commit is contained in:
Joey
2026-08-23 21:23:41 +08:00
commit 3ae396776d
31 changed files with 3454 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import * as echarts from 'echarts';
import fs from 'fs';
// 项目根目录(从 test/regress.mjs 向上一层,或直接指定绝对路径)
const P = '/Users/joey/sites/vest-tools/echart-demo/';
const {parseCSV}=await import(P+'js/csv.js');
const all=[
['inventory','inventoryChart','data/inventory.csv'],
['margin','marginChart','data/margin.csv'],
['twoline','twoLineChart','data/margin.csv'],
['growth','growthChart','data/growth.csv'],
['fx','fxChart','data/fx.csv'],
['ppmt_assets','assetsChart','data/ppmt_assets.csv'],
['ppmt_profit','profitChart','data/ppmt_profit.csv'],
['ppmt_ip','ipChart','data/ppmt_ip.csv'],
['ppmt_category','categoryChart','data/ppmt_category.csv'],
['ppmt_sankey','sankeyChart','data/ppmt_sankey_2026h1.csv'],
['ppmt_region_yoy','regionChannelYoyChart','data/ppmt_region_channel_yoy.csv'],
['ppmt_growth','ppmtGrowthChart','data/ppmt_growth.csv'],
['ppmt_margins','ppmtMarginsChart','data/ppmt_margins.csv'],
['ppmt_bridge','ppmtBridgeChart','data/ppmt_margin_bridge.csv'],
['ppmt_core_profit','ppmtCoreProfitChart','data/ppmt_core_profit.csv'],
['ppmt_core_margin','ppmtCoreMarginChart','data/ppmt_core_margin.csv'],
['ppmt_ip_share','ppmtIpShareChart','data/ppmt_ip_share_2026h1.csv'],
['ppmt_ip_share_history','ppmtIpShareHistoryChart','data/ppmt_ip_share_history.csv'],
['ppmt_ip_ex_monsters','ppmtIpExMonstersChart','data/ppmt_ip_ex_monsters.csv'],
];
// 可传图表名只跑指定的(改一张图不用全量回归):
// node test/regress.mjs ppmt_ip_share ppmt_core_margin
const only=process.argv.slice(2);
const targets=only.length>0 ? all.filter(([f])=>only.includes(f)) : all;
if(only.length>0){
const missing=only.filter(f=>!all.some(([name])=>name===f));
if(missing.length>0) console.log(`⚠ 未找到图表:${missing.join(', ')}`);
}
// 样式规范断言:从核心利润/利润率拆解图总结的标准
const STYLE_RULES={
legendTop:85, // 固定值:副标题到图例约25px
gridTop:145, // 固定值:图例到绘图区约60px
lineSeries:{ // 折线标准样式(空心圆点)
symbolSize:9,
borderWidth:2.5,
fill:'#fff', // 白心
},
};
let ok=0,fail=0,styleWarnings=[];
for(const [f,fn,csv] of targets){
try{
const mod=await import(P+`js/charts/${f}.js`);
const rows=parseCSV(fs.readFileSync(P+csv,'utf8'));
const c=echarts.init(null,null,{renderer:'svg',ssr:true,width:1100,height:720});
const opt=mod[fn](rows,{width:1100,height:720});
c.setOption(opt);
const svg=c.renderToSVGString();
// 1. SVG 格式检查(字体引号)
const badAttr=/style="[^"]*"[^"=>\s][^>]*>/.test(svg.slice(0,3000));
// 2. 样式规范检查(跳过无图例的图表)
if(opt.legend?.top!==undefined){
if(opt.legend.top!==STYLE_RULES.legendTop){
styleWarnings.push(`${f}: legend.top=${opt.legend.top} (期望${STYLE_RULES.legendTop})`);
}
}
if(opt.grid?.top!==undefined && opt.legend?.top!==undefined){
if(opt.grid.top!==STYLE_RULES.gridTop){
styleWarnings.push(`${f}: grid.top=${opt.grid.top} (期望${STYLE_RULES.gridTop})`);
}
}
// 3. 折线圆点样式检查(在 SVG 里验证 symbolSize 和 borderWidth
const circles=[...svg.matchAll(/<path d="M1 0A1[^"]*" transform="matrix\(([\d.]+),0,0,[\d.]+,[^)]+\)" fill="#fff" stroke="[^"]*" stroke-width="([\d.]+)"/g)];
if(circles.length>0){
const scale=+circles[0][1], sw=+circles[0][2];
const symbolSize=scale*2, borderWidth=sw*scale;
if(Math.abs(symbolSize-STYLE_RULES.lineSeries.symbolSize)>0.1){
styleWarnings.push(`${f}: 折线 symbolSize=${symbolSize.toFixed(1)} (期望${STYLE_RULES.lineSeries.symbolSize})`);
}
if(Math.abs(borderWidth-STYLE_RULES.lineSeries.borderWidth)>0.1){
styleWarnings.push(`${f}: 折线 borderWidth=${borderWidth.toFixed(1)} (期望${STYLE_RULES.lineSeries.borderWidth})`);
}
}
console.log(`${badAttr?'⚠':'✓'} ${f.padEnd(17)} ${String(svg.length).padStart(6)}B`);
ok++; c.dispose();
}catch(e){console.log(`${f.padEnd(17)} ${e.message}`);fail++;}
}
console.log(`\n${ok} 张成功 / ${fail} 张失败`);
if(styleWarnings.length>0){
console.log(`\n${styleWarnings.length} 处样式偏离规范:`);
styleWarnings.forEach(w=>console.log(` ${w}`));
}else{
console.log('✓ 样式规范全部符合');
}
process.exit(fail>0?1:0);