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
+122
View File
@@ -0,0 +1,122 @@
import { col } from '../csv.js';
import {
PALETTE, FONT, axisCommon, titleStyle, footnote, watermarks,
} from '../theme.js';
/**
* 净利率同比拆解瀑布图(2025H1 → 2026H1
*
* ECharts 没有原生瀑布图,用「透明占位柱 + 可见柱」堆叠模拟:
* 占位柱撑到该项起点,可见柱只画增量部分。
*
* 口径要点:各项先算占营收比的税前变化,再乘税盾 (1-t₁) 折成对净利率的贡献,
* 税率变化单独成项。恒等式 净利率 = 税前利润率 × (1-税率),
* Δ净 = Δ税前×(1-t₁) − 税前₂×Δt,实测闭合误差 0.00pct。
*/
const COLOR = {
total: '#2F5C85', // 首尾锚点
neg: '#C0392B', // 拖累
pos: '#1A9E5C', // 利好
};
export function ppmtBridgeChart(rows, { width, height }) {
const items = col(rows, 'item');
const types = col(rows, 'type');
const bases = col(rows, 'base');
const values = col(rows, 'value');
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: '净利率同比拆解:33.7% → 29.7%2025H1 → 2026H1',
subtext: '单位:占营收百分点。汇兑等其他收益项是最大拖累,费用率改善仅部分对冲',
},
grid: { left: 114, right: 83, top: 120, bottom: 130 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
textStyle: { fontFamily: FONT },
formatter: (ps) => {
const p = ps.find((x) => x.seriesName === '变化');
if (!p) return '';
const i = p.dataIndex;
if (types[i] === 'total') return `<b>${items[i]}</b><br/>${values[i].toFixed(2)}%`;
const sign = types[i] === 'pos' ? '+' : '';
return `<b>${items[i]}</b><br/>${sign}${values[i].toFixed(2)} pct`;
},
},
xAxis: {
type: 'category',
data: items,
axisLine: { show: true, lineStyle: { color: PALETTE.axisLine } },
axisTick: { show: false },
axisLabel: {
color: PALETTE.ink,
fontSize: 11,
fontFamily: FONT,
interval: 0,
rotate: 32,
margin: 12,
},
},
yAxis: {
...axisCommon,
type: 'value',
name: '净利率(%',
nameLocation: 'middle',
nameGap: 52,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 26,
max: 36,
interval: 2,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
},
series: [
{
// 占位柱:撑到每项起点,完全透明
name: '占位',
type: 'bar',
stack: 'bridge',
silent: true,
itemStyle: { color: 'transparent' },
emphasis: { itemStyle: { color: 'transparent' } },
data: bases,
},
{
name: '变化',
type: 'bar',
stack: 'bridge',
// 柱宽固定时 barCategoryGap 无效,间距只能靠 grid 左右边距调节:
// 步距 = 绘图宽 / 类目数,间隙 = 步距 − barWidth
barWidth: 51,
data: values.map((v, i) => ({
value: v,
itemStyle: { color: COLOR[types[i]] },
})),
label: {
show: true,
position: 'top',
fontFamily: FONT,
fontSize: 11,
fontWeight: 'bold',
color: PALETTE.ink,
formatter: (p) => {
const i = p.dataIndex;
if (types[i] === 'total') return `${values[i].toFixed(1)}%`;
// 所有项都显示(资产减值0.01、其他业务净额0.01也必须标)
const sign = types[i] === 'pos' ? '+' : '';
return `${sign}${values[i].toFixed(2)}pct`;
},
},
},
],
graphic: [
...watermarks(width, height),
footnote(
'各项为占营收百分点,已折算为对净利率影响(×税盾系数)\n' +
'其他收益净额:26H1净损失6.89亿,主要为汇兑损失7.20亿(25H1为汇兑收益1.20亿) | @思考的Joey'
),
],
};
}