Files
2026-08-23 21:23:41 +08:00

122 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'),
],
};
}