diff --git a/data/ppmt/ppmt_channel_share.csv b/data/ppmt/ppmt_channel_share.csv
new file mode 100644
index 0000000..2b29bed
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share.csv
@@ -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
diff --git a/data/ppmt/ppmt_channel_share_annual.csv b/data/ppmt/ppmt_channel_share_annual.csv
new file mode 100644
index 0000000..ce460d6
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_annual.csv
@@ -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
diff --git a/data/ppmt/ppmt_channel_share_halfyear.csv b/data/ppmt/ppmt_channel_share_halfyear.csv
new file mode 100644
index 0000000..153d0b2
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_halfyear.csv
@@ -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
diff --git a/data/ppmt/rawData/地区渠道收入.csv b/data/ppmt/rawData/地区渠道收入.csv
new file mode 100644
index 0000000..7118114
--- /dev/null
+++ b/data/ppmt/rawData/地区渠道收入.csv
@@ -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
diff --git a/data/ppmt/rawData/泡泡玛特-rawdata.xlsx b/data/ppmt/rawData/泡泡玛特-rawdata.xlsx
new file mode 100644
index 0000000..ee2c6e9
Binary files /dev/null and b/data/ppmt/rawData/泡泡玛特-rawdata.xlsx differ
diff --git a/js/charts/index.js b/js/charts/index.js
index a19f66a..c7f7134 100644
--- a/js/charts/index.js
+++ b/js/charts/index.js
@@ -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,
diff --git a/js/charts/ppmt_channel_share.js b/js/charts/ppmt_channel_share.js
new file mode 100644
index 0000000..cea360f
--- /dev/null
+++ b/js/charts/ppmt_channel_share.js
@@ -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 `${params[0].axisValue} 总收入 ${row.total_revenue.toFixed(2)} 亿元
${params
+ .filter((item) => item.value !== null)
+ .slice()
+ .reverse()
+ .map((item) => `${item.marker}${item.seriesName}: ${item.value.toFixed(1)}%`)
+ .join('
')}`;
+ },
+ },
+ 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'),
+ ],
+ };
+}
diff --git a/manifest.json b/manifest.json
index ffc2f9b..0b10ebd 100644
--- a/manifest.json
+++ b/manifest.json
@@ -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"] }
]
},
diff --git a/scripts/normalize_ppmt_financials.mjs b/scripts/normalize_ppmt_financials.mjs
index 174507a..2cbf922 100644
--- a/scripts/normalize_ppmt_financials.mjs
+++ b/scripts/normalize_ppmt_financials.mjs
@@ -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。');