import { col } from '../csv.js'; import { PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; /** * 泡泡玛特营收到利润的增速对比(2020H1-2026H1) * 四条折线展示从成本到净利的增速分化。 * 净利为主角走实线,其余三条虚线退为参照。 */ const SERIES = [ { key: 'non_gaap_net_profit', name: '非GAAP归母净利', color: '#8B4789', width: 2.6, z: 40, dashed: false }, { key: 'gross_profit', name: '毛利润', color: '#D9822B', width: 1.8, z: 30, dashed: true }, { key: 'revenue', name: '营业收入', color: '#2F5C85', width: 1.8, z: 20, dashed: true }, { key: 'cost', name: '营业成本', color: '#5B8C5A', width: 1.8, z: 10, dashed: true }, ]; /** * 逐点决定标签方向:同一期内按数值排名交替 top/bottom, * 相邻名次朝反方向散开。静态方向在 2020H1(四值挤在 16~61%)必然重叠, * 而 label.position 不接受函数,只能按点覆盖 data。 */ function labeledData(rows, key) { return rows.map((row, i) => { const value = row[key]; if (value === '' || value == null) return null; // 该期所有系列的值,降序排名 const ranked = SERIES .map((s) => row[s.key]) .filter((v) => v !== '' && v != null) .sort((a, b) => b - a); const rank = ranked.indexOf(value); return { value, label: { position: rank % 2 === 0 ? 'top' : 'bottom', distance: 11 + Math.floor(rank / 2) * 13, }, }; }); } export function ppmtGrowthChart(rows, { width, height }) { const pct = (p) => `${p.value}%`; return { backgroundColor: '#fff', title: { ...titleStyle, text: '泡泡玛特营收到利润的增速对比(2020H1-2026H1)', subtext: '同比增速(%),净利增速显著高于营收增速体现利润弹性', }, legend: { // 不继承 legendStyle,让 ECharts 按系列自动生成「短线+空心圆」图例 top: LAYOUT.legendTop, itemGap: 34, textStyle: { color: PALETTE.ink, fontSize: 14, fontFamily: FONT, }, data: ['非GAAP归母净利', '毛利润', '营业收入', '营业成本'], }, grid: { left: 84, right: 56, top: LAYOUT.gridTop, bottom: 80 }, tooltip: { trigger: 'axis', valueFormatter: (v) => `${v}%`, textStyle: { fontFamily: FONT }, }, xAxis: { ...periodAxis, data: col(rows, 'period') }, yAxis: { ...axisCommon, type: 'value', name: '同比增速(%)', nameLocation: 'middle', nameGap: 58, nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT }, min: -100, max: 400, interval: 100, axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' }, }, series: SERIES.map((s) => ({ ...lineSeries({ name: s.name, data: labeledData(rows, s.key), color: s.color, formatter: pct, width: s.width, }), // 整体替换 lineStyle,必须带上 color,否则会退回主题默认色 lineStyle: { color: s.color, type: s.dashed ? 'dashed' : 'solid', width: s.width, }, // 四条线数值常贴到一起。白描边保证交叠处可读, // labelLayout.hideOverlap 让 ECharts 自动丢弃仍然碰撞的标签, // 按 z 决定谁被保留 —— 净利最高,成本最先被舍弃。 label: { show: true, color: s.color, fontSize: 12, fontWeight: 500, fontFamily: FONT, formatter: pct, textBorderColor: '#fff', textBorderWidth: 3, }, labelLayout: { hideOverlap: true }, z: s.z, })), graphic: [ ...watermarks(width, height), footnote('数据来源:泡泡玛特财报,2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'), ], }; }