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
+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。');