From 44c6437b64d00794719e39ae7e851e9653e3038a Mon Sep 17 00:00:00 2001 From: R524809 Date: Mon, 7 Sep 2026 17:42:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=90=84=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E6=94=B6=E5=85=A5=E5=8D=A0=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/ppmt/ppmt_channel_share.csv | 8 +++ data/ppmt/ppmt_channel_share_annual.csv | 3 + data/ppmt/ppmt_channel_share_halfyear.csv | 6 ++ data/ppmt/rawData/地区渠道收入.csv | 81 +++++++++++++++++++++ data/ppmt/rawData/泡泡玛特-rawdata.xlsx | Bin 0 -> 10186 bytes js/charts/index.js | 2 + js/charts/ppmt_channel_share.js | 83 ++++++++++++++++++++++ manifest.json | 3 +- scripts/normalize_ppmt_financials.mjs | 51 ++++++++++++- 9 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 data/ppmt/ppmt_channel_share.csv create mode 100644 data/ppmt/ppmt_channel_share_annual.csv create mode 100644 data/ppmt/ppmt_channel_share_halfyear.csv create mode 100644 data/ppmt/rawData/地区渠道收入.csv create mode 100644 data/ppmt/rawData/泡泡玛特-rawdata.xlsx create mode 100644 js/charts/ppmt_channel_share.js 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 0000000000000000000000000000000000000000..ee2c6e9d909cbf4c2df50266977e64bebdcd41f0 GIT binary patch literal 10186 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 literal 0 HcmV?d00001 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。');