Compare commits
3 Commits
d093a322d3
...
44c6437b64
| Author | SHA1 | Date | |
|---|---|---|---|
| 44c6437b64 | |||
| 9605228c3f | |||
| b3c7c30a1f |
@@ -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,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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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,14 +1,14 @@
|
|||||||
period,inventory_days,receivable_days,payable_days,cash_conversion_cycle
|
period,inventory_days,receivable_days,payable_days,cash_conversion_cycle
|
||||||
2020H1,101.4,9.6,27.6,83.3
|
2020H1,102.0,9.6,27.8,83.8
|
||||||
2020FY,63.0,8.9,28.4,43.5
|
2020FY,63.9,9.0,28.8,44.1
|
||||||
2021H1,74.3,10.9,40.9,44.3
|
2021H1,74.7,10.9,41.1,44.6
|
||||||
2021FY,105.4,10.0,29.9,85.5
|
2021FY,106.9,10.1,30.4,86.6
|
||||||
2022H1,159.0,11.6,44.7,125.9
|
2022H1,159.9,11.7,44.9,126.6
|
||||||
2022FY,151.8,14.3,46.3,119.8
|
2022FY,154.0,14.5,47.0,121.5
|
||||||
2023H1,131.2,12.9,54.4,89.7
|
2023H1,131.9,13.0,54.7,90.2
|
||||||
2023FY,130.9,14.7,51.2,94.4
|
2023FY,132.7,14.9,51.9,95.7
|
||||||
2024H1,100.0,11.6,54.5,57.1
|
2024H1,100.6,11.6,54.8,57.4
|
||||||
2024FY,101.0,11.0,52.9,59.1
|
2024FY,102.4,11.2,53.6,59.9
|
||||||
2025H1,83.1,9.4,48.8,43.7
|
2025H1,83.5,9.5,49.1,43.9
|
||||||
2025FY,121.6,6.8,36.1,92.3
|
2025FY,123.3,6.9,36.6,93.6
|
||||||
2026H1,200.0,8.1,42.6,165.5
|
2026H1,201.2,8.2,42.9,166.5
|
||||||
|
|||||||
|
@@ -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,9 +25,14 @@ import { cashflowCompositionChart } from './ppmt_cashflow_composition.js';
|
|||||||
import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
||||||
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
||||||
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
||||||
|
import { channelShareChart } from './ppmt_channel_share.js';
|
||||||
import { storesRetailChart } from './ppmt_stores_retail.js';
|
import { storesRetailChart } from './ppmt_stores_retail.js';
|
||||||
import { storesRobotChart } from './ppmt_stores_robot.js';
|
import { storesRobotChart } from './ppmt_stores_robot.js';
|
||||||
import { regionShareChart } from './ppmt_region_share.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 函数。 */
|
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
||||||
export const chartRegistry = {
|
export const chartRegistry = {
|
||||||
@@ -58,9 +63,14 @@ export const chartRegistry = {
|
|||||||
'ppmt-working-capital-days': workingCapitalDaysChart,
|
'ppmt-working-capital-days': workingCapitalDaysChart,
|
||||||
'ppmt-expense-structure': expenseStructureChart,
|
'ppmt-expense-structure': expenseStructureChart,
|
||||||
'ppmt-channel-revenue': channelRevenueChart,
|
'ppmt-channel-revenue': channelRevenueChart,
|
||||||
|
'ppmt-channel-share': channelShareChart,
|
||||||
'ppmt-stores-retail': storesRetailChart,
|
'ppmt-stores-retail': storesRetailChart,
|
||||||
'ppmt-stores-robot': storesRobotChart,
|
'ppmt-stores-robot': storesRobotChart,
|
||||||
'ppmt-region-share': regionShareChart,
|
'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) {
|
export function getChartBuilder(key) {
|
||||||
|
|||||||
@@ -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 totalMax = Math.max(...rows.map((row) => num(row.total_revenue) || 0));
|
||||||
const axisMax = NICE_MAX.find((max) => max >= totalMax * 1.02)
|
const axisMax = NICE_MAX.find((max) => max >= totalMax * 1.02)
|
||||||
?? Math.ceil((totalMax * 1.2) / 100) * 100;
|
?? 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
|
// 绘图区像素高:画布高 − grid top 145 − grid bottom 90
|
||||||
const pxPerUnit = (height - LAYOUT.gridTop - 90) / axisMax;
|
const pxPerUnit = (height - LAYOUT.gridTop - 90) / axisMax;
|
||||||
|
|
||||||
@@ -111,8 +115,8 @@ export function channelRevenueChart(rows, { width, height, view = 'all' }) {
|
|||||||
nameGap: 60,
|
nameGap: 60,
|
||||||
nameRotate: -90,
|
nameRotate: -90,
|
||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
min: 0,
|
min: yoyMin,
|
||||||
max: 250,
|
max: yoyMax,
|
||||||
interval: 50,
|
interval: 50,
|
||||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ export function workingCapitalDaysChart(rows, { width, height }) {
|
|||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特营运资本效率',
|
text: '泡泡玛特营运资本效率',
|
||||||
subtext: '现金转换周期 = 存货周转天数 + 应收周转天数 − 应付周转天数',
|
subtext: '现金转换周期 = 存货周转天数 + 应收周转天数 − 应付周转天数(中报按181天、年报按365天)',
|
||||||
},
|
},
|
||||||
legend: { ...legendStyle, top: LAYOUT.legendTop, data: SERIES.map((item) => item.name) },
|
legend: { ...legendStyle, top: LAYOUT.legendTop, data: SERIES.map((item) => item.name) },
|
||||||
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
@@ -78,7 +78,7 @@ export function workingCapitalDaysChart(rows, { width, height }) {
|
|||||||
})),
|
})),
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特财务指标(合并报表)| @思考的Joey'),
|
footnote('数据来源:泡泡玛特财务指标(合并报表);原始180/360天口径已换算为181/365天 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -12,6 +12,7 @@
|
|||||||
"ppmt-bridge": { "key": "ppmt-bridge", "title": "净利率同比拆解", "csv": "data/ppmt/ppmt_margin_bridge.csv", "height": 700, "filename": "泡泡玛特净利率同比拆解" },
|
"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-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-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": { "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": { "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半年收入占比" },
|
"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半年收入占比" },
|
||||||
@@ -26,17 +27,22 @@
|
|||||||
"ppmt-working-capital-days": { "key": "ppmt-working-capital-days", "title": "营运资本效率", "csv": "data/ppmt/ppmt_working_capital_days.csv", "height": 700, "filename": "泡泡玛特营运资本效率" },
|
"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-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-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": { "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-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": "泡泡玛特分地区营收占比" }
|
"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": [
|
"tabs": [
|
||||||
{ "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share", "ppmt-region-channel"] },
|
{ "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": "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": "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": "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-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 ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const RAW_DIR = path.join(ROOT, 'data/ppmt/raw');
|
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 OUTPUT_DIR = path.join(ROOT, 'data/ppmt');
|
||||||
|
|
||||||
const FILES = {
|
const FILES = {
|
||||||
@@ -184,13 +185,21 @@ const workingCapitalDaysRows = indicators.dates.map((date, index) => ({
|
|||||||
})).filter((item) => (
|
})).filter((item) => (
|
||||||
item.date >= '2020-01-01'
|
item.date >= '2020-01-01'
|
||||||
&& item.inventory !== null && item.receivable !== null && item.payable !== null
|
&& item.inventory !== null && item.receivable !== null && item.payable !== null
|
||||||
)).map((item) => [
|
)).map((item) => {
|
||||||
periodFor(item.date),
|
const period = periodFor(item.date);
|
||||||
item.inventory.toFixed(1),
|
// 原始指标表按半年 180 天、全年 360 天计算;此处统一换算为自然日 181/365 天。
|
||||||
item.receivable.toFixed(1),
|
const factor = period.endsWith('H1') ? 181 / 180 : 365 / 360;
|
||||||
item.payable.toFixed(1),
|
const inventory = item.inventory * factor;
|
||||||
(item.inventory + item.receivable - item.payable).toFixed(1),
|
const receivable = item.receivable * factor;
|
||||||
]).reverse();
|
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', [
|
writeCSV('ppmt_working_capital_days.csv', [
|
||||||
'period', 'inventory_days', 'receivable_days', 'payable_days', 'cash_conversion_cycle',
|
'period', 'inventory_days', 'receivable_days', 'payable_days', 'cash_conversion_cycle',
|
||||||
@@ -341,9 +350,9 @@ function yoyFor(rows, key, outputKey = key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOutputRows(rows, headers, digits = {}) {
|
function toOutputRows(rows, headers, digits = {}, startYear = OUTPUT_START_YEAR) {
|
||||||
return rows
|
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) => {
|
.map((row) => headers.map((header) => {
|
||||||
if (header === 'period') return row.period;
|
if (header === 'period') return row.period;
|
||||||
const value = row[header];
|
const value = row[header];
|
||||||
@@ -351,14 +360,14 @@ function toOutputRows(rows, headers, digits = {}) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeViewFiles(base, headers, views, digits) {
|
function writeViewFiles(base, headers, views, digits, startYear = OUTPUT_START_YEAR) {
|
||||||
const filenames = {
|
const filenames = {
|
||||||
all: `${base}.csv`,
|
all: `${base}.csv`,
|
||||||
annual: `${base}_annual.csv`,
|
annual: `${base}_annual.csv`,
|
||||||
halfyear: `${base}_halfyear.csv`,
|
halfyear: `${base}_halfyear.csv`,
|
||||||
};
|
};
|
||||||
Object.entries(filenames).forEach(([view, filename]) => {
|
Object.entries(filenames).forEach(([view, filename]) => {
|
||||||
writeCSV(filename, headers, toOutputRows(views[view], headers, digits));
|
writeCSV(filename, headers, toOutputRows(views[view], headers, digits, startYear));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,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,
|
sales_expense: 2, admin_expense: 2, financial_expense: 2, four_expense_rate: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
function readRuntimeRows(filename) {
|
function readCsvRows(dir, filename) {
|
||||||
const [header, ...lines] = fs.readFileSync(path.join(OUTPUT_DIR, filename), 'utf8').trim().split(/\r?\n/);
|
const [header, ...lines] = fs.readFileSync(path.join(dir, filename), 'utf8').trim().split(/\r?\n/);
|
||||||
const headers = header.split(',');
|
const headers = header.split(',');
|
||||||
return lines.map((line) => Object.fromEntries(line.split(',').map((cell, index) => {
|
return lines.map((line) => Object.fromEntries(line.split(',').map((cell, index) => {
|
||||||
const value = Number(cell);
|
const value = Number(cell);
|
||||||
@@ -566,6 +575,10 @@ function readRuntimeRows(filename) {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readRuntimeRows(filename) {
|
||||||
|
return readCsvRows(OUTPUT_DIR, filename);
|
||||||
|
}
|
||||||
|
|
||||||
function writeRuntimeViews(base, headers, views, digits = {}) {
|
function writeRuntimeViews(base, headers, views, digits = {}) {
|
||||||
writeViewFiles(base, headers, views, digits);
|
writeViewFiles(base, headers, views, digits);
|
||||||
}
|
}
|
||||||
@@ -614,6 +627,31 @@ writeRuntimeViews('ppmt_category', ['period', 'plush', 'figure', 'mega', 'other'
|
|||||||
plush: 2, figure: 2, mega: 2, other: 2,
|
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 是年末时点,而非“全年累计”。
|
// 资产负债表为时点指标:H2 是年末时点,而非“全年累计”。
|
||||||
function buildSnapshotViews(base, headers, digits) {
|
function buildSnapshotViews(base, headers, digits) {
|
||||||
const source = readRuntimeRows(`${base}.csv`);
|
const source = readRuntimeRows(`${base}.csv`);
|
||||||
@@ -643,4 +681,67 @@ buildSnapshotViews('ppmt_liquidity_working_capital', ['period', 'cash', 'total_l
|
|||||||
cash: 2, total_liabilities: 2, net_working_capital: 2,
|
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。');
|
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