feat: 新增各渠道收入占比
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
period,retail,online,robot_store,wholesale,total_revenue
|
||||
2024H1,57.4,25.9,8.1,8.6,45.59
|
||||
2024H2,49.0,35.2,5.4,10.4,84.78
|
||||
2024FY,51.9,31.9,6.4,9.8,130.37
|
||||
2025H1,49.8,39.6,5.9,4.7,138.75
|
||||
2025H2,44.5,47.0,3.7,4.7,232.45
|
||||
2025FY,46.5,44.3,4.6,4.7,371.20
|
||||
2026H1,53.1,36.2,5.5,5.2,171.74
|
||||
|
@@ -0,0 +1,3 @@
|
||||
period,retail,online,robot_store,wholesale,total_revenue
|
||||
2024FY,51.9,31.9,6.4,9.8,130.37
|
||||
2025FY,46.5,44.3,4.6,4.7,371.20
|
||||
|
@@ -0,0 +1,6 @@
|
||||
period,retail,online,robot_store,wholesale,total_revenue
|
||||
2024H1,57.4,25.9,8.1,8.6,45.59
|
||||
2024H2,49.0,35.2,5.4,10.4,84.78
|
||||
2025H1,49.8,39.6,5.9,4.7,138.75
|
||||
2025H2,44.5,47.0,3.7,4.7,232.45
|
||||
2026H1,53.1,36.2,5.5,5.2,171.74
|
||||
|
@@ -0,0 +1,81 @@
|
||||
region,channel,period,revenue
|
||||
中国,零售店,2024H1,20.04
|
||||
中国,零售店,2024FY,46.01
|
||||
中国,零售店,2025H1,44.06
|
||||
中国,零售店,2025FY,100.75
|
||||
中国,零售店,2026H1,60.90
|
||||
中国,机器人商店,2024H1,3.38
|
||||
中国,机器人商店,2024FY,7.51
|
||||
中国,机器人商店,2025H1,6.78
|
||||
中国,机器人商店,2025FY,13.46
|
||||
中国,机器人商店,2026H1,7.79
|
||||
中国,线上渠道,2024H1,9.41
|
||||
中国,线上渠道,2024FY,27.72
|
||||
中国,线上渠道,2025H1,29.37
|
||||
中国,线上渠道,2025FY,85.22
|
||||
中国,线上渠道,2026H1,47.79
|
||||
中国,批发及其他,2024H1,2.39
|
||||
中国,批发及其他,2024FY,7.63
|
||||
中国,批发及其他,2025H1,2.61
|
||||
中国,批发及其他,2025FY,9.08
|
||||
中国,批发及其他,2026H1,5.53
|
||||
亚太,零售店,2024H1,4.85
|
||||
亚太,零售店,2024FY,16.68
|
||||
亚太,零售店,2025H1,14.93
|
||||
亚太,零售店,2025FY,43.19
|
||||
亚太,零售店,2026H1,17.16
|
||||
亚太,机器人商店,2024H1,0.17
|
||||
亚太,机器人商店,2024FY,0.40
|
||||
亚太,机器人商店,2025H1,0.33
|
||||
亚太,机器人商店,2025FY,0.95
|
||||
亚太,机器人商店,2026H1,0.57
|
||||
亚太,线上渠道,2024H1,1.65
|
||||
亚太,线上渠道,2024FY,9.72
|
||||
亚太,线上渠道,2025H1,10.69
|
||||
亚太,线上渠道,2025FY,30.53
|
||||
亚太,线上渠道,2026H1,6.44
|
||||
亚太,批发及其他,2024H1,1.29
|
||||
亚太,批发及其他,2024FY,4.30
|
||||
亚太,批发及其他,2025H1,2.56
|
||||
亚太,批发及其他,2025FY,5.44
|
||||
亚太,批发及其他,2026H1,1.59
|
||||
美洲,零售店,2024H1,0.87
|
||||
美洲,零售店,2024FY,3.41
|
||||
美洲,零售店,2025H1,7.37
|
||||
美洲,零售店,2025FY,20.04
|
||||
美洲,零售店,2026H1,9.03
|
||||
美洲,机器人商店,2024H1,0.13
|
||||
美洲,机器人商店,2024FY,0.38
|
||||
美洲,机器人商店,2025H1,1.05
|
||||
美洲,机器人商店,2025FY,2.32
|
||||
美洲,机器人商店,2026H1,1.04
|
||||
美洲,线上渠道,2024H1,0.64
|
||||
美洲,线上渠道,2024FY,3.64
|
||||
美洲,线上渠道,2025H1,13.27
|
||||
美洲,线上渠道,2025FY,43.54
|
||||
美洲,线上渠道,2026H1,7.22
|
||||
美洲,批发及其他,2024H1,0.19
|
||||
美洲,批发及其他,2024FY,0.58
|
||||
美洲,批发及其他,2025H1,0.96
|
||||
美洲,批发及其他,2025FY,2.17
|
||||
美洲,批发及其他,2026H1,1.62
|
||||
欧洲及其他,零售店,2024H1,0.41
|
||||
欧洲及其他,零售店,2024FY,1.57
|
||||
欧洲及其他,零售店,2025H1,2.72
|
||||
欧洲及其他,零售店,2025FY,8.56
|
||||
欧洲及其他,零售店,2026H1,4.03
|
||||
欧洲及其他,机器人商店,2024H1,0.01
|
||||
欧洲及其他,机器人商店,2024FY,0.02
|
||||
欧洲及其他,机器人商店,2025H1,0.04
|
||||
欧洲及其他,机器人商店,2025FY,0.17
|
||||
欧洲及其他,机器人商店,2026H1,0.11
|
||||
欧洲及其他,线上渠道,2024H1,0.11
|
||||
欧洲及其他,线上渠道,2024FY,0.55
|
||||
欧洲及其他,线上渠道,2025H1,1.65
|
||||
欧洲及其他,线上渠道,2025FY,5.05
|
||||
欧洲及其他,线上渠道,2026H1,0.68
|
||||
欧洲及其他,批发及其他,2024H1,0.05
|
||||
欧洲及其他,批发及其他,2024FY,0.25
|
||||
欧洲及其他,批发及其他,2025H1,0.36
|
||||
欧洲及其他,批发及其他,2025FY,0.73
|
||||
欧洲及其他,批发及其他,2026H1,0.24
|
||||
|
Binary file not shown.
@@ -25,6 +25,7 @@ import { cashflowCompositionChart } from './ppmt_cashflow_composition.js';
|
||||
import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
||||
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
||||
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
||||
import { channelShareChart } from './ppmt_channel_share.js';
|
||||
import { storesRetailChart } from './ppmt_stores_retail.js';
|
||||
import { storesRobotChart } from './ppmt_stores_robot.js';
|
||||
import { regionShareChart } from './ppmt_region_share.js';
|
||||
@@ -62,6 +63,7 @@ export const chartRegistry = {
|
||||
'ppmt-working-capital-days': workingCapitalDaysChart,
|
||||
'ppmt-expense-structure': expenseStructureChart,
|
||||
'ppmt-channel-revenue': channelRevenueChart,
|
||||
'ppmt-channel-share': channelShareChart,
|
||||
'ppmt-stores-retail': storesRetailChart,
|
||||
'ppmt-stores-robot': storesRobotChart,
|
||||
'ppmt-region-share': regionShareChart,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||
financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
const CHANNELS = [
|
||||
{ key: 'retail', name: '零售店', color: CHANNEL_COLORS.retail, labelColor: '#fff' },
|
||||
{ key: 'online', name: '线上渠道', color: CHANNEL_COLORS.online, labelColor: '#fff' },
|
||||
{ key: 'robot_store', name: '机器人商店', color: CHANNEL_COLORS.robotStore, labelColor: '#fff' },
|
||||
{ key: 'wholesale', name: '批发及其他', color: CHANNEL_COLORS.wholesale, labelColor: PALETTE.ink },
|
||||
];
|
||||
|
||||
function sharePoint(value, labelColor) {
|
||||
if (!Number.isFinite(value) || value === 0) return null;
|
||||
// 3.5% 以下的段放不下 11px 段内标签;当前数据最小为 3.7%(2025H2 机器人商店)
|
||||
return {
|
||||
value,
|
||||
label: value >= 3.5
|
||||
? { position: 'inside', color: labelColor }
|
||||
: { show: false },
|
||||
};
|
||||
}
|
||||
|
||||
/** 泡泡玛特全渠道收入占比:全部地区合并,每根柱按当期总收入归一化为 100%。 */
|
||||
export function channelShareChart(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(CHANNELS.map((item) => item.name)),
|
||||
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.total_revenue.toFixed(2)} 亿元<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: CHANNELS.map((item) => barSeries({
|
||||
name: item.name,
|
||||
data: rows.map((row) => sharePoint(row[item.key], item.labelColor)),
|
||||
color: item.color,
|
||||
barWidth,
|
||||
stack: 'channel-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('数据来源:泡泡玛特分地区渠道收入披露(中国含港澳台);H2为单半年(全年−H1)\n线上渠道含抽盒机、天猫、抖音、自研APP及官网等 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
+2
-1
@@ -27,6 +27,7 @@
|
||||
"ppmt-working-capital-days": { "key": "ppmt-working-capital-days", "title": "营运资本效率", "csv": "data/ppmt/ppmt_working_capital_days.csv", "height": 700, "filename": "泡泡玛特营运资本效率" },
|
||||
"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-channel-share": { "key": "ppmt-channel-share", "title": "渠道收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_share.csv", "annual": "data/ppmt/ppmt_channel_share_annual.csv", "halfyear": "data/ppmt/ppmt_channel_share_halfyear.csv" }, "height": 680, "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": "泡泡玛特各地区零售店单店收入" },
|
||||
@@ -40,7 +41,7 @@
|
||||
{ "id": "expense", "name": "费用率", "chartRefs": ["ppmt-margins", "ppmt-expense-structure"] },
|
||||
{ "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-retail-share", "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-channel-share", "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"] }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const RAW_DIR = path.join(ROOT, 'data/ppmt/raw');
|
||||
const RAWDATA_DIR = path.join(ROOT, 'data/ppmt/rawData');
|
||||
const OUTPUT_DIR = path.join(ROOT, 'data/ppmt');
|
||||
|
||||
const FILES = {
|
||||
@@ -565,8 +566,8 @@ writeViewFiles('ppmt_expense_structure', ['period', 'sales_expense', 'admin_expe
|
||||
sales_expense: 2, admin_expense: 2, financial_expense: 2, four_expense_rate: 1,
|
||||
});
|
||||
|
||||
function readRuntimeRows(filename) {
|
||||
const [header, ...lines] = fs.readFileSync(path.join(OUTPUT_DIR, filename), 'utf8').trim().split(/\r?\n/);
|
||||
function readCsvRows(dir, filename) {
|
||||
const [header, ...lines] = fs.readFileSync(path.join(dir, filename), 'utf8').trim().split(/\r?\n/);
|
||||
const headers = header.split(',');
|
||||
return lines.map((line) => Object.fromEntries(line.split(',').map((cell, index) => {
|
||||
const value = Number(cell);
|
||||
@@ -574,6 +575,10 @@ function readRuntimeRows(filename) {
|
||||
})));
|
||||
}
|
||||
|
||||
function readRuntimeRows(filename) {
|
||||
return readCsvRows(OUTPUT_DIR, filename);
|
||||
}
|
||||
|
||||
function writeRuntimeViews(base, headers, views, digits = {}) {
|
||||
writeViewFiles(base, headers, views, digits);
|
||||
}
|
||||
@@ -698,3 +703,45 @@ writeViewFiles('ppmt_inventory', ['period', 'inventory', 'yoy'], inventoryViews,
|
||||
}, 2017);
|
||||
|
||||
console.log('已生成第二批连续图表的全部、年度、半年 CSV。');
|
||||
|
||||
// 渠道收入占比:底表 data/ppmt/rawData/地区渠道收入.csv 整理自
|
||||
// 泡泡玛特-rawdata.xlsx「各地区渠道收入」工作表(中国含港澳台的四地区 × 四渠道明细)。
|
||||
// 全球合计 = 四地区加总;披露口径只有 H1 与全年,H2 单半年 = 全年 − H1;
|
||||
// 占比在金额视图之后计算,按当期四渠道合计归一化,不可用占比直接相减。
|
||||
const CHANNEL_NAME_TO_KEY = {
|
||||
零售店: 'retail',
|
||||
机器人商店: 'robot_store',
|
||||
线上渠道: 'online',
|
||||
批发及其他: 'wholesale',
|
||||
};
|
||||
const channelRevenueByPeriod = new Map();
|
||||
readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
|
||||
const key = CHANNEL_NAME_TO_KEY[channel];
|
||||
if (!key) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
|
||||
if (!period || !Number.isFinite(revenue)) throw new Error(`地区渠道收入底表行不完整:${region} ${channel} ${period} ${revenue}`);
|
||||
if (!channelRevenueByPeriod.has(period)) {
|
||||
channelRevenueByPeriod.set(period, { retail: 0, online: 0, robot_store: 0, wholesale: 0 });
|
||||
}
|
||||
channelRevenueByPeriod.get(period)[key] += revenue;
|
||||
});
|
||||
|
||||
const channelAmountViews = buildViews((year, type) => {
|
||||
const bucket = channelRevenueByPeriod.get(type === 'H1' ? `${year}H1` : `${year}FY`);
|
||||
return bucket ?? null;
|
||||
});
|
||||
const channelShareViews = Object.fromEntries(Object.entries(channelAmountViews).map(([view, rows]) => [
|
||||
view,
|
||||
rows.map(({ period, ...amounts }) => {
|
||||
const total = Object.values(amounts).reduce((sum, value) => sum + value, 0);
|
||||
return {
|
||||
period,
|
||||
...Object.fromEntries(Object.entries(amounts).map(([key, value]) => [key, value / total * 100])),
|
||||
total_revenue: total,
|
||||
};
|
||||
}),
|
||||
]));
|
||||
writeRuntimeViews('ppmt_channel_share', ['period', 'retail', 'online', 'robot_store', 'wholesale', 'total_revenue'], channelShareViews, {
|
||||
retail: 1, online: 1, robot_store: 1, wholesale: 1, total_revenue: 2,
|
||||
});
|
||||
|
||||
console.log('已生成渠道收入占比的全部、年度、半年 CSV。');
|
||||
|
||||
Reference in New Issue
Block a user