feat: 更新泡泡玛特图表,加入单店收入

This commit is contained in:
2026-09-06 22:31:55 +08:00
parent 2555453b79
commit b3c7c30a1f
22 changed files with 566 additions and 30 deletions
+17
View File
@@ -0,0 +1,17 @@
period,inventory,yoy
2017FY,0.16,
2018FY,0.29,87.0
2019FY,0.96,231.4
2020H1,2.24,
2020FY,2.25,134.0
2021H1,3.16,41.0
2021FY,7.89,250.0
2022H1,9.57,203.0
2022FY,8.67,9.9
2023H1,7.59,-20.8
2023FY,9.05,4.4
2024H1,9.17,20.8
2024FY,15.25,68.5
2025H1,22.74,148.0
2025FY,54.73,259.0
2026H1,61.02,168.4
1 period inventory yoy
2 2017FY 0.16
3 2018FY 0.29 87.0
4 2019FY 0.96 231.4
5 2020H1 2.24
6 2020FY 2.25 134.0
7 2021H1 3.16 41.0
8 2021FY 7.89 250.0
9 2022H1 9.57 203.0
10 2022FY 8.67 9.9
11 2023H1 7.59 -20.8
12 2023FY 9.05 4.4
13 2024H1 9.17 20.8
14 2024FY 15.25 68.5
15 2025H1 22.74 148.0
16 2025FY 54.73 259.0
17 2026H1 61.02 168.4
+10
View File
@@ -0,0 +1,10 @@
period,inventory,yoy
2017FY,0.16,
2018FY,0.29,87.0
2019FY,0.96,231.4
2020FY,2.25,134.0
2021FY,7.89,250.0
2022FY,8.67,9.9
2023FY,9.05,4.4
2024FY,15.25,68.5
2025FY,54.73,259.0
1 period inventory yoy
2 2017FY 0.16
3 2018FY 0.29 87.0
4 2019FY 0.96 231.4
5 2020FY 2.25 134.0
6 2021FY 7.89 250.0
7 2022FY 8.67 9.9
8 2023FY 9.05 4.4
9 2024FY 15.25 68.5
10 2025FY 54.73 259.0
+8
View File
@@ -0,0 +1,8 @@
period,inventory,yoy
2020H1,2.24,
2021H1,3.16,41.0
2022H1,9.57,203.0
2023H1,7.59,-20.8
2024H1,9.17,20.8
2025H1,22.74,148.0
2026H1,61.02,168.4
1 period inventory yoy
2 2020H1 2.24
3 2021H1 3.16 41.0
4 2022H1 9.57 203.0
5 2023H1 7.59 -20.8
6 2024H1 9.17 20.8
7 2025H1 22.74 148.0
8 2026H1 61.02 168.4
+8
View File
@@ -0,0 +1,8 @@
period,china_retail_revenue,china_average_stores,china_per_store,apac_retail_revenue,apac_average_stores,apac_per_store,americas_retail_revenue,americas_average_stores,americas_per_store,europe_other_retail_revenue,europe_other_average_stores,europe_other_per_store
2024H1,20.04,381.0,526.0,,,,,,,,,
2024H2,25.97,415.0,625.8,11.83,46.5,2544.1,2.54,16.0,1587.5,1.16,11.5,1008.7
2024FY,46.01,397.0,1158.9,,,,,,,,,
2025H1,44.06,437.0,1008.2,14.93,61.5,2427.6,7.37,31.5,2339.7,2.72,16.0,1700.0
2025H2,56.69,444.0,1276.8,28.26,77.0,3670.1,12.67,52.5,2413.3,5.84,27.0,2163.0
2025FY,100.75,438.0,2300.2,43.19,69.5,6214.4,20.04,43.0,4660.5,8.56,25.0,3424.0
2026H1,60.90,450.0,1353.3,17.16,87.5,1961.1,9.03,75.0,1204.0,4.03,40.5,995.1
1 period china_retail_revenue china_average_stores china_per_store apac_retail_revenue apac_average_stores apac_per_store americas_retail_revenue americas_average_stores americas_per_store europe_other_retail_revenue europe_other_average_stores europe_other_per_store
2 2024H1 20.04 381.0 526.0
3 2024H2 25.97 415.0 625.8 11.83 46.5 2544.1 2.54 16.0 1587.5 1.16 11.5 1008.7
4 2024FY 46.01 397.0 1158.9
5 2025H1 44.06 437.0 1008.2 14.93 61.5 2427.6 7.37 31.5 2339.7 2.72 16.0 1700.0
6 2025H2 56.69 444.0 1276.8 28.26 77.0 3670.1 12.67 52.5 2413.3 5.84 27.0 2163.0
7 2025FY 100.75 438.0 2300.2 43.19 69.5 6214.4 20.04 43.0 4660.5 8.56 25.0 3424.0
8 2026H1 60.90 450.0 1353.3 17.16 87.5 1961.1 9.03 75.0 1204.0 4.03 40.5 995.1
@@ -0,0 +1,3 @@
period,china_retail_revenue,china_average_stores,china_per_store,apac_retail_revenue,apac_average_stores,apac_per_store,americas_retail_revenue,americas_average_stores,americas_per_store,europe_other_retail_revenue,europe_other_average_stores,europe_other_per_store
2024FY,46.01,397.0,1158.9,,,,,,,,,
2025FY,100.75,438.0,2300.2,43.19,69.5,6214.4,20.04,43.0,4660.5,8.56,25.0,3424.0
1 period china_retail_revenue china_average_stores china_per_store apac_retail_revenue apac_average_stores apac_per_store americas_retail_revenue americas_average_stores americas_per_store europe_other_retail_revenue europe_other_average_stores europe_other_per_store
2 2024FY 46.01 397.0 1158.9
3 2025FY 100.75 438.0 2300.2 43.19 69.5 6214.4 20.04 43.0 4660.5 8.56 25.0 3424.0
@@ -0,0 +1,6 @@
period,china_retail_revenue,china_average_stores,china_per_store,apac_retail_revenue,apac_average_stores,apac_per_store,americas_retail_revenue,americas_average_stores,americas_per_store,europe_other_retail_revenue,europe_other_average_stores,europe_other_per_store
2024H1,20.04,381.0,526.0,,,,,,,,,
2024H2,25.97,415.0,625.8,11.83,46.5,2544.1,2.54,16.0,1587.5,1.16,11.5,1008.7
2025H1,44.06,437.0,1008.2,14.93,61.5,2427.6,7.37,31.5,2339.7,2.72,16.0,1700.0
2025H2,56.69,444.0,1276.8,28.26,77.0,3670.1,12.67,52.5,2413.3,5.84,27.0,2163.0
2026H1,60.90,450.0,1353.3,17.16,87.5,1961.1,9.03,75.0,1204.0,4.03,40.5,995.1
1 period china_retail_revenue china_average_stores china_per_store apac_retail_revenue apac_average_stores apac_per_store americas_retail_revenue americas_average_stores americas_per_store europe_other_retail_revenue europe_other_average_stores europe_other_per_store
2 2024H1 20.04 381.0 526.0
3 2024H2 25.97 415.0 625.8 11.83 46.5 2544.1 2.54 16.0 1587.5 1.16 11.5 1008.7
4 2025H1 44.06 437.0 1008.2 14.93 61.5 2427.6 7.37 31.5 2339.7 2.72 16.0 1700.0
5 2025H2 56.69 444.0 1276.8 28.26 77.0 3670.1 12.67 52.5 2413.3 5.84 27.0 2163.0
6 2026H1 60.90 450.0 1353.3 17.16 87.5 1961.1 9.03 75.0 1204.0 4.03 40.5 995.1
+8
View File
@@ -0,0 +1,8 @@
period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
2024H1,3.38,2207.0,15.3,,,,
2024H2,4.13,2281.5,18.1,0.49,,
2024FY,7.51,2264.5,33.2,0.80,116.0,69.0
2025H1,6.78,2388.0,28.4,1.42,146.5,96.9
2025H2,6.68,2416.5,27.6,2.02,200.5,100.7
2025FY,13.46,2367.5,56.9,3.44,187.0,184.0
2026H1,7.79,2439.0,31.9,1.72,293.0,58.7
1 period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
2 2024H1,3.38,2207.0,15.3,,,,
3 2024H2,4.13,2281.5,18.1,0.49,,
4 2024FY,7.51,2264.5,33.2,0.80,116.0,69.0
5 2025H1,6.78,2388.0,28.4,1.42,146.5,96.9
6 2025H2,6.68,2416.5,27.6,2.02,200.5,100.7
7 2025FY,13.46,2367.5,56.9,3.44,187.0,184.0
8 2026H1,7.79,2439.0,31.9,1.72,293.0,58.7
@@ -0,0 +1,3 @@
period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
2024FY,7.51,2264.5,33.2,0.80,116.0,69.0
2025FY,13.46,2367.5,56.9,3.44,187.0,184.0
1 period china_robot_revenue china_average_stores china_per_store overseas_robot_revenue overseas_average_stores overseas_per_store
2 2024FY 7.51 2264.5 33.2 0.80 116.0 69.0
3 2025FY 13.46 2367.5 56.9 3.44 187.0 184.0
@@ -0,0 +1,6 @@
period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
2024H1,3.38,2207.0,15.3,,,,
2024H2,4.13,2281.5,18.1,0.49,,
2025H1,6.78,2388.0,28.4,1.42,146.5,96.9
2025H2,6.68,2416.5,27.6,2.02,200.5,100.7
2026H1,7.79,2439.0,31.9,1.72,293.0,58.7
1 period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
2 2024H1,3.38,2207.0,15.3,,,,
3 2024H2,4.13,2281.5,18.1,0.49,,
4 2025H1,6.78,2388.0,28.4,1.42,146.5,96.9
5 2025H2,6.68,2416.5,27.6,2.02,200.5,100.7
6 2026H1,7.79,2439.0,31.9,1.72,293.0,58.7
+6
View File
@@ -0,0 +1,6 @@
period,global,china,apac,americas,europe,other
2024H1,457,87.3,8.5,2.2,2.0,0.0
2024FY,521,82.7,10.4,4.2,2.7,0.0
2025H1,571,77.6,12.1,7.2,3.2,0.0
2025FY,630,70.6,13.5,10.2,5.7,0.0
2026H1,676,67.3,13.3,12.7,6.7,0.0
1 period global china apac americas europe other
2 2024H1 457 87.3 8.5 2.2 2.0 0.0
3 2024FY 521 82.7 10.4 4.2 2.7 0.0
4 2025H1 571 77.6 12.1 7.2 3.2 0.0
5 2025FY 630 70.6 13.5 10.2 5.7 0.0
6 2026H1 676 67.3 13.3 12.7 6.7 0.0
@@ -0,0 +1,3 @@
period,global,china,apac,americas,europe,other
2024FY,521,82.7,10.4,4.2,2.7,0.0
2025FY,630,70.6,13.5,10.2,5.7,0.0
1 period global china apac americas europe other
2 2024FY 521 82.7 10.4 4.2 2.7 0.0
3 2025FY 630 70.6 13.5 10.2 5.7 0.0
@@ -0,0 +1,4 @@
period,global,china,apac,americas,europe,other
2024H1,457,87.3,8.5,2.2,2.0,0.0
2025H1,571,77.6,12.1,7.2,3.2,0.0
2026H1,676,67.3,13.3,12.7,6.7,0.0
1 period global china apac americas europe other
2 2024H1 457 87.3 8.5 2.2 2.0 0.0
3 2025H1 571 77.6 12.1 7.2 3.2 0.0
4 2026H1 676 67.3 13.3 12.7 6.7 0.0
+13 -13
View File
@@ -1,14 +1,14 @@
period,inventory_days,receivable_days,payable_days,cash_conversion_cycle
2020H1,101.4,9.6,27.6,83.3
2020FY,63.0,8.9,28.4,43.5
2021H1,74.3,10.9,40.9,44.3
2021FY,105.4,10.0,29.9,85.5
2022H1,159.0,11.6,44.7,125.9
2022FY,151.8,14.3,46.3,119.8
2023H1,131.2,12.9,54.4,89.7
2023FY,130.9,14.7,51.2,94.4
2024H1,100.0,11.6,54.5,57.1
2024FY,101.0,11.0,52.9,59.1
2025H1,83.1,9.4,48.8,43.7
2025FY,121.6,6.8,36.1,92.3
2026H1,200.0,8.1,42.6,165.5
2020H1,102.0,9.6,27.8,83.8
2020FY,63.9,9.0,28.8,44.1
2021H1,74.7,10.9,41.1,44.6
2021FY,106.9,10.1,30.4,86.6
2022H1,159.9,11.7,44.9,126.6
2022FY,154.0,14.5,47.0,121.5
2023H1,131.9,13.0,54.7,90.2
2023FY,132.7,14.9,51.9,95.7
2024H1,100.6,11.6,54.8,57.4
2024FY,102.4,11.2,53.6,59.9
2025H1,83.5,9.5,49.1,43.9
2025FY,123.3,6.9,36.6,93.6
2026H1,201.2,8.2,42.9,166.5
1 period inventory_days receivable_days payable_days cash_conversion_cycle
2 2020H1 101.4 102.0 9.6 27.6 27.8 83.3 83.8
3 2020FY 63.0 63.9 8.9 9.0 28.4 28.8 43.5 44.1
4 2021H1 74.3 74.7 10.9 40.9 41.1 44.3 44.6
5 2021FY 105.4 106.9 10.0 10.1 29.9 30.4 85.5 86.6
6 2022H1 159.0 159.9 11.6 11.7 44.7 44.9 125.9 126.6
7 2022FY 151.8 154.0 14.3 14.5 46.3 47.0 119.8 121.5
8 2023H1 131.2 131.9 12.9 13.0 54.4 54.7 89.7 90.2
9 2023FY 130.9 132.7 14.7 14.9 51.2 51.9 94.4 95.7
10 2024H1 100.0 100.6 11.6 54.5 54.8 57.1 57.4
11 2024FY 101.0 102.4 11.0 11.2 52.9 53.6 59.1 59.9
12 2025H1 83.1 83.5 9.4 9.5 48.8 49.1 43.7 43.9
13 2025FY 121.6 123.3 6.8 6.9 36.1 36.6 92.3 93.6
14 2026H1 200.0 201.2 8.1 8.2 42.6 42.9 165.5 166.5
+8
View File
@@ -28,6 +28,10 @@ import { channelRevenueChart } from './ppmt_channel_revenue.js';
import { storesRetailChart } from './ppmt_stores_retail.js';
import { storesRobotChart } from './ppmt_stores_robot.js';
import { regionShareChart } from './ppmt_region_share.js';
import { inventoryBalanceChart } from './ppmt_inventory.js';
import { storesRetailShareChart } from './ppmt_stores_retail_share.js';
import { retailStoreRevenueChart } from './ppmt_retail_store_revenue.js';
import { robotStoreRevenueChart } from './ppmt_robot_store_revenue.js';
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
export const chartRegistry = {
@@ -61,6 +65,10 @@ export const chartRegistry = {
'ppmt-stores-retail': storesRetailChart,
'ppmt-stores-robot': storesRobotChart,
'ppmt-region-share': regionShareChart,
'ppmt-inventory': inventoryBalanceChart,
'ppmt-stores-retail-share': storesRetailShareChart,
'ppmt-retail-store-revenue': retailStoreRevenueChart,
'ppmt-robot-store-revenue': robotStoreRevenueChart,
};
export function getChartBuilder(key) {
+6 -2
View File
@@ -31,6 +31,10 @@ export function channelRevenueChart(rows, { width, height, view = 'all' }) {
const totalMax = Math.max(...rows.map((row) => num(row.total_revenue) || 0));
const axisMax = NICE_MAX.find((max) => max >= totalMax * 1.02)
?? Math.ceil((totalMax * 1.2) / 100) * 100;
const yoyValues = col(rows, 'revenue_yoy').filter(Number.isFinite);
// 全部/半年视图存在 2022H2 的负增长,右轴需包含负区间,避免折线被 0 轴裁切。
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoyValues) / 50) * 50);
// 绘图区像素高:画布高 − grid top 145 − grid bottom 90
const pxPerUnit = (height - LAYOUT.gridTop - 90) / axisMax;
@@ -111,8 +115,8 @@ export function channelRevenueChart(rows, { width, height, view = 'all' }) {
nameGap: 60,
nameRotate: -90,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 0,
max: 250,
min: yoyMin,
max: yoyMax,
interval: 50,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
splitLine: { show: false },
+82
View File
@@ -0,0 +1,82 @@
import { col } from '../csv.js';
import {
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
} from '../theme.js';
/** 泡泡玛特存货余额与同报告期同比:存货为期末时点数。 */
export function inventoryBalanceChart(rows, { width, height, view = 'all' }) {
const yoy = col(rows, 'yoy').filter(Number.isFinite);
const yoyMin = Math.min(0, Math.floor(Math.min(...yoy) / 20) * 20);
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoy) / 50) * 50);
const barWidth = view === 'all' ? 28.8 : 60.8;
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: `泡泡玛特存货余额及同比(${financialViewLabel(view)})`,
subtext: '柱状为报告期末存货余额;折线为同报告期同比(H1 对 H1、全年对全年)',
},
legend: standardLegend(['存货余额', '存货同比']),
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 90 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
textStyle: { fontFamily: FONT },
valueFormatter: (value) => `${value.toFixed(1)}`,
},
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
yAxis: [
{
...axisCommon,
type: 'value',
name: '存货余额(亿元)',
nameLocation: 'middle',
nameGap: 52,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 0,
},
{
...axisCommon,
type: 'value',
name: '同比(%)',
nameLocation: 'middle',
nameGap: 58,
nameRotate: -90,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: yoyMin,
max: yoyMax,
interval: (yoyMax - yoyMin) / 5,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
splitLine: { show: false },
},
],
series: [
{
...barSeries({
name: '存货余额',
data: col(rows, 'inventory'),
color: FINANCIAL_COLORS.investingCashFlow,
barWidth,
formatter: (point) => point.value.toFixed(1),
}),
emphasis: { focus: 'series' },
},
{
...lineSeries({
name: '存货同比',
data: col(rows, 'yoy'),
color: FINANCIAL_COLORS.cashConversion,
formatter: (point) => `${point.value.toFixed(1)}%`,
}),
yAxisIndex: 1,
z: 10,
},
],
graphic: [
...watermarks(width, height),
footnote('数据来源:泡泡玛特历年合并资产负债表;存货为报告期末时点数 | @思考的Joey'),
],
};
}
+110
View File
@@ -0,0 +1,110 @@
import { col } from '../csv.js';
import {
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
} from '../theme.js';
const REGIONS = [
{ key: 'china', name: '中国(含港澳台)', color: '#3F7FBE' },
{ key: 'apac', name: '亚太', color: '#36A28F' },
{ key: 'americas', name: '美洲', color: '#E4A349' },
{ key: 'europe_other', name: '欧洲及其他', color: PALETTE.brown },
];
function yoyByPeriod(rows, key) {
const values = new Map(rows.map((row) => [row.period, row[key]]));
return rows.map((row) => {
const year = Number(row.period.slice(0, 4));
const prior = values.get(`${year - 1}${row.period.slice(4)}`);
return Number.isFinite(row[key]) && Number.isFinite(prior) && prior !== 0
? (row[key] / prior - 1) * 100
: null;
});
}
/** 各地区零售店单店收入:收入 ÷ 期初、期末平均零售店数。 */
export function retailStoreRevenueChart(rows, { width, height, view = 'all' }) {
const values = REGIONS.flatMap((region) => col(rows, `${region.key}_per_store`)).filter(Number.isFinite);
const yMax = Math.ceil(Math.max(...values) / 1000) * 1000;
const yoySeries = REGIONS.map((region) => ({ ...region, data: yoyByPeriod(rows, `${region.key}_per_store`) }));
const yoyValues = yoySeries.flatMap((region) => region.data).filter(Number.isFinite);
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoyValues) / 50) * 50);
const legendItems = REGIONS.flatMap((region) => [region.name, `${region.name}同比`]);
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: `泡泡玛特各地区零售店单店收入(${financialViewLabel(view)})`,
subtext: '柱状为单店收入;同色虚线为单店收入同比;H2为下半年单期',
},
legend: standardLegend(legendItems, { itemGap: 18 }),
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
textStyle: { fontFamily: FONT },
formatter: (params) => `<b>${params[0].axisValue}</b><br/>${params
.filter((item) => Number.isFinite(item.value))
.map((item) => `${item.marker}${item.seriesName}: ${Number(item.value).toFixed(1)}${item.seriesName.endsWith('同比') ? '%' : ' 万元/店'}`)
.join('<br/>')}`,
},
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
yAxis: [
{
...axisCommon,
type: 'value',
name: '单店收入(万元/店)',
nameLocation: 'middle',
nameGap: 58,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 0,
max: yMax,
interval: yMax / 5,
},
{
...axisCommon,
type: 'value',
name: '单店收入同比(%)',
nameLocation: 'middle',
nameGap: 60,
nameRotate: -90,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: yoyMin,
max: yoyMax,
interval: 50,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
splitLine: { show: false },
},
],
series: [
...REGIONS.map((region) => ({
...barSeries({
name: region.name,
data: col(rows, `${region.key}_per_store`),
color: region.color,
barWidth: view === 'all' ? 17 : 34,
formatter: (point) => point.value.toFixed(0),
}),
emphasis: { focus: 'series' },
})),
...yoySeries.map((region) => ({
...lineSeries({
name: `${region.name}同比`,
data: region.data,
color: region.color,
width: 2,
formatter: (point) => `${point.value.toFixed(0)}%`,
}),
yAxisIndex: 1,
lineStyle: { color: region.color, width: 2, type: 'dashed' },
z: 10,
})),
],
graphic: [
...watermarks(width, height),
footnote('数据来源:泡泡玛特历年公告;收入仅含“零售店”渠道;H2收入=全年−H1\n2024年海外各区域缺少期初店数,相关单店收入留空 | @思考的Joey'),
],
};
}
+108
View File
@@ -0,0 +1,108 @@
import { col } from '../csv.js';
import {
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
} from '../theme.js';
const REGIONS = [
{ key: 'china', name: '中国(含港澳台)', color: '#71AD81' },
{ key: 'overseas', name: '海外', color: PALETTE.brown },
];
function yoyByPeriod(rows, key) {
const values = new Map(rows.map((row) => [row.period, row[key]]));
return rows.map((row) => {
const year = Number(row.period.slice(0, 4));
const prior = values.get(`${year - 1}${row.period.slice(4)}`);
return Number.isFinite(row[key]) && Number.isFinite(prior) && prior !== 0
? (row[key] / prior - 1) * 100
: null;
});
}
/** 中国与海外机器人商店单店收入:收入 ÷ 期初、期末平均机器人商店数。 */
export function robotStoreRevenueChart(rows, { width, height, view = 'all' }) {
const values = REGIONS.flatMap((region) => col(rows, `${region.key}_per_store`)).filter(Number.isFinite);
const yMax = Math.ceil(Math.max(...values) / 50) * 50;
const yoySeries = REGIONS.map((region) => ({ ...region, data: yoyByPeriod(rows, `${region.key}_per_store`) }));
const yoyValues = yoySeries.flatMap((region) => region.data).filter(Number.isFinite);
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoyValues) / 50) * 50);
const legendItems = REGIONS.flatMap((region) => [region.name, `${region.name}同比`]);
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: `泡泡玛特机器人商店单店收入(${financialViewLabel(view)})`,
subtext: '柱状为单店收入;同色虚线为单店收入同比;H2为下半年单期',
},
legend: standardLegend(legendItems, { itemGap: 18 }),
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
textStyle: { fontFamily: FONT },
formatter: (params) => `<b>${params[0].axisValue}</b><br/>${params
.filter((item) => Number.isFinite(item.value))
.map((item) => `${item.marker}${item.seriesName}: ${Number(item.value).toFixed(1)}${item.seriesName.endsWith('同比') ? '%' : ' 万元/店'}`)
.join('<br/>')}`,
},
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
yAxis: [
{
...axisCommon,
type: 'value',
name: '单店收入(万元/店)',
nameLocation: 'middle',
nameGap: 58,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 0,
max: yMax,
interval: yMax / 5,
},
{
...axisCommon,
type: 'value',
name: '单店收入同比(%)',
nameLocation: 'middle',
nameGap: 60,
nameRotate: -90,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: yoyMin,
max: yoyMax,
interval: 50,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
splitLine: { show: false },
},
],
series: [
...REGIONS.map((region) => ({
...barSeries({
name: region.name,
data: col(rows, `${region.key}_per_store`),
color: region.color,
barWidth: view === 'all' ? 28 : 52,
formatter: (point) => point.value.toFixed(0),
}),
emphasis: { focus: 'series' },
})),
...yoySeries.map((region) => ({
...lineSeries({
name: `${region.name}同比`,
data: region.data,
color: region.color,
width: 2,
formatter: (point) => `${point.value.toFixed(0)}%`,
}),
yAxisIndex: 1,
lineStyle: { color: region.color, width: 2, type: 'dashed' },
z: 10,
})),
],
graphic: [
...watermarks(width, height),
footnote('数据来源:泡泡玛特历年公告;海外收入=亚太+美洲+欧洲及其他的机器人商店收入;H2收入=全年−H1\n2024H1海外期末机器人商店数未披露,相关单店收入留空 | @思考的Joey'),
],
};
}
+83
View File
@@ -0,0 +1,83 @@
import { col } from '../csv.js';
import {
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
} from '../theme.js';
/** 泡泡玛特分地区零售门店占比:每根柱标准化为各期末全球门店数的 100%。 */
const REGIONS = [
{ key: 'china', name: '中国(含港澳台)', color: '#3F7FBE', labelColor: '#fff' },
{ key: 'apac', name: '亚太', color: '#36A28F', labelColor: '#fff' },
{ key: 'americas', name: '美洲', color: '#E4A349', labelColor: PALETTE.ink },
{ key: 'europe', name: '欧洲', color: PALETTE.brown, labelColor: '#fff' },
{ key: 'other', name: '其他', color: PALETTE.sand, labelColor: PALETTE.ink },
];
function sharePoint(value, labelColor) {
if (!Number.isFinite(value) || value === 0) return null;
return {
value,
label: value >= 5
? { position: 'inside', color: labelColor }
: { show: false },
};
}
export function storesRetailShareChart(rows, { width, height, view = 'all' }) {
const periods = col(rows, 'period');
const barWidth = view === 'all' ? 48 : 80;
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: `泡泡玛特分地区零售门店数量占比(${financialViewLabel(view)})`,
subtext: '归一化堆叠:每根柱为当期末全球零售门店数的 100%',
},
legend: standardLegend(REGIONS.map((item) => item.name), { itemGap: 20 }),
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
textStyle: { fontFamily: FONT },
formatter: (params) => {
const row = rows[periods.indexOf(params[0].axisValue)];
return `<b>${params[0].axisValue}</b> 全球合计 ${row.global} 家<br/>${params
.filter((item) => item.value !== null)
.slice()
.reverse()
.map((item) => `${item.marker}${item.seriesName}: ${item.value.toFixed(1)}%`)
.join('<br/>')}`;
},
},
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 100 },
xAxis: { ...standalonePeriodAxis, data: periods },
yAxis: {
...axisCommon,
type: 'value',
min: 0,
max: 100,
interval: 20,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
},
series: REGIONS.map((item) => barSeries({
name: item.name,
data: rows.map((row) => sharePoint(row[item.key], item.labelColor)),
color: item.color,
barWidth,
stack: 'stores-retail-share',
label: {
show: true,
position: 'inside',
fontSize: 11,
fontFamily: FONT,
fontWeight: 500,
formatter: (point) => `${point.value.toFixed(1)}%`,
},
emphasis: { focus: 'series' },
})),
graphic: [
...watermarks(width, height),
footnote('数据来源:泡泡玛特历年公告;门店数为各报告期末时点数\n中国=中国内地+港澳台;其他=全球合计−中国−亚太−美洲−欧洲 | @思考的Joey'),
],
};
}
+2 -2
View File
@@ -36,7 +36,7 @@ export function workingCapitalDaysChart(rows, { width, height }) {
title: {
...titleStyle,
text: '泡泡玛特营运资本效率',
subtext: '现金转换周期 = 存货周转天数 + 应收周转天数 − 应付周转天数',
subtext: '现金转换周期 = 存货周转天数 + 应收周转天数 − 应付周转天数(中报按181天、年报按365天)',
},
legend: { ...legendStyle, top: LAYOUT.legendTop, data: SERIES.map((item) => item.name) },
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
@@ -78,7 +78,7 @@ export function workingCapitalDaysChart(rows, { width, height }) {
})),
graphic: [
...watermarks(width, height),
footnote('数据来源:泡泡玛特财务指标(合并报表)| @思考的Joey'),
footnote('数据来源:泡泡玛特财务指标(合并报表);原始180/360天口径已换算为181/365天 | @思考的Joey'),
],
};
}
+7 -2
View File
@@ -12,6 +12,7 @@
"ppmt-bridge": { "key": "ppmt-bridge", "title": "净利率同比拆解", "csv": "data/ppmt/ppmt_margin_bridge.csv", "height": 700, "filename": "泡泡玛特净利率同比拆解" },
"ppmt-margins": { "key": "ppmt-margins", "title": "利润率与费用率拆解", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_margins.csv", "annual": "data/ppmt/ppmt_margins_annual.csv", "halfyear": "data/ppmt/ppmt_margins_halfyear.csv" }, "height": 680, "filename": "泡泡玛特利润率与费用率拆解" },
"ppmt-assets": { "key": "ppmt-assets", "title": "资产结构演变", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_assets.csv", "annual": "data/ppmt/ppmt_assets_annual.csv", "halfyear": "data/ppmt/ppmt_assets_halfyear.csv" }, "height": 680, "filename": "泡泡玛特资产结构" },
"ppmt-inventory": { "key": "ppmt-inventory", "title": "存货余额及同比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_inventory.csv", "annual": "data/ppmt/ppmt_inventory_annual.csv", "halfyear": "data/ppmt/ppmt_inventory_halfyear.csv" }, "height": 680, "filename": "泡泡玛特存货余额及同比" },
"ppmt-ip": { "key": "ppmt-ip", "title": "主要 IP 收入趋势", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip.csv", "annual": "data/ppmt/ppmt_ip_annual.csv", "halfyear": "data/ppmt/ppmt_ip_halfyear.csv" }, "height": 720, "filename": "泡泡玛特IP收入趋势" },
"ppmt-ip-share": { "key": "ppmt-ip-share", "title": "主要 IP 收入占比", "csv": "data/ppmt/ppmt_ip_share_2026h1.csv", "height": 720, "filename": "泡泡玛特IP收入占比2026H1" },
"ppmt-ip-share-history": { "key": "ppmt-ip-share-history", "title": "各 IP 半年收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_share_history.csv", "annual": "data/ppmt/ppmt_ip_share_history_annual.csv", "halfyear": "data/ppmt/ppmt_ip_share_history_halfyear.csv" }, "height": 700, "filename": "泡泡玛特各IP半年收入占比" },
@@ -27,16 +28,20 @@
"ppmt-expense-structure": { "key": "ppmt-expense-structure", "title": "费用结构与费用率", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_expense_structure.csv", "annual": "data/ppmt/ppmt_expense_structure_annual.csv", "halfyear": "data/ppmt/ppmt_expense_structure_halfyear.csv" }, "height": 700, "filename": "泡泡玛特费用结构与费用率" },
"ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_revenue.csv", "annual": "data/ppmt/ppmt_channel_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_channel_revenue_halfyear.csv" }, "height": 720, "filename": "泡泡玛特渠道收入结构与增速" },
"ppmt-stores-retail": { "key": "ppmt-stores-retail", "title": "分地区零售门店数量与同比增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_retail.csv", "annual": "data/ppmt/ppmt_stores_retail_annual.csv", "halfyear": "data/ppmt/ppmt_stores_retail_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区零售门店数量与同比增速" },
"ppmt-stores-retail-share": { "key": "ppmt-stores-retail-share", "title": "分地区零售门店数量占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_retail_share.csv", "annual": "data/ppmt/ppmt_stores_retail_share_annual.csv", "halfyear": "data/ppmt/ppmt_stores_retail_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区零售门店数量占比" },
"ppmt-retail-store-revenue": { "key": "ppmt-retail-store-revenue", "title": "各地区零售店单店收入", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_retail_store_revenue.csv", "annual": "data/ppmt/ppmt_retail_store_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_retail_store_revenue_halfyear.csv" }, "height": 680, "filename": "泡泡玛特各地区零售店单店收入" },
"ppmt-stores-robot": { "key": "ppmt-stores-robot", "title": "分地区机器人商店数量与同比增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_robot.csv", "annual": "data/ppmt/ppmt_stores_robot_annual.csv", "halfyear": "data/ppmt/ppmt_stores_robot_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区机器人商店数量与同比增速" },
"ppmt-robot-store-revenue": { "key": "ppmt-robot-store-revenue", "title": "机器人商店单店收入", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_robot_store_revenue.csv", "annual": "data/ppmt/ppmt_robot_store_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_robot_store_revenue_halfyear.csv" }, "height": 680, "filename": "泡泡玛特机器人商店单店收入" },
"ppmt-region-share": { "key": "ppmt-region-share", "title": "分地区营收占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_region_share.csv", "annual": "data/ppmt/ppmt_region_share_annual.csv", "halfyear": "data/ppmt/ppmt_region_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区营收占比" }
},
"tabs": [
{ "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share", "ppmt-region-channel"] },
{ "id": "profitability", "name": "盈利能力", "chartRefs": ["ppmt-growth", "ppmt-core-profit", "ppmt-core-margin", "ppmt-bridge", "ppmt-sankey", "ppmt-cash-conversion", "ppmt-cashflow-composition"] },
{ "id": "expense", "name": "费用率", "chartRefs": ["ppmt-margins", "ppmt-expense-structure"] },
{ "id": "balance-sheet", "name": "资产负债", "chartRefs": ["ppmt-assets", "ppmt-liquidity-working-capital", "ppmt-working-capital-days"] },
{ "id": "balance-sheet", "name": "资产负债", "chartRefs": ["ppmt-assets", "ppmt-inventory", "ppmt-liquidity-working-capital", "ppmt-working-capital-days"] },
{ "id": "ip-category", "name": "IP 与品类", "chartRefs": ["ppmt-ip", "ppmt-ip-share", "ppmt-ip-share-history", "ppmt-ip-ex-monsters", "ppmt-category"] },
{ "id": "region-channel", "name": "区域渠道", "chartRefs": ["ppmt-region-channel", "ppmt-region-yoy", "ppmt-region-yoy-timeseries", "ppmt-channel-revenue", "ppmt-stores-retail", "ppmt-stores-robot", "ppmt-region-share"] }
{ "id": "region-channel", "name": "区域渠道", "chartRefs": ["ppmt-region-channel", "ppmt-region-yoy", "ppmt-region-yoy-timeseries", "ppmt-channel-revenue", "ppmt-stores-retail", "ppmt-stores-retail-share", "ppmt-stores-robot", "ppmt-region-share"] },
{ "id": "store-revenue", "name": "单店收入", "chartRefs": ["ppmt-retail-store-revenue", "ppmt-robot-store-revenue"] }
]
},
{
+65 -11
View File
@@ -184,13 +184,21 @@ const workingCapitalDaysRows = indicators.dates.map((date, index) => ({
})).filter((item) => (
item.date >= '2020-01-01'
&& item.inventory !== null && item.receivable !== null && item.payable !== null
)).map((item) => [
periodFor(item.date),
item.inventory.toFixed(1),
item.receivable.toFixed(1),
item.payable.toFixed(1),
(item.inventory + item.receivable - item.payable).toFixed(1),
]).reverse();
)).map((item) => {
const period = periodFor(item.date);
// 原始指标表按半年 180 天、全年 360 天计算;此处统一换算为自然日 181/365 天。
const factor = period.endsWith('H1') ? 181 / 180 : 365 / 360;
const inventory = item.inventory * factor;
const receivable = item.receivable * factor;
const payable = item.payable * factor;
return [
period,
inventory.toFixed(1),
receivable.toFixed(1),
payable.toFixed(1),
(inventory + receivable - payable).toFixed(1),
];
}).reverse();
writeCSV('ppmt_working_capital_days.csv', [
'period', 'inventory_days', 'receivable_days', 'payable_days', 'cash_conversion_cycle',
@@ -341,9 +349,9 @@ function yoyFor(rows, key, outputKey = key) {
});
}
function toOutputRows(rows, headers, digits = {}) {
function toOutputRows(rows, headers, digits = {}, startYear = OUTPUT_START_YEAR) {
return rows
.filter((row) => Number(row.period.slice(0, 4)) >= OUTPUT_START_YEAR)
.filter((row) => Number(row.period.slice(0, 4)) >= startYear)
.map((row) => headers.map((header) => {
if (header === 'period') return row.period;
const value = row[header];
@@ -351,14 +359,14 @@ function toOutputRows(rows, headers, digits = {}) {
}));
}
function writeViewFiles(base, headers, views, digits) {
function writeViewFiles(base, headers, views, digits, startYear = OUTPUT_START_YEAR) {
const filenames = {
all: `${base}.csv`,
annual: `${base}_annual.csv`,
halfyear: `${base}_halfyear.csv`,
};
Object.entries(filenames).forEach(([view, filename]) => {
writeCSV(filename, headers, toOutputRows(views[view], headers, digits));
writeCSV(filename, headers, toOutputRows(views[view], headers, digits, startYear));
});
}
@@ -614,6 +622,31 @@ writeRuntimeViews('ppmt_category', ['period', 'plush', 'figure', 'mega', 'other'
plush: 2, figure: 2, mega: 2, other: 2,
});
// 2024H1 起门店披露可将中国内地与港澳台合并,并按全球总数归一化为地区占比。
const retailStoreRows = readRuntimeRows('ppmt_stores_retail.csv');
const retailStoreShareViews = { all: [], annual: [], halfyear: [] };
retailStoreRows.filter((row) => (
Number(row.period.slice(0, 4)) >= 2024
&& ['mainland', 'hkmtw', 'apac', 'americas', 'europe', 'global'].every((key) => Number.isFinite(row[key]))
)).forEach((row) => {
const china = row.mainland + row.hkmtw;
const other = row.global - china - row.apac - row.americas - row.europe;
const shareRow = {
period: row.period,
global: row.global,
china: china / row.global * 100,
apac: row.apac / row.global * 100,
americas: row.americas / row.global * 100,
europe: row.europe / row.global * 100,
other: other / row.global * 100,
};
retailStoreShareViews.all.push(shareRow);
retailStoreShareViews[row.period.endsWith('FY') ? 'annual' : 'halfyear'].push(shareRow);
});
writeRuntimeViews('ppmt_stores_retail_share', ['period', 'global', 'china', 'apac', 'americas', 'europe', 'other'], retailStoreShareViews, {
global: 0, china: 1, apac: 1, americas: 1, europe: 1, other: 1,
});
// 资产负债表为时点指标:H2 是年末时点,而非“全年累计”。
function buildSnapshotViews(base, headers, digits) {
const source = readRuntimeRows(`${base}.csv`);
@@ -643,4 +676,25 @@ buildSnapshotViews('ppmt_liquidity_working_capital', ['period', 'cash', 'total_l
cash: 2, total_liabilities: 2, net_working_capital: 2,
});
// 存货为资产负债表期末时点数:半年仅取中报期末,年度仅取年报期末。
// 同比严格按相同报告期(H1 对 H1、FY 对 FY)计算,不把中报与年报交叉比较。
const inventoryBalances = row(balance, '存货');
const inventoryViews = { all: [], annual: [], halfyear: [] };
balance.dates.map((date, index) => ({
date,
inventory: inventoryBalances[index],
})).filter(({ date, inventory }) => (
date >= '2017-01-01' && inventory > 0 && (date.endsWith('-06-30') || date.endsWith('-12-31'))
)).sort((a, b) => a.date.localeCompare(b.date)).forEach(({ date, inventory }) => {
const row = { period: periodFor(date), inventory: inventory / HUNDRED_MILLION };
inventoryViews.all.push(row);
inventoryViews[row.period.endsWith('FY') ? 'annual' : 'halfyear'].push(row);
});
Object.keys(inventoryViews).forEach((view) => {
inventoryViews[view] = yoyFor(inventoryViews[view], 'inventory', 'yoy');
});
writeViewFiles('ppmt_inventory', ['period', 'inventory', 'yoy'], inventoryViews, {
inventory: 2, yoy: 1,
}, 2017);
console.log('已生成第二批连续图表的全部、年度、半年 CSV。');