5 Commits

Author SHA1 Message Date
R524809 44c6437b64 feat: 新增各渠道收入占比 2026-09-07 17:42:45 +08:00
R524809 9605228c3f Merge branch 'main' into feat-1.1.0 2026-09-07 14:54:38 +08:00
R524809 d093a322d3 docs: 更新方案文档 2026-09-04 17:33:06 +08:00
R524809 04105ad6eb feat:更新rawData 2026-09-04 16:47:14 +08:00
R524809 143cd1fbf8 docs: add maintainable PPMT financial workbook 2026-08-31 17:49:00 +08:00
16 changed files with 440 additions and 7 deletions
+4
View File
@@ -45,6 +45,10 @@ pnpm test # 全量,只在改了 t
## 数据口径
**底稿与图表数据分层。** `data/{company}/rawData/` 是唯一可维护的数据层。当前泡泡玛特的维护入口是其中的 `泡泡玛特_财报分析底稿.xlsx`:人工或 AI 从 PDF、截图、XLSX 提取的基础值先写入这份底稿,并保留来源文件、工作表和定位信息。`data/{company}/*.csv` 是图表运行时投影,只能由脚本生成,禁止手工编辑。
**维护明细,不维护可推导结果。** 收入、门店数、财报科目等基础值应保存;占比、同比、利润率、H2 单半年和分析分类小计应由脚本生成。若财报披露了占比,可作为核对值保留,但不是图表的唯一输入。资产负债表的“类现金/经营类资产/有息负债”等分类以独立规则映射明细科目,不复制成第二套人工数字。
**数值默认保留一位小数。** CSV 数据列与图表展示(柱/点标签、tooltip)统一保留一位小数(如 `71.1`、`311.4%`),四舍五入;确实需要其他精度时必须在 `footnote()` 或代码注释里说明原因。
**CSV 存四位年份**(`2026H1`),简写只在显示层做。核对数据时无歧义。
+8
View File
@@ -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
1 period retail online robot_store wholesale total_revenue
2 2024H1 57.4 25.9 8.1 8.6 45.59
3 2024H2 49.0 35.2 5.4 10.4 84.78
4 2024FY 51.9 31.9 6.4 9.8 130.37
5 2025H1 49.8 39.6 5.9 4.7 138.75
6 2025H2 44.5 47.0 3.7 4.7 232.45
7 2025FY 46.5 44.3 4.6 4.7 371.20
8 2026H1 53.1 36.2 5.5 5.2 171.74
+3
View File
@@ -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
1 period retail online robot_store wholesale total_revenue
2 2024FY 51.9 31.9 6.4 9.8 130.37
3 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
1 period retail online robot_store wholesale total_revenue
2 2024H1 57.4 25.9 8.1 8.6 45.59
3 2024H2 49.0 35.2 5.4 10.4 84.78
4 2025H1 49.8 39.6 5.9 4.7 138.75
5 2025H2 44.5 47.0 3.7 4.7 232.45
6 2026H1 53.1 36.2 5.5 5.2 171.74
Binary file not shown.
Binary file not shown.
+22
View File
@@ -0,0 +1,22 @@
# 泡泡玛特财报分析底稿
本目录是泡泡玛特的唯一维护数据层。日常只维护 [泡泡玛特_财报分析底稿.xlsx](泡泡玛特_财报分析底稿.xlsx):图表使用的 `data/ppmt/*.csv` 由脚本生成,不直接编辑。
## 工作簿结构
- `说明`:维护入口、口径和颜色约定。
- `资产负债表`:期末时点的财报明细。
- `利润表`:半年累计与全年累计的财报明细。
- `现金流量表`:现金流量表披露的原始明细。
- `资产分类规则`:把财报明细映射为“类现金、投资资产、有息负债”等分析分类的规则;不维护分类金额。
- `检查`:资产负债、现金流量的勾稽差额。出现红色警告时,应回到来源核对,不能为了消除警告而改数。
## 更新流程
1. 将新财报 PDF、截图或原始表格保留在 `data/ppmt/raw/`,作为证据文件。
2. 由 AI 读取证据后,将直接披露的科目、期间和金额录入本工作簿的蓝色数字区域,并记录来源定位。
3. 检查期间、单位和勾稽关系;确认后再生成图表 CSV 与图表。
只维护基础值:收入、门店数、财报科目等。同比、占比、利润率、H2 单半年和分析分类小计都由后续脚本计算。若财报另有披露占比,可作为核对依据,不应成为第二套手工维护的数据。
期间约定:`FY` 是全年累计/年末,`H1` 是上半年累计/六月末;不得把来源的 `H2` 直接当作下半年单期。金额单位为人民币亿元。
+81
View File
@@ -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
1 region channel period revenue
2 中国 零售店 2024H1 20.04
3 中国 零售店 2024FY 46.01
4 中国 零售店 2025H1 44.06
5 中国 零售店 2025FY 100.75
6 中国 零售店 2026H1 60.90
7 中国 机器人商店 2024H1 3.38
8 中国 机器人商店 2024FY 7.51
9 中国 机器人商店 2025H1 6.78
10 中国 机器人商店 2025FY 13.46
11 中国 机器人商店 2026H1 7.79
12 中国 线上渠道 2024H1 9.41
13 中国 线上渠道 2024FY 27.72
14 中国 线上渠道 2025H1 29.37
15 中国 线上渠道 2025FY 85.22
16 中国 线上渠道 2026H1 47.79
17 中国 批发及其他 2024H1 2.39
18 中国 批发及其他 2024FY 7.63
19 中国 批发及其他 2025H1 2.61
20 中国 批发及其他 2025FY 9.08
21 中国 批发及其他 2026H1 5.53
22 亚太 零售店 2024H1 4.85
23 亚太 零售店 2024FY 16.68
24 亚太 零售店 2025H1 14.93
25 亚太 零售店 2025FY 43.19
26 亚太 零售店 2026H1 17.16
27 亚太 机器人商店 2024H1 0.17
28 亚太 机器人商店 2024FY 0.40
29 亚太 机器人商店 2025H1 0.33
30 亚太 机器人商店 2025FY 0.95
31 亚太 机器人商店 2026H1 0.57
32 亚太 线上渠道 2024H1 1.65
33 亚太 线上渠道 2024FY 9.72
34 亚太 线上渠道 2025H1 10.69
35 亚太 线上渠道 2025FY 30.53
36 亚太 线上渠道 2026H1 6.44
37 亚太 批发及其他 2024H1 1.29
38 亚太 批发及其他 2024FY 4.30
39 亚太 批发及其他 2025H1 2.56
40 亚太 批发及其他 2025FY 5.44
41 亚太 批发及其他 2026H1 1.59
42 美洲 零售店 2024H1 0.87
43 美洲 零售店 2024FY 3.41
44 美洲 零售店 2025H1 7.37
45 美洲 零售店 2025FY 20.04
46 美洲 零售店 2026H1 9.03
47 美洲 机器人商店 2024H1 0.13
48 美洲 机器人商店 2024FY 0.38
49 美洲 机器人商店 2025H1 1.05
50 美洲 机器人商店 2025FY 2.32
51 美洲 机器人商店 2026H1 1.04
52 美洲 线上渠道 2024H1 0.64
53 美洲 线上渠道 2024FY 3.64
54 美洲 线上渠道 2025H1 13.27
55 美洲 线上渠道 2025FY 43.54
56 美洲 线上渠道 2026H1 7.22
57 美洲 批发及其他 2024H1 0.19
58 美洲 批发及其他 2024FY 0.58
59 美洲 批发及其他 2025H1 0.96
60 美洲 批发及其他 2025FY 2.17
61 美洲 批发及其他 2026H1 1.62
62 欧洲及其他 零售店 2024H1 0.41
63 欧洲及其他 零售店 2024FY 1.57
64 欧洲及其他 零售店 2025H1 2.72
65 欧洲及其他 零售店 2025FY 8.56
66 欧洲及其他 零售店 2026H1 4.03
67 欧洲及其他 机器人商店 2024H1 0.01
68 欧洲及其他 机器人商店 2024FY 0.02
69 欧洲及其他 机器人商店 2025H1 0.04
70 欧洲及其他 机器人商店 2025FY 0.17
71 欧洲及其他 机器人商店 2026H1 0.11
72 欧洲及其他 线上渠道 2024H1 0.11
73 欧洲及其他 线上渠道 2024FY 0.55
74 欧洲及其他 线上渠道 2025H1 1.65
75 欧洲及其他 线上渠道 2025FY 5.05
76 欧洲及其他 线上渠道 2026H1 0.68
77 欧洲及其他 批发及其他 2024H1 0.05
78 欧洲及其他 批发及其他 2024FY 0.25
79 欧洲及其他 批发及其他 2025H1 0.36
80 欧洲及其他 批发及其他 2025FY 0.73
81 欧洲及其他 批发及其他 2026H1 0.24
Binary file not shown.
+172
View File
@@ -0,0 +1,172 @@
# 财报图表项目数据架构改造方案
> 状态:建议实施
>
> 目标:保持静态图表站点的低复杂度,同时建立可追溯、可重复生成的数据链路。
## 1. 决策
当前不建设在线数据库和后端 API。
本项目目前是静态数据展示工具:浏览器按需读取 CSV,数据量很小,没有多人并发写入、权限控制、运行时查询或服务端计算的需求。此时引入 Postgres、API、鉴权和迁移机制会增加运维成本,但不能解决当前的数据正确性与可维护性问题。
CSV 保留为图表的运行时交付格式;需要改造的是 CSV 之前的数据生产链路。
## 2. 当前问题
### 2.1 权威数据源不一致
设计文档和 `data/ppmt/rawData/README.md` 将 `财报分析底稿.xlsx` 定义为唯一维护入口,但 `scripts/normalize_ppmt_financials.mjs` 实际读取的是 `data/ppmt/raw/*.csv`。底稿更新不会稳定地传递到图表。
### 2.2 派生产物被当作输入
归一化脚本的一部分通过 `readRuntimeRows()` 读取 `data/ppmt/*.csv`,再生成年度、半年等视图。IP、品类、资产等数据不能完全从上游重新构建;运行时 CSV 被删除、手改或过期时,会影响后续结果。
### 2.3 数据更新不完整
已出现底稿含有新期间、但相应图表 CSV 尚未更新的情况。问题不在某个数值,而在于链路没有可重复的全量构建保证。
### 2.4 发布边界不清晰
Vite 目前递归复制 `data/` 下的全部 CSV,因此原始披露 CSV 也会进入 `dist/`。原始证据、维护输入和公开运行时数据应有物理边界。
### 2.5 现有测试只验证展示
`pnpm test` 已能验证 manifest、CSV 可读性、图表渲染和部分视觉规范;还没有验证期间、单位、勾稽、同比口径、来源定位或派生逻辑。
## 3. 目标架构
```text
原始披露文件(PDF / XLSX / 原始导出 CSV,只读证据)
│
▼
人工录入与复核的规范化基础事实层(唯一权威)
│ 数据构建:映射、同比、期间拆分、勾稽、图表数据整形
▼
图表运行时 CSV(完全可重建,不允许手工修改)
│
▼
静态站点:manifest + Vanilla JS + ECharts
```
### 3.1 各层职责
| 层 | 内容 | 是否人工编辑 | 发布到网站 |
| --- | --- | --- | --- |
| 原始证据 | 财报 PDF、公司 XLSX、原始导出 CSV、截图 | 否 | 否 |
| 基础事实层 | 直接披露的数值、期间、单位、来源定位、科目映射 | 是,受控编辑 | 否 |
| 派生层 | H2、同比、利润率、分类汇总、年度/半年视图 | 否,脚本生成 | 否 |
| 图表 CSV | 仅包含图表需要的字段和布局 | 否,脚本生成 | 是 |
## 4. 基础事实层如何维护
基础事实层需要人工维护,但人工只编辑直接披露且可追溯的事实。
允许人工编辑的内容:
- 原始财报直接披露的金额、门店数等基础数值;
- 期间、币种、单位、合并口径;
- 来源文件、页码、表格、行号或单元格定位;
- 科目映射与分类规则,例如“类现金”“投资资产”“经营资产”。
禁止人工编辑的内容:
- 同比、环比、占比、利润率;
- `H2 = FY - H1` 等期间拆分;
- 分析分类小计;
- 任何图表运行时 CSV。
现有 `财报分析底稿.xlsx` 可以继续作为人工录入界面。改造后的要求是:构建脚本必须从这份工作簿或其一次性导出的规范化输入文件读取,不再并行从其他位置读取同一事实。工作簿是编辑器,不是与其他 CSV 并列的第二权威来源。
基础事实建议采用“长表”结构,便于复用、校验和未来接入多家公司:
| company_id | metric_id | period_end | period_type | value | unit | source_file | source_locator | revision |
| --- | --- | --- | --- | ---: | --- | --- | --- | --- |
| ppmt | revenue | 2026-06-30 | H1_YTD | 171.73 | CNY_100M | 2026H1.pdf | p.12 / 利润表 / 营业收入 | 1 |
运营类事实可使用相同结构,或按需要增加 `dimension_type`、`dimension_value`,例如地区、渠道、IP 和品类。
## 5. 目录与构建边界
建议将数据目录拆分,避免将证据文件误发布:
```text
data/
ppmt/
evidence/ # 原始披露文件,只读,不发布
input/ # 基础事实与映射规则,受控维护,不发布
generated/ # 构建中间产物,可删除并重建,不发布
public/
data/
ppmt/ # 仅图表运行时 CSV,发布
scripts/
import_ppmt_facts.* # 从 XLSX 或受控输入导入基础事实
build_ppmt_datasets.* # 从基础事实完整生成图表 CSV
check_ppmt_data.* # 质量与勾稽校验
```
Vite 仅复制 `public/`;不再以“复制整个 `data/` 再过滤扩展名”的方式发布文件。
## 6. 数据构建与校验
推荐提供三个明确命令:
```bash
pnpm data:import # 可选:从人工维护的 XLSX 导出/读取基础事实
pnpm data:build # 从唯一权威源全量生成 generated 与 public/data
pnpm data:check # 校验输入、派生结果与勾稽关系
pnpm test # 图表渲染和展示回归
pnpm build # 静态站点构建
```
`data:build` 必须满足幂等性:在清空 `generated/` 和 `public/data/` 后,仍能只凭 `input/` 与 `evidence/` 完整生成相同的图表数据。构建脚本不得读取自身输出目录作为输入。
`data:check` 至少覆盖:
- 必填字段、数值类型、单位和币种;
- 期间格式、排序、重复与缺失;
- 流量类 `H2 = FY - H1`,时点类 H2/年末口径;
- 同比仅比较同口径上一年期间;
- 资产 = 负债 + 权益、现金流勾稽等适用检查;
- 图表字段 schema、空值约束和合理数值范围;
- 每条人工事实都有来源定位。
## 7. 分阶段实施
### P0:先恢复单一真相源
1. 决定以现有底稿 XLSX 作为人工维护入口。
2. 为底稿建立稳定导入器,输出规范化基础事实;或将其导出为版本化输入 CSV。
3. 将现有脚本中读取运行时 CSV 的逻辑改为读取基础事实。
4. 将图表 CSV 移到仅发布目录,原始证据移出发布路径。
5. 增加 `data:check` 的最小校验与全量重建测试。
完成标准:删除全部图表 CSV 后,仍能从唯一输入源重建所有图表;底稿新增期间后,数据检查能明确指出哪些图表应更新。
### P1:第二家公司接入后抽象共性
1. 固化跨公司通用的基础事实 schema、期间模型、来源定位格式和校验规则。
2. 仅在两个公司确实复用同一字段映射与口径时,抽取通用图表或数据模板。
3. 为 manifest 增加可选的数据契约版本,避免图表代码默默依赖 CSV 列名。
### P2:需要分析型查询时引入本地数据库
当需要跨公司指标筛选、较多事实数据查询、复杂聚合或批量分析时,引入 DuckDB + Parquet 作为本地构建和分析层。它可以替代或补强 `generated/`,但前端仍然发布静态 CSV 或预聚合 JSON。
### P3:满足以下条件才建设在线数据库
仅在出现多人在线录入、权限分级、审计版本、自动数据抓取、实时更新或面向用户的任意组合查询时,再建设 Postgres + 后端 API。
在线数据库只替换“基础事实层的存储与协作方式”;图表运行时仍应优先使用缓存后的静态数据集或受版本控制的 API 响应,不应让每张图直接查询底层事实表。
## 8. 数据库选择原则
“使用数据库”不是替代数据治理的方案。无论存储介质是 XLSX、CSV、DuckDB 还是 Postgres,都必须先满足:
1. 只有一个权威输入源;
2. 派生产物可从该输入源完整重建;
3. 每个数值可回溯到披露来源;
4. 口径、单位和期间语义经过自动校验;
5. 前端只消费稳定的数据契约。
在当前规模下,最优路线是“XLSX 受控人工录入 + 规范化事实 + 自动生成 CSV + 静态发布”,而不是立即迁移在线数据库。
+8 -4
View File
@@ -23,10 +23,13 @@
## 2. 当前架构
```
原始财报 / PDF / XLSX(仓库外保存)
│ 人工、AI 或脚本:抽取、归一化、口径确认
原始财报 / PDF / XLSX / 截图(证据文件)
│ 人工或 AI 提取、复核并记录来源定位
▼
data/{company}/*.csv ← 运行时数据
data/{company}/rawData/财报分析底稿.xlsx ← 财报分析底稿:唯一可维护的数据层
│ 脚本:分类、同比/占比、年度/半年视图、勾稽
▼
data/{company}/*.csv ← 图表运行时数据,可随时重建,禁止手工编辑
▼
manifest.json ← 展示编排与图表配置的唯一事实来源
├─ chartCatalog ← 每张图只定义一次
@@ -69,7 +72,8 @@ js/main.js + js/ui.js ← 解析引用、渲染三栏、导出和
- `js/charts/index.js`:注册 `key → build 函数`。
- `js/charts/*.js`:只处理该图的业务字段与 ECharts 表达。
- `js/theme.js`:字体、配色、图例、坐标轴、脚注和水印等统一视觉规范。
- `data/{company}/`:图表需要的规范化 CSV。
- `data/{company}/rawData/`:财报分析底稿。每家公司以一份可读、可维护的工作簿作为入口;基础科目、期间、单位、来源文件/工作表/定位信息在此维护,分析分类通过独立规则映射,不复制维护分类金额。
- `data/{company}/*.csv`:图表需要的规范化 CSV,由底稿生成,不是维护入口。
---
+2
View File
@@ -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,
+83
View File
@@ -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
View File
@@ -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"] }
]
},
+49 -2
View File
@@ -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。');