diff --git a/data/brk/brk_holdings.csv b/data/brk/brk_holdings.csv
new file mode 100644
index 0000000..26f5299
--- /dev/null
+++ b/data/brk/brk_holdings.csv
@@ -0,0 +1,76 @@
+year,company,shares
+1988,大都会/ABC(Capital Cities/ABC),3000000
+1988,房地美(Freddie Mac),2400000
+1988,盖可保险(GEICO),6850000
+1988,华盛顿邮报(Washington Post),1727765
+1988,可口可乐(Coca-Cola),14172500
+1989,大都会/ABC(Capital Cities/ABC),3000000
+1989,房地美(Freddie Mac),2400000
+1989,盖可保险(GEICO),6850000
+1989,华盛顿邮报(Washington Post),1727765
+1989,可口可乐(Coca-Cola),23350000
+1990,大都会/ABC(Capital Cities/ABC),3000000
+1990,房地美(Freddie Mac),2400000
+1990,富国银行(Wells Fargo),5000000
+1990,盖可保险(GEICO),6850000
+1990,华盛顿邮报(Washington Post),1727765
+1990,可口可乐(Coca-Cola),46700000
+1991,大都会/ABC(Capital Cities/ABC),3000000
+1991,房地美(Freddie Mac),2495200
+1991,富国银行(Wells Fargo),5000000
+1991,盖可保险(GEICO),6850000
+1991,华盛顿邮报(Washington Post),1727765
+1991,吉列(Gillette),24000000
+1991,健力士(Guinness),31247000
+1991,可口可乐(Coca-Cola),46700000
+1992,大都会/ABC(Capital Cities/ABC),3000000
+1992,房地美(Freddie Mac),16196700
+1992,富国银行(Wells Fargo),6358418
+1992,盖可保险(GEICO),34250000
+1992,华盛顿邮报(Washington Post),1727765
+1992,吉列(Gillette),24000000
+1992,健力士(Guinness),38335000
+1992,可口可乐(Coca-Cola),93400000
+1992,通用动力(General Dynamics),4350000
+1993,大都会/ABC(Capital Cities/ABC),2000000
+1993,房地美(Freddie Mac),13654600
+1993,富国银行(Wells Fargo),6791218
+1993,盖可保险(GEICO),34250000
+1993,华盛顿邮报(Washington Post),1727765
+1993,吉列(Gillette),24000000
+1993,健力士(Guinness),38335000
+1993,可口可乐(Coca-Cola),93400000
+1993,通用动力(General Dynamics),4350000
+1994,大都会/ABC(Capital Cities/ABC),20000000
+1994,房地美(Freddie Mac),12761200
+1994,富国银行(Wells Fargo),6791218
+1994,盖可保险(GEICO),34250000
+1994,甘尼特(Gannett),6854500
+1994,华盛顿邮报(Washington Post),1727765
+1994,吉列(Gillette),24000000
+1994,可口可乐(Coca-Cola),100000000
+1994,美国运通(American Express),27759941
+1994,PNC 银行(PNC Bank),19453300
+1995,大都会/ABC(Capital Cities/ABC),20000000
+1995,房地美(Freddie Mac),12502500
+1995,富国银行(Wells Fargo),6791218
+1995,盖可保险(GEICO),34250000
+1995,吉列(Gillette),48000000
+1995,可口可乐(Coca-Cola),100000000
+1995,美国运通(American Express),49456900
+1996,房地美(Freddie Mac),64246000
+1996,富国银行(Wells Fargo),7291418
+1996,华盛顿邮报(Washington Post),1727765
+1996,华特迪士尼(Walt Disney),24614214
+1996,吉列(Gillette),48000000
+1996,可口可乐(Coca-Cola),200000000
+1996,麦当劳(McDonald's),30156600
+1996,美国运通(American Express),49456900
+1997,房地美(Freddie Mac),63977600
+1997,富国银行(Wells Fargo),6690218
+1997,华盛顿邮报(Washington Post),1727765
+1997,华特迪士尼(Walt Disney),21563414
+1997,吉列(Gillette),48000000
+1997,可口可乐(Coca-Cola),200000000
+1997,旅行家集团(Travelers Group),23733198
+1997,美国运通(American Express),49456900
diff --git a/data/brk/rawData/brk_holdings_1988_1997_export.csv b/data/brk/rawData/brk_holdings_1988_1997_export.csv
new file mode 100644
index 0000000..24a2b1c
--- /dev/null
+++ b/data/brk/rawData/brk_holdings_1988_1997_export.csv
@@ -0,0 +1,105 @@
+1997/12/31,,,,,,,,,,,
+公司名称,股份数量,成本(截至1997/12/31)(百万美元)*,市值(截至1997/12/31)(百万美元),,,,,,,,
+美国运通 (American Express),"49,456,900 ","1,392.70 ","4,414.00 ",,,,,,,,
+可口可乐 (The Coca-Cola Company),"200,000,000 ","1,298.90 ","13,337.50 ",,,,,,,,
+迪士尼 (The Walt Disney Company),"21,563,414 ",381,"2,134.80 ",,,,,,,,
+房地美 (Freddie Mac),"63,977,600 ",329,"2,683.10 ",,,,,,,,
+吉列 (The Gillette Company),"48,000,000 ",600,"4,821.00 ",,,,,,,,
+旅行家集团 (Travelers Group Inc.),"23,733,198 ",604,"1,278.60 ",,,,,,,,
+华盛顿邮报 (The Washington Post Company),"1,727,765 ",11,841,,,,,,,,
+富国银行 (Wells Fargo & Company),"6,690,218 ",413,"2,270.90 ",,,,,,,,
+,,,,,,,,,,,
+1996/12/31,,,,,,,,,,,
+公司,股数,成本\*(百万美元,截至1996/12/31),市值(百万美元,截至1996/12/31),,,,,,,,
+美国运通,"49,456,900 ","1,392.70 ","2,794.30 ",,,,,,,,
+可口可乐,"200,000,000 ","1,298.90 ","10,525.00 ",,,,,,,,
+华特迪士尼,"24,614,214 ",577,"1,716.80 ",,,,,,,,
+房地美,"64,246,000 ",333,"1,772.80 ",,,,,,,,
+吉列,"48,000,000 ",600,"3,732.00 ",,,,,,,,
+麦当劳,"30,156,600 ","1,265.30 ","1,368.40 ",,,,,,,,
+华盛顿邮报,"1,727,765 ",11,579,,,,,,,,
+富国银行,"7,291,418 ",498,"1,966.90 ",,,,,,,,
+,,,,,,,,,,,
+1995/12/31,,,,,,,,,,,
+公司,股数,成本 1995/12/31(百万美元),市值 1995/12/31(百万美元),,,,,,,,
+美国运通 (American Express),"49,456,900 ","1,392.70 ","2,046.30 ",,,,,,,,
+大都会/ABC (Capital Cities/ABC),"20,000,000 ",345,"2,467.50 ",,,,,,,,
+可口可乐 (The Coca-Cola Company),"100,000,000 ","1,298.90 ","7,425.00 ",,,,,,,,
+房地美 (Freddie Mac),"12,502,500 ",260,"1,044.00 ",,,,,,,,
+盖可保险 (GEICO Corp.),"34,250,000 ",46,"2,393.20 ",,,,,,,,
+吉列 (The Gillette Company),"48,000,000 ",600,"2,502.00 ",,,,,,,,
+富国银行 (Wells Fargo & Company),"6,791,218 ",424,"1,466.90 ",,,,,,,,
+,,,,,,,,,,,
+1994/12/31,,,,,,,,,,,
+公司,股份数量,成本 (千美元),市值(千美元),,,,,,,,
+美国运通 ,"27,759,941 ","US$723,919 ","US$818,918 ",,,,,,,,
+大都会/ABC,"20,000,000 ","345,000 ","1,705,000 ",,,,,,,,
+可口可乐,"100,000,000 ","1,298,888 ","5,150,000 ",,,,,,,,
+房地美,"12,761,200 ","270,468 ","644,441 ",,,,,,,,
+甘尼特公司,"6,854,500 ","335,216 ","365,002 ",,,,,,,,
+盖可保险 (GEICO),"34,250,000 ","45,713 ","1,678,250 ",,,,,,,,
+吉列 (Gillette),"24,000,000 ","600,000 ","1,797,000 ",,,,,,,,
+PNC银行 (PNC Bank),"19,453,300 ","503,046 ","410,951 ",,,,,,,,
+华盛顿邮报,"1,727,765 ","9,731 ","418,983 ",,,,,,,,
+富国银行 ,"6,791,218 ","423,680 ","984,727 ",,,,,,,,
+,,,,,,,,,,,
+,,,,,,,,,,,
+1993/12/31,,,,,,,,,,,
+公司名称,股份数量,成本(千美元),市值(千美元),,,,,,,,
+大都会/ABC ,"2,000,000 ","US$345,000 ","US$1,239,000 ",,,,,,,,
+可口可乐 ,"93,400,000 ","1,023,920 ","4,167,975 ",,,,,,,,
+房地美 ,"13,654,600 ","307,505 ","681,023 ",,,,,,,,
+盖可保险 ,"34,250,000 ","45,713 ","1,759,594 ",,,,,,,,
+通用动力 ,"4,350,000 ","94,938 ","401,287 ",,,,,,,,
+吉列 ,"24,000,000 ","600,000 ","1,431,000 ",,,,,,,,
+健力士,"38,335,000 ","333,019 ","270,822 ",,,,,,,,
+华盛顿邮报 ,"1,727,765 ","9,731 ","440,148 ",,,,,,,,
+富国银行,"6,791,218 ","423,680 ","878,614 ",,,,,,,,
+,,,,,,,,,,,
+1992/12/31,,,,,,,,,,,
+公司名称,股份数量,成本 (千美元),市值(千美元),,,,,,,,
+大都会/ABC,"3,000,000 ","US$517,500 ","US$1,523,500 ",,,,,,,,
+可口可乐,"93,400,000 ","1,023,920 ","3,911,125 ",,,,,,,,
+房地美 ,"16,196,700 ","414,257 ","783,515 ",,,,,,,,
+盖可保险 ,"34,250,000 ","45,713 ","2,226,250 ",,,,,,,,
+通用动力,"4,350,000 ","312,438 ","450,769 ",,,,,,,,
+吉列 ,"24,000,000 ","600,000 ","1,365,000 ",,,,,,,,
+健力士,"38,335,000 ","333,019 ","299,581 ",,,,,,,,
+华盛顿邮报 ,"1,727,765 ","9,731 ","396,954 ",,,,,,,,
+富国银行,"6,358,418 ","380,983 ","485,624 ",,,,,,,,
+,,,,,,,,,,,
+1991/12/31,,,,,,,,,,,
+公司,股数,成本(千美元),市值(千美元),,,,,,,,
+大都会/ABC,"3,000,000 ","US$517,500 ","US$1,300,500 ",,,,,,,,
+可口可乐,"46,700,000 ","1,023,920 ","3,747,675 ",,,,,,,,
+房地美,"2,495,200 ","77,245 ","343,090 ",,,,,,,,
+盖可保险,"6,850,000 ","45,713 ","1,363,150 ",,,,,,,,
+吉列,"24,000,000 ","600,000 ","1,347,000 ",,,,,,,,
+健力士 ,"31,247,000 ","264,782 ","296,755 ",,,,,,,,
+华盛顿邮报,"1,727,765 ","9,731 ","336,050 ",,,,,,,,
+富国银行,"5,000,000 ","289,431 ","290,000 ",,,,,,,,
+,,,,,,,,,,,
+1990/12/31,,,,,,,,,,,
+公司,持股数量,成本(千美元),市值(千美元),,,,,,,,
+大都会/ABC公司,"3,000,000 ","US$517,500 ","US$1,377,375 ",,,,,,,,
+可口可乐公司,"46,700,000 ","1,023,920 ","2,171,550 ",,,,,,,,
+联邦住房贷款抵押公司,"2,400,000 ","71,729 ","117,000 ",,,,,,,,
+盖可保险公司,"6,850,000 ","45,713 ","1,110,556 ",,,,,,,,
+华盛顿邮报公司,"1,727,765 ","9,731 ","342,097 ",,,,,,,,
+富国银行,"5,000,000 ","289,431 ","289,375 ",,,,,,,,
+,,,,,,,,,,,
+1989/12/31,,,,,,,,,,,
+股份数量,公司名称,成本(千美元),市值(千美元),,,,,,,,
+"3,000,000 ",大都会/ABC,"US$517,500 ","US$1,692,375 ",,,,,,,,
+"23,350,000 ",可口可乐,"1,023,920 ","1,803,787 ",,,,,,,,
+"2,400,000 ",房地美优先股,"71,729 ","161,100 ",,,,,,,,
+"6,850,000 ",盖可保险,"45,713 ","1,044,625 ",,,,,,,,
+"1,727,765 ",华盛顿邮报 ,"9,731 ","486,366 ",,,,,,,,
+,,,,,,,,,,,
+1988/12/31,,,,,,,,,,,
+股份数量,公司名称,成本(千美元),市值(千美元),,,,,,,,
+"3,000,000 ",大都会/ABC,"US$517,500 ","US$1,086,750 ",,,,,,,,
+"14,172,500 ",可口可乐公司,"592,540 ","632,448 ",,,,,,,,
+"2,400,000 ",联邦住宅贷款抵押公司优先股\*,"71,729 ","121,200 ",,,,,,,,
+"6,850,000 ",盖可保险,"45,713 ","849,400 ",,,,,,,,
+"1,727,765 ",华盛顿邮报公司,"9,731 ","364,126 ",,,,,,,,
diff --git a/data/brk/rawData/伯克希尔 1988-1997 持仓变化.numbers b/data/brk/rawData/伯克希尔 1988-1997 持仓变化.numbers
new file mode 100644
index 0000000..1b69d5b
Binary files /dev/null and b/data/brk/rawData/伯克希尔 1988-1997 持仓变化.numbers differ
diff --git a/data/ppmt/ppmt_channel_share_americas.csv b/data/ppmt/ppmt_channel_share_americas.csv
new file mode 100644
index 0000000..0382cb2
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_americas.csv
@@ -0,0 +1,8 @@
+period,retail,robot_store,own_app,tiktok,amazon,online_other,wholesale,total_revenue
+2024H1,47.5,7.1,15.8,8.2,0.0,10.9,10.4,1.83
+2024H2,41.0,4.0,26.3,15.0,0.0,7.3,6.3,6.19
+2024FY,42.5,4.7,23.9,13.5,0.0,8.1,7.2,8.02
+2025H1,32.5,4.6,39.2,13.9,0.0,5.5,4.2,22.65
+2025H2,27.9,2.8,43.7,16.0,0.0,7.0,2.7,45.42
+2025FY,29.4,3.4,42.2,15.3,0.0,6.5,3.2,68.07
+2026H1,47.8,5.5,26.0,0.0,8.9,3.3,8.6,18.91
diff --git a/data/ppmt/ppmt_channel_share_americas_annual.csv b/data/ppmt/ppmt_channel_share_americas_annual.csv
new file mode 100644
index 0000000..c3e5080
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_americas_annual.csv
@@ -0,0 +1,3 @@
+period,retail,robot_store,own_app,tiktok,amazon,online_other,wholesale,total_revenue
+2024FY,42.5,4.7,23.9,13.5,0.0,8.1,7.2,8.02
+2025FY,29.4,3.4,42.2,15.3,0.0,6.5,3.2,68.07
diff --git a/data/ppmt/ppmt_channel_share_americas_halfyear.csv b/data/ppmt/ppmt_channel_share_americas_halfyear.csv
new file mode 100644
index 0000000..3e11c07
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_americas_halfyear.csv
@@ -0,0 +1,6 @@
+period,retail,robot_store,own_app,tiktok,amazon,online_other,wholesale,total_revenue
+2024H1,47.5,7.1,15.8,8.2,0.0,10.9,10.4,1.83
+2024H2,41.0,4.0,26.3,15.0,0.0,7.3,6.3,6.19
+2025H1,32.5,4.6,39.2,13.9,0.0,5.5,4.2,22.65
+2025H2,27.9,2.8,43.7,16.0,0.0,7.0,2.7,45.42
+2026H1,47.8,5.5,26.0,0.0,8.9,3.3,8.6,18.91
diff --git a/data/ppmt/ppmt_channel_share_apac.csv b/data/ppmt/ppmt_channel_share_apac.csv
new file mode 100644
index 0000000..e4bf965
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_apac.csv
@@ -0,0 +1,8 @@
+period,retail,robot_store,own_app,shopee,online_other,wholesale,total_revenue
+2024H1,60.9,2.1,5.0,5.1,10.7,16.2,7.97
+2024H2,51.1,1.0,11.5,10.5,12.8,13.0,23.13
+2024FY,53.6,1.3,9.8,9.2,12.3,13.8,31.10
+2025H1,52.4,1.2,11.0,9.9,16.7,9.0,28.51
+2025H2,54.8,1.2,22.2,5.6,10.7,5.6,51.60
+2025FY,53.9,1.2,18.2,7.1,12.8,6.8,80.11
+2026H1,66.6,2.2,13.1,4.2,7.7,6.2,25.76
diff --git a/data/ppmt/ppmt_channel_share_apac_annual.csv b/data/ppmt/ppmt_channel_share_apac_annual.csv
new file mode 100644
index 0000000..21fb45e
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_apac_annual.csv
@@ -0,0 +1,3 @@
+period,retail,robot_store,own_app,shopee,online_other,wholesale,total_revenue
+2024FY,53.6,1.3,9.8,9.2,12.3,13.8,31.10
+2025FY,53.9,1.2,18.2,7.1,12.8,6.8,80.11
diff --git a/data/ppmt/ppmt_channel_share_apac_halfyear.csv b/data/ppmt/ppmt_channel_share_apac_halfyear.csv
new file mode 100644
index 0000000..8b297d8
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_apac_halfyear.csv
@@ -0,0 +1,6 @@
+period,retail,robot_store,own_app,shopee,online_other,wholesale,total_revenue
+2024H1,60.9,2.1,5.0,5.1,10.7,16.2,7.97
+2024H2,51.1,1.0,11.5,10.5,12.8,13.0,23.13
+2025H1,52.4,1.2,11.0,9.9,16.7,9.0,28.51
+2025H2,54.8,1.2,22.2,5.6,10.7,5.6,51.60
+2026H1,66.6,2.2,13.1,4.2,7.7,6.2,25.76
diff --git a/data/ppmt/ppmt_channel_share_china.csv b/data/ppmt/ppmt_channel_share_china.csv
new file mode 100644
index 0000000..6ffd2c9
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_china.csv
@@ -0,0 +1,8 @@
+period,retail,robot_store,roboshop,tmall,douyin,online_other,wholesale,total_revenue
+2024H1,56.9,9.6,11.3,5.7,5.9,3.8,6.8,35.22
+2024H2,48.4,7.7,13.3,8.0,7.3,5.5,9.8,53.65
+2024FY,51.8,8.5,12.5,7.1,6.8,4.8,8.6,88.87
+2025H1,53.2,8.2,13.6,7.9,6.8,7.2,3.2,82.82
+2025H2,45.1,5.3,18.3,9.0,8.2,9.0,5.1,125.69
+2025FY,48.3,6.5,16.4,8.6,7.6,8.3,4.4,208.51
+2026H1,49.9,6.4,16.9,7.4,8.0,6.9,4.5,122.00
diff --git a/data/ppmt/ppmt_channel_share_china_annual.csv b/data/ppmt/ppmt_channel_share_china_annual.csv
new file mode 100644
index 0000000..77d5cd1
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_china_annual.csv
@@ -0,0 +1,3 @@
+period,retail,robot_store,roboshop,tmall,douyin,online_other,wholesale,total_revenue
+2024FY,51.8,8.5,12.5,7.1,6.8,4.8,8.6,88.87
+2025FY,48.3,6.5,16.4,8.6,7.6,8.3,4.4,208.51
diff --git a/data/ppmt/ppmt_channel_share_china_halfyear.csv b/data/ppmt/ppmt_channel_share_china_halfyear.csv
new file mode 100644
index 0000000..901f026
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_china_halfyear.csv
@@ -0,0 +1,6 @@
+period,retail,robot_store,roboshop,tmall,douyin,online_other,wholesale,total_revenue
+2024H1,56.9,9.6,11.3,5.7,5.9,3.8,6.8,35.22
+2024H2,48.4,7.7,13.3,8.0,7.3,5.5,9.8,53.65
+2025H1,53.2,8.2,13.6,7.9,6.8,7.2,3.2,82.82
+2025H2,45.1,5.3,18.3,9.0,8.2,9.0,5.1,125.69
+2026H1,49.9,6.4,16.9,7.4,8.0,6.9,4.5,122.00
diff --git a/data/ppmt/ppmt_channel_share_europe.csv b/data/ppmt/ppmt_channel_share_europe.csv
new file mode 100644
index 0000000..26ab2d2
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_europe.csv
@@ -0,0 +1,8 @@
+period,retail,robot_store,own_app,online_other,wholesale,total_revenue
+2024H1,70.7,1.7,1.7,17.2,8.6,0.58
+2024H2,64.1,0.6,11.0,13.3,11.0,1.81
+2024FY,65.7,0.8,8.8,14.2,10.5,2.39
+2025H1,57.0,0.8,17.8,16.8,7.5,4.77
+2025H2,60.0,1.3,23.7,11.2,3.8,9.74
+2025FY,59.0,1.2,21.8,13.0,5.0,14.51
+2026H1,79.6,2.2,10.5,3.0,4.7,5.06
diff --git a/data/ppmt/ppmt_channel_share_europe_annual.csv b/data/ppmt/ppmt_channel_share_europe_annual.csv
new file mode 100644
index 0000000..77f86e8
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_europe_annual.csv
@@ -0,0 +1,3 @@
+period,retail,robot_store,own_app,online_other,wholesale,total_revenue
+2024FY,65.7,0.8,8.8,14.2,10.5,2.39
+2025FY,59.0,1.2,21.8,13.0,5.0,14.51
diff --git a/data/ppmt/ppmt_channel_share_europe_halfyear.csv b/data/ppmt/ppmt_channel_share_europe_halfyear.csv
new file mode 100644
index 0000000..a706a13
--- /dev/null
+++ b/data/ppmt/ppmt_channel_share_europe_halfyear.csv
@@ -0,0 +1,6 @@
+period,retail,robot_store,own_app,online_other,wholesale,total_revenue
+2024H1,70.7,1.7,1.7,17.2,8.6,0.58
+2024H2,64.1,0.6,11.0,13.3,11.0,1.81
+2025H1,57.0,0.8,17.8,16.8,7.5,4.77
+2025H2,60.0,1.3,23.7,11.2,3.8,9.74
+2026H1,79.6,2.2,10.5,3.0,4.7,5.06
diff --git a/data/ppmt/rawData/地区渠道收入.csv b/data/ppmt/rawData/地区渠道收入.csv
index 7118114..14012fb 100644
--- a/data/ppmt/rawData/地区渠道收入.csv
+++ b/data/ppmt/rawData/地区渠道收入.csv
@@ -14,6 +14,26 @@ region,channel,period,revenue
中国,线上渠道,2025H1,29.37
中国,线上渠道,2025FY,85.22
中国,线上渠道,2026H1,47.79
+中国,泡泡玛特抽盒机,2024H1,3.99
+中国,泡泡玛特抽盒机,2024FY,11.14
+中国,泡泡玛特抽盒机,2025H1,11.25
+中国,泡泡玛特抽盒机,2025FY,34.23
+中国,泡泡玛特抽盒机,2026H1,20.63
+中国,天猫旗舰店,2024H1,1.99
+中国,天猫旗舰店,2024FY,6.28
+中国,天猫旗舰店,2025H1,6.58
+中国,天猫旗舰店,2025FY,17.85
+中国,天猫旗舰店,2026H1,9.01
+中国,抖音平台,2024H1,2.09
+中国,抖音平台,2024FY,6.01
+中国,抖音平台,2025H1,5.61
+中国,抖音平台,2025FY,15.90
+中国,抖音平台,2026H1,9.76
+中国,其他线上渠道,2024H1,1.34
+中国,其他线上渠道,2024FY,4.29
+中国,其他线上渠道,2025H1,5.93
+中国,其他线上渠道,2025FY,17.24
+中国,其他线上渠道,2026H1,8.38
中国,批发及其他,2024H1,2.39
中国,批发及其他,2024FY,7.63
中国,批发及其他,2025H1,2.61
@@ -34,6 +54,21 @@ region,channel,period,revenue
亚太,线上渠道,2025H1,10.69
亚太,线上渠道,2025FY,30.53
亚太,线上渠道,2026H1,6.44
+亚太,自研APP及官方网站,2024H1,0.40
+亚太,自研APP及官方网站,2024FY,3.05
+亚太,自研APP及官方网站,2025H1,3.13
+亚太,自研APP及官方网站,2025FY,14.58
+亚太,自研APP及官方网站,2026H1,3.38
+亚太,Shopee,2024H1,0.41
+亚太,Shopee,2024FY,2.85
+亚太,Shopee,2025H1,2.81
+亚太,Shopee,2025FY,5.69
+亚太,Shopee,2026H1,1.07
+亚太,其他线上渠道,2024H1,0.85
+亚太,其他线上渠道,2024FY,3.82
+亚太,其他线上渠道,2025H1,4.75
+亚太,其他线上渠道,2025FY,10.26
+亚太,其他线上渠道,2026H1,1.99
亚太,批发及其他,2024H1,1.29
亚太,批发及其他,2024FY,4.30
亚太,批发及其他,2025H1,2.56
@@ -54,6 +89,26 @@ region,channel,period,revenue
美洲,线上渠道,2025H1,13.27
美洲,线上渠道,2025FY,43.54
美洲,线上渠道,2026H1,7.22
+美洲,自研APP及官方网站,2024H1,0.29
+美洲,自研APP及官方网站,2024FY,1.92
+美洲,自研APP及官方网站,2025H1,8.87
+美洲,自研APP及官方网站,2025FY,28.70
+美洲,自研APP及官方网站,2026H1,4.91
+美洲,TikTok,2024H1,0.15
+美洲,TikTok,2024FY,1.08
+美洲,TikTok,2025H1,3.15
+美洲,TikTok,2025FY,10.40
+美洲,TikTok,2026H1,0.00
+美洲,亚马逊,2024H1,0.00
+美洲,亚马逊,2024FY,0.00
+美洲,亚马逊,2025H1,0.00
+美洲,亚马逊,2025FY,0.00
+美洲,亚马逊,2026H1,1.68
+美洲,其他线上渠道,2024H1,0.20
+美洲,其他线上渠道,2024FY,0.65
+美洲,其他线上渠道,2025H1,1.25
+美洲,其他线上渠道,2025FY,4.44
+美洲,其他线上渠道,2026H1,0.63
美洲,批发及其他,2024H1,0.19
美洲,批发及其他,2024FY,0.58
美洲,批发及其他,2025H1,0.96
@@ -74,6 +129,16 @@ region,channel,period,revenue
欧洲及其他,线上渠道,2025H1,1.65
欧洲及其他,线上渠道,2025FY,5.05
欧洲及其他,线上渠道,2026H1,0.68
+欧洲及其他,自研APP及官方网站,2024H1,0.01
+欧洲及其他,自研APP及官方网站,2024FY,0.21
+欧洲及其他,自研APP及官方网站,2025H1,0.85
+欧洲及其他,自研APP及官方网站,2025FY,3.16
+欧洲及其他,自研APP及官方网站,2026H1,0.53
+欧洲及其他,其他线上渠道,2024H1,0.10
+欧洲及其他,其他线上渠道,2024FY,0.34
+欧洲及其他,其他线上渠道,2025H1,0.80
+欧洲及其他,其他线上渠道,2025FY,1.89
+欧洲及其他,其他线上渠道,2026H1,0.15
欧洲及其他,批发及其他,2024H1,0.05
欧洲及其他,批发及其他,2024FY,0.25
欧洲及其他,批发及其他,2025H1,0.36
diff --git a/js/charts/brk_holdings.js b/js/charts/brk_holdings.js
new file mode 100644
index 0000000..7921ddf
--- /dev/null
+++ b/js/charts/brk_holdings.js
@@ -0,0 +1,174 @@
+import {
+ PALETTE, FONT, categoryAxis, titleStyle,
+ standardLegend, footnote, watermarks, LAYOUT,
+} from '../theme.js';
+
+const STATE_LABELS = {
+ initial: '期初持有',
+ added: '新建仓',
+ held: '持续持有',
+ sold: '清仓',
+};
+
+const STATE_COLORS = {
+ initial: PALETTE.sand,
+ added: PALETTE.green,
+ sold: PALETTE.coral,
+};
+
+const SHORT_NAMES = {
+ '美国运通(American Express)': '美国运通',
+ '可口可乐(Coca-Cola)': '可口可乐',
+ '华特迪士尼(Walt Disney)': '迪士尼',
+ '房地美(Freddie Mac)': '房地美',
+ '吉列(Gillette)': '吉列',
+ '旅行家集团(Travelers Group)': '旅行家集团',
+ '华盛顿邮报(Washington Post)': '华盛顿邮报',
+ '富国银行(Wells Fargo)': '富国银行',
+ '大都会/ABC(Capital Cities/ABC)': '大都会/ABC',
+ '盖可保险(GEICO)': '盖可保险',
+ '甘尼特(Gannett)': '甘尼特',
+ 'PNC 银行(PNC Bank)': 'PNC 银行',
+ '通用动力(General Dynamics)': '通用动力',
+ '健力士(Guinness)': '健力士',
+ "麦当劳(McDonald's)": '麦当劳',
+};
+
+function formatShares(value) {
+ return Number(value).toLocaleString('en-US');
+}
+
+function quantityChange(current, previous) {
+ if (!Number.isFinite(previous)) return '无可比上年数据';
+ const change = current - previous;
+ const sign = change > 0 ? '+' : '';
+ return `${sign}${formatShares(change)} 股`;
+}
+
+export function brkHoldingsChart(rows, { width, height }) {
+ const years = [...new Set(rows.map((row) => row.year))].sort((a, b) => a - b);
+ const holdingsByYear = new Map(years.map((year) => [year, new Map()]));
+ const firstYearByCompany = new Map();
+ const maxSharesByCompany = new Map();
+
+ rows.forEach((row) => {
+ holdingsByYear.get(row.year).set(row.company, row.shares);
+ firstYearByCompany.set(row.company, Math.min(firstYearByCompany.get(row.company) ?? row.year, row.year));
+ maxSharesByCompany.set(row.company, Math.max(maxSharesByCompany.get(row.company) ?? 0, row.shares));
+ });
+
+ const companies = [...firstYearByCompany.keys()].sort((a, b) => (
+ firstYearByCompany.get(a) - firstYearByCompany.get(b) || a.localeCompare(b, 'zh-CN')
+ ));
+ const dataByState = { initial: [], added: [], held: [], sold: [] };
+
+ years.forEach((year, yearIndex) => {
+ const current = holdingsByYear.get(year);
+ const previous = yearIndex > 0 ? holdingsByYear.get(years[yearIndex - 1]) : new Map();
+
+ companies.forEach((company, companyIndex) => {
+ const shares = current.get(company);
+ const priorShares = previous.get(company);
+ let state;
+ if (Number.isFinite(shares)) {
+ state = yearIndex === 0 ? 'initial' : (Number.isFinite(priorShares) ? 'held' : 'added');
+ } else if (yearIndex > 0 && Number.isFinite(priorShares)) {
+ state = 'sold';
+ } else {
+ return;
+ }
+
+ const relativeSize = Number.isFinite(shares)
+ ? Math.sqrt(shares / maxSharesByCompany.get(company))
+ : 1;
+ const color = state === 'held'
+ ? `rgba(59, 110, 165, ${(0.38 + relativeSize * 0.62).toFixed(2)})`
+ : STATE_COLORS[state];
+ const labelColor = state === 'initial' || (state === 'held' && relativeSize < 0.55)
+ ? PALETTE.ink
+ : '#fff';
+ dataByState[state].push({
+ value: [yearIndex, companyIndex, shares ?? 0],
+ shares,
+ priorShares,
+ state,
+ shortName: SHORT_NAMES[company] || company,
+ itemStyle: { color, borderColor: '#fff', borderWidth: 3 },
+ label: { color: labelColor },
+ });
+ });
+ });
+
+ return {
+ backgroundColor: '#fff',
+ title: {
+ ...titleStyle,
+ text: '伯克希尔主要持仓变动(1988–1997)',
+ subtext: '每格标注公司简称;持续持有的蓝色越深,表示相对持股数量越大',
+ },
+ legend: standardLegend([
+ STATE_LABELS.initial,
+ STATE_LABELS.added,
+ STATE_LABELS.held,
+ STATE_LABELS.sold,
+ ], { selectedMode: false, itemGap: 20 }),
+ tooltip: {
+ trigger: 'item',
+ textStyle: { fontFamily: FONT },
+ formatter: (params) => {
+ const data = params.data;
+ const company = companies[data.value[1]];
+ const year = years[data.value[0]];
+ const lines = [`${year} · ${company}`, `状态:${STATE_LABELS[data.state]}`];
+ if (Number.isFinite(data.shares)) {
+ lines.push(`年末持股:${formatShares(data.shares)} 股`);
+ if (data.state !== 'initial') lines.push(`较上年:${quantityChange(data.shares, data.priorShares)}`);
+ } else {
+ lines.push(`上年末持股:${formatShares(data.priorShares)} 股`);
+ }
+ return lines.join('
');
+ },
+ },
+ // ECharts 的 heatmap 组件要求声明 visualMap;实际颜色由每个状态单元格的 itemStyle 控制。
+ visualMap: {
+ show: false,
+ min: 0,
+ max: Math.max(...rows.map((row) => row.shares)),
+ dimension: 2,
+ inRange: { color: ['#ffffff'] },
+ },
+ grid: { left: 258, right: 48, top: LAYOUT.gridTop, bottom: 72 },
+ xAxis: {
+ ...categoryAxis,
+ type: 'category',
+ data: years.map(String),
+ splitArea: { show: true, areaStyle: { color: ['#fbfbfa'] } },
+ },
+ yAxis: {
+ ...categoryAxis,
+ type: 'category',
+ inverse: true,
+ data: companies,
+ axisLabel: { ...categoryAxis.axisLabel, fontSize: 12, margin: 14 },
+ splitArea: { show: true, areaStyle: { color: ['#fbfbfa'] } },
+ },
+ series: Object.entries(dataByState).map(([state, data]) => ({
+ name: STATE_LABELS[state],
+ type: 'heatmap',
+ data,
+ label: {
+ show: true,
+ position: 'inside',
+ fontFamily: FONT,
+ fontSize: 12,
+ fontWeight: 600,
+ formatter: (params) => params.data.shortName,
+ },
+ emphasis: { itemStyle: { borderColor: PALETTE.ink, borderWidth: 2 } },
+ })),
+ graphic: [
+ ...watermarks(width, height),
+ footnote('绿色=相对上一年新增;红色=相对上一年清仓;蓝色深浅=相对持股数量 | @思考的Joey'),
+ ],
+ };
+}
diff --git a/js/charts/index.js b/js/charts/index.js
index c7f7134..c7e6ec5 100644
--- a/js/charts/index.js
+++ b/js/charts/index.js
@@ -26,6 +26,7 @@ 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 { makeRegionChannelShareChart } from './ppmt_channel_share_region.js';
import { storesRetailChart } from './ppmt_stores_retail.js';
import { storesRobotChart } from './ppmt_stores_robot.js';
import { regionShareChart } from './ppmt_region_share.js';
@@ -33,6 +34,7 @@ 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';
+import { brkHoldingsChart } from './brk_holdings.js';
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
export const chartRegistry = {
@@ -64,6 +66,10 @@ export const chartRegistry = {
'ppmt-expense-structure': expenseStructureChart,
'ppmt-channel-revenue': channelRevenueChart,
'ppmt-channel-share': channelShareChart,
+ 'ppmt-channel-share-china': makeRegionChannelShareChart('china'),
+ 'ppmt-channel-share-apac': makeRegionChannelShareChart('apac'),
+ 'ppmt-channel-share-americas': makeRegionChannelShareChart('americas'),
+ 'ppmt-channel-share-europe': makeRegionChannelShareChart('europe'),
'ppmt-stores-retail': storesRetailChart,
'ppmt-stores-robot': storesRobotChart,
'ppmt-region-share': regionShareChart,
@@ -71,6 +77,7 @@ export const chartRegistry = {
'ppmt-stores-retail-share': storesRetailShareChart,
'ppmt-retail-store-revenue': retailStoreRevenueChart,
'ppmt-robot-store-revenue': robotStoreRevenueChart,
+ 'brk-holdings': brkHoldingsChart,
};
export function getChartBuilder(key) {
diff --git a/js/charts/ppmt_channel_share.js b/js/charts/ppmt_channel_share.js
index cea360f..40759b0 100644
--- a/js/charts/ppmt_channel_share.js
+++ b/js/charts/ppmt_channel_share.js
@@ -1,7 +1,7 @@
import { col } from '../csv.js';
import {
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
- financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
+ financialViewLabel, barSeries, stackedSharePoint, fanTinyLabelOffsets, footnote, watermarks, LAYOUT,
} from '../theme.js';
const CHANNELS = [
@@ -11,21 +11,14 @@ const CHANNELS = [
{ 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;
+ // 绘图区像素高:画布高 − grid top 145 − grid bottom 100;轴固定 0–100%
+ const pxPerUnit = (height - LAYOUT.gridTop - 100) / 100;
+ const seriesKeys = CHANNELS.map((item) => item.key);
+ const rowOffsets = fanTinyLabelOffsets(rows, seriesKeys, pxPerUnit);
return {
backgroundColor: '#fff',
@@ -59,9 +52,13 @@ export function channelShareChart(rows, { width, height, view = 'all' }) {
interval: 20,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
},
- series: CHANNELS.map((item) => barSeries({
+ series: CHANNELS.map((item, ci) => barSeries({
name: item.name,
- data: rows.map((row) => sharePoint(row[item.key], item.labelColor)),
+ data: rows.map((row, ri) => stackedSharePoint(row[item.key], {
+ pxPerUnit,
+ labelColor: item.labelColor,
+ dy: rowOffsets[ri][ci] ?? 0,
+ })),
color: item.color,
barWidth,
stack: 'channel-share',
diff --git a/js/charts/ppmt_channel_share_region.js b/js/charts/ppmt_channel_share_region.js
new file mode 100644
index 0000000..9d5612d
--- /dev/null
+++ b/js/charts/ppmt_channel_share_region.js
@@ -0,0 +1,122 @@
+import { col } from '../csv.js';
+import {
+ CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
+ financialViewLabel, barSeries, stackedSharePoint, fanTinyLabelOffsets, footnote, watermarks, LAYOUT,
+} from '../theme.js';
+
+/**
+ * 四地区共用同一套渠道语义色:零售店/机器人商店/批发及其他沿用渠道色板;
+ * 线上细分逐平台一色,抖音与 TikTok 同属一条生态线共用青色,其他线上为中性灰。
+ * 各图 5~7 个系列,图内两两 ΔE 已验算 > 25(最紧:抖音青 vs 中性灰 25.1)。
+ */
+const SERIES = {
+ retail: { name: '零售店', color: CHANNEL_COLORS.retail, labelColor: '#fff' },
+ robot_store: { name: '机器人商店', color: CHANNEL_COLORS.robotStore, labelColor: '#fff' },
+ wholesale: { name: '批发及其他', color: CHANNEL_COLORS.wholesale, labelColor: PALETTE.ink },
+ online_other: { name: '其他线上', color: PALETTE.grey, labelColor: '#fff' },
+ roboshop: { name: '抽盒机', color: CHANNEL_COLORS.online, labelColor: '#fff' },
+ tmall: { name: '天猫旗舰店', color: '#8256B2', labelColor: '#fff' },
+ douyin: { name: '抖音平台', color: '#4E9EA7', labelColor: '#fff' },
+ own_app: { name: '自研APP及官网', color: '#C05B76', labelColor: '#fff' },
+ shopee: { name: 'Shopee', color: PALETTE.brown, labelColor: '#fff' },
+ tiktok: { name: 'TikTok', color: '#4E9EA7', labelColor: '#fff' },
+ amazon: { name: '亚马逊', color: PALETTE.red, labelColor: '#fff' },
+};
+
+const REGIONS = {
+ china: {
+ title: '中国区',
+ source: '中国(含港澳台)',
+ series: ['retail', 'robot_store', 'roboshop', 'tmall', 'douyin', 'online_other', 'wholesale'],
+ footnote: '数据来源:泡泡玛特中国区(含港澳台)分渠道收入披露;H2为单半年(全年−H1)\n线上渠道按财报披露拆分为抽盒机、天猫旗舰店、抖音平台与其他线上 | @思考的Joey',
+ },
+ apac: {
+ title: '亚太区',
+ source: '亚太区',
+ series: ['retail', 'robot_store', 'own_app', 'shopee', 'online_other', 'wholesale'],
+ footnote: '数据来源:泡泡玛特亚太区分渠道收入披露;H2为单半年(全年−H1)\n线上渠道按财报披露拆分为自研APP及官网、Shopee与其他线上 | @思考的Joey',
+ },
+ americas: {
+ title: '美洲区',
+ source: '美洲区',
+ series: ['retail', 'robot_store', 'own_app', 'tiktok', 'amazon', 'online_other', 'wholesale'],
+ footnote: '数据来源:泡泡玛特美洲区分渠道收入披露;H2为单半年(全年−H1)\n2026H1起TikTok无收入,亚马逊为2026年新开渠道 | @思考的Joey',
+ },
+ europe: {
+ title: '欧洲及其他区',
+ source: '欧洲及其他地区',
+ series: ['retail', 'robot_store', 'own_app', 'online_other', 'wholesale'],
+ footnote: '数据来源:泡泡玛特欧洲及其他地区分渠道收入披露;H2为单半年(全年−H1)\n线上渠道按财报披露拆分为自研APP及官网与其他线上 | @思考的Joey',
+ },
+};
+
+/** 分地区渠道收入占比:线上渠道拆分披露的细分平台,每根柱按当期该地区总收入归一化。 */
+export function makeRegionChannelShareChart(regionKey) {
+ const region = REGIONS[regionKey];
+ if (!region) throw new Error(`未配置的地区渠道占比图:${regionKey}`);
+ return function regionChannelShareChart(rows, { width, height, view = 'all' }) {
+ const periods = col(rows, 'period');
+ const barWidth = view === 'all' ? 48 : 80;
+ // 绘图区像素高:画布高 − grid top 145 − grid bottom 100;轴固定 0–100%
+ const pxPerUnit = (height - LAYOUT.gridTop - 100) / 100;
+ const rowOffsets = fanTinyLabelOffsets(rows, region.series, pxPerUnit);
+
+ return {
+ backgroundColor: '#fff',
+ title: {
+ ...titleStyle,
+ text: `泡泡玛特${region.title}渠道收入占比(${financialViewLabel(view)})`,
+ subtext: '归一化堆叠:每根柱为当期该地区总收入的 100%;线上渠道按披露细分拆分',
+ },
+ legend: standardLegend(region.series.map((key) => SERIES[key].name)),
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ textStyle: { fontFamily: FONT },
+ formatter: (params) => {
+ const row = rows[periods.indexOf(params[0].axisValue)];
+ return `${params[0].axisValue} ${region.source}总收入 ${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: region.series.map((key, ci) => barSeries({
+ name: SERIES[key].name,
+ data: rows.map((row, ri) => stackedSharePoint(row[key], {
+ pxPerUnit,
+ labelColor: SERIES[key].labelColor,
+ dy: rowOffsets[ri][ci] ?? 0,
+ })),
+ color: SERIES[key].color,
+ barWidth,
+ stack: `channel-share-${regionKey}`,
+ 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(region.footnote),
+ ],
+ };
+ };
+}
diff --git a/js/charts/ppmt_stores_retail_share.js b/js/charts/ppmt_stores_retail_share.js
index 381964b..d2f0dfb 100644
--- a/js/charts/ppmt_stores_retail_share.js
+++ b/js/charts/ppmt_stores_retail_share.js
@@ -1,7 +1,7 @@
import { col } from '../csv.js';
import {
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
- financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
+ financialViewLabel, barSeries, stackedSharePoint, fanTinyLabelOffsets, footnote, watermarks, LAYOUT,
} from '../theme.js';
/** 泡泡玛特分地区零售门店占比:每根柱标准化为各期末全球门店数的 100%。 */
@@ -13,19 +13,13 @@ const REGIONS = [
{ 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;
+ // 绘图区像素高:画布高 − grid top 145 − grid bottom 100;轴固定 0–100%
+ const pxPerUnit = (height - LAYOUT.gridTop - 100) / 100;
+ const seriesKeys = REGIONS.map((item) => item.key);
+ const rowOffsets = fanTinyLabelOffsets(rows, seriesKeys, pxPerUnit);
return {
backgroundColor: '#fff',
@@ -59,9 +53,13 @@ export function storesRetailShareChart(rows, { width, height, view = 'all' }) {
interval: 20,
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
},
- series: REGIONS.map((item) => barSeries({
+ series: REGIONS.map((item, ci) => barSeries({
name: item.name,
- data: rows.map((row) => sharePoint(row[item.key], item.labelColor)),
+ data: rows.map((row, ri) => stackedSharePoint(row[item.key], {
+ pxPerUnit,
+ labelColor: item.labelColor,
+ dy: rowOffsets[ri][ci] ?? 0,
+ })),
color: item.color,
barWidth,
stack: 'stores-retail-share',
diff --git a/js/theme.js b/js/theme.js
index c22664b..5885f78 100644
--- a/js/theme.js
+++ b/js/theme.js
@@ -291,6 +291,67 @@ export function barSeries({ name, data, color, barWidth, formatter, ...rest }) {
};
}
+/**
+ * 归一化堆叠占比图的矮段标签外置偏移。
+ * 段高不足(< minInsidePx 像素)放不下段内标签时,标签标到柱体右侧;
+ * 同一根柱里有多个矮段时,柱侧标签自下而上按 gap 像素扇形排开避免重叠
+ * (先例:ppmt_channel_revenue 的 fanOutsideLabels、ppmt_region_share 的柱侧标签)。
+ * 返回 { 行序号: { 系列序号: dy } },dy 为 ECharts label.offset 的纵向偏移。
+ */
+export function fanTinyLabelOffsets(rows, seriesKeys, pxPerUnit, { minInsidePx = 15, gap = 13 } = {}) {
+ return rows.map((row) => {
+ const centers = [];
+ let cum = 0;
+ seriesKeys.forEach((key) => {
+ const value = Number.isFinite(row[key]) ? row[key] : 0;
+ centers.push((cum + value / 2) * pxPerUnit);
+ cum += value;
+ });
+ const tinyIdx = seriesKeys
+ .map((_, index) => index)
+ .filter((index) => Number.isFinite(row[seriesKeys[index]])
+ && row[seriesKeys[index]] > 0
+ && row[seriesKeys[index]] * pxPerUnit < minInsidePx);
+ if (tinyIdx.length === 0) return {};
+ const ys = tinyIdx.map((index) => centers[index]);
+ for (let k = 1; k < ys.length; k += 1) ys[k] = Math.max(ys[k], ys[k - 1] + gap);
+ if (tinyIdx.length === seriesKeys.length) {
+ // 整根柱都是矮段:扇形整体对中到柱身中部,避免标签飘离柱顶
+ const shift = cum * pxPerUnit / 2 - (ys[0] + ys[ys.length - 1]) / 2;
+ for (let k = 0; k < ys.length; k += 1) ys[k] += shift;
+ if (ys[0] < 2) {
+ for (let k = 0; k < ys.length; k += 1) ys[k] += 2 - ys[0];
+ }
+ }
+ const offsets = {};
+ tinyIdx.forEach((index, k) => { offsets[index] = centers[index] - ys[k]; });
+ return offsets;
+ });
+}
+
+/**
+ * 归一化堆叠占比图的逐点标签:段高够时段内显示(用系列的段内配色),
+ * 矮段外置到柱体右侧(深字白描边),保证每个分类的占比数值都可见。
+ * 必须配合 fanTinyLabelOffsets 计算的 dy 使用;value 为空或 0 时不渲染该段。
+ */
+export function stackedSharePoint(value, { pxPerUnit, labelColor, dy = 0, minInsidePx = 15 }) {
+ if (!Number.isFinite(value) || value === 0) return null;
+ if (value * pxPerUnit >= minInsidePx) {
+ return { value, label: { position: 'inside', color: labelColor } };
+ }
+ return {
+ value,
+ label: {
+ position: 'right',
+ color: PALETTE.ink,
+ distance: 3,
+ offset: [0, dy],
+ textBorderColor: '#fff',
+ textBorderWidth: 2,
+ },
+ };
+}
+
/** 底部注释 */
export function footnote(text) {
return {
diff --git a/manifest.json b/manifest.json
index 0b10ebd..cae879e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -28,6 +28,10 @@
"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-channel-share-china": { "key": "ppmt-channel-share-china", "title": "中国区渠道收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_share_china.csv", "annual": "data/ppmt/ppmt_channel_share_china_annual.csv", "halfyear": "data/ppmt/ppmt_channel_share_china_halfyear.csv" }, "height": 680, "filename": "泡泡玛特中国区渠道收入占比" },
+ "ppmt-channel-share-apac": { "key": "ppmt-channel-share-apac", "title": "亚太区渠道收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_share_apac.csv", "annual": "data/ppmt/ppmt_channel_share_apac_annual.csv", "halfyear": "data/ppmt/ppmt_channel_share_apac_halfyear.csv" }, "height": 680, "filename": "泡泡玛特亚太区渠道收入占比" },
+ "ppmt-channel-share-americas": { "key": "ppmt-channel-share-americas", "title": "美洲区渠道收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_share_americas.csv", "annual": "data/ppmt/ppmt_channel_share_americas_annual.csv", "halfyear": "data/ppmt/ppmt_channel_share_americas_halfyear.csv" }, "height": 680, "filename": "泡泡玛特美洲区渠道收入占比" },
+ "ppmt-channel-share-europe": { "key": "ppmt-channel-share-europe", "title": "欧洲及其他区渠道收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_share_europe.csv", "annual": "data/ppmt/ppmt_channel_share_europe_annual.csv", "halfyear": "data/ppmt/ppmt_channel_share_europe_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": "泡泡玛特各地区零售店单店收入" },
@@ -41,10 +45,20 @@
{ "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-channel-share", "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-channel-share-china", "ppmt-channel-share-apac", "ppmt-channel-share-americas", "ppmt-channel-share-europe", "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"] }
]
},
+ {
+ "id": "brk",
+ "name": "巴菲特致股东信",
+ "chartCatalog": {
+ "brk-holdings": { "key": "brk-holdings", "title": "1988–1997 年主要持仓变动", "csv": "data/brk/brk_holdings.csv", "height": 860, "filename": "伯克希尔1988-1997主要持仓变动" }
+ },
+ "tabs": [
+ { "id": "holdings", "name": "主要持仓", "chartRefs": ["brk-holdings"] }
+ ]
+ },
{
"id": "demo",
"name": "示例",
diff --git a/scripts/normalize_brk_holdings.mjs b/scripts/normalize_brk_holdings.mjs
new file mode 100644
index 0000000..73c22b4
--- /dev/null
+++ b/scripts/normalize_brk_holdings.mjs
@@ -0,0 +1,95 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
+const sourcePath = path.resolve('data/brk/rawData/brk_holdings_1988_1997_export.csv');
+const outputPath = path.resolve('data/brk/brk_holdings.csv');
+
+const COMPANY_NAMES = [
+ ['american express|美国运通', '美国运通(American Express)'],
+ ['coca-cola|可口可乐', '可口可乐(Coca-Cola)'],
+ ['disney|迪士尼', '华特迪士尼(Walt Disney)'],
+ ['freddie mac|房地美|联邦住', '房地美(Freddie Mac)'],
+ ['gillette|吉列', '吉列(Gillette)'],
+ ['travelers|旅行家', '旅行家集团(Travelers Group)'],
+ ['washington post|华盛顿邮报', '华盛顿邮报(Washington Post)'],
+ ['wells fargo|富国银行', '富国银行(Wells Fargo)'],
+ ['capital cities|大都会', '大都会/ABC(Capital Cities/ABC)'],
+ ['geico|盖可', '盖可保险(GEICO)'],
+ ['gannett|甘尼特', '甘尼特(Gannett)'],
+ ['pnc', 'PNC 银行(PNC Bank)'],
+ ['general dynamics|通用动力', '通用动力(General Dynamics)'],
+ ['guinness|健力士', '健力士(Guinness)'],
+ ["mcdonald|麦当劳", '麦当劳(McDonald\'s)'],
+].map(([pattern, name]) => ({ pattern: new RegExp(pattern, 'i'), name }));
+
+function parseCSV(text) {
+ const rows = [];
+ let row = [];
+ let value = '';
+ let quoted = false;
+
+ for (let i = 0; i < text.length; i += 1) {
+ const char = text[i];
+ if (char === '"') {
+ if (quoted && text[i + 1] === '"') {
+ value += '"';
+ i += 1;
+ } else {
+ quoted = !quoted;
+ }
+ } else if (char === ',' && !quoted) {
+ row.push(value.trim());
+ value = '';
+ } else if ((char === '\n' || char === '\r') && !quoted) {
+ if (char === '\r' && text[i + 1] === '\n') i += 1;
+ row.push(value.trim());
+ rows.push(row);
+ row = [];
+ value = '';
+ } else {
+ value += char;
+ }
+ }
+ if (value || row.length) rows.push([...row, value.trim()]);
+ return rows;
+}
+
+function canonicalName(value) {
+ return COMPANY_NAMES.find(({ pattern }) => pattern.test(value))?.name || value.trim();
+}
+
+function parseShares(value) {
+ const digits = value.replace(/[^\d]/g, '');
+ return digits ? Number(digits) : NaN;
+}
+
+const rows = parseCSV(await fs.readFile(sourcePath, 'utf8'));
+const holdings = [];
+
+for (let index = 0; index < rows.length; index += 1) {
+ const dateMatch = rows[index][0]?.match(/^(\d{4})\/12\/31$/);
+ if (!dateMatch) continue;
+
+ const year = Number(dateMatch[1]);
+ const header = rows[index + 1] || [];
+ const sharesFirst = /股(份)?数量|股数|持股数量/.test(header[0] || '');
+ const companyIndex = sharesFirst ? 1 : 0;
+ const sharesIndex = sharesFirst ? 0 : 1;
+
+ for (let rowIndex = index + 2; rowIndex < rows.length; rowIndex += 1) {
+ const record = rows[rowIndex];
+ if (!record.some(Boolean)) break;
+ if (/^\d{4}\/12\/31$/.test(record[0] || '')) break;
+ const company = record[companyIndex];
+ const shares = parseShares(record[sharesIndex] || '');
+ if (!company || !Number.isFinite(shares)) continue;
+ holdings.push({ year, company: canonicalName(company), shares });
+ }
+}
+
+holdings.sort((a, b) => a.year - b.year || a.company.localeCompare(b.company, 'zh-CN'));
+const lines = ['year,company,shares', ...holdings.map((row) => `${row.year},${row.company},${row.shares}`)];
+await fs.mkdir(path.dirname(outputPath), { recursive: true });
+await fs.writeFile(outputPath, `${lines.join('\n')}\n`, 'utf8');
+
+console.log(`Normalized ${holdings.length} holdings across ${new Set(holdings.map((row) => row.year)).size} years.`);
diff --git a/scripts/normalize_ppmt_financials.mjs b/scripts/normalize_ppmt_financials.mjs
index 2cbf922..d49f3d3 100644
--- a/scripts/normalize_ppmt_financials.mjs
+++ b/scripts/normalize_ppmt_financials.mjs
@@ -714,10 +714,24 @@ const CHANNEL_NAME_TO_KEY = {
线上渠道: 'online',
批发及其他: 'wholesale',
};
+const ONLINE_SUB_NAME_TO_KEY = {
+ 泡泡玛特抽盒机: 'roboshop',
+ 天猫旗舰店: 'tmall',
+ 抖音平台: 'douyin',
+ 自研APP及官方网站: 'own_app',
+ Shopee: 'shopee',
+ TikTok: 'tiktok',
+ 亚马逊: 'amazon',
+ 其他线上渠道: 'online_other',
+};
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 (!key) {
+ // 线上细分行只进分地区图;未知渠道仍视为底表错误。
+ if (!ONLINE_SUB_NAME_TO_KEY[channel]) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
+ return;
+ }
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 });
@@ -745,3 +759,66 @@ writeRuntimeViews('ppmt_channel_share', ['period', 'retail', 'online', 'robot_st
});
console.log('已生成渠道收入占比的全部、年度、半年 CSV。');
+
+// 分地区渠道收入占比:线上渠道按披露细分拆开,各地区细分不同。
+// 底表中「线上渠道」合计行仅作核对值;细分空缺期已按 0 录入并核对
+// (美洲区 2026H1 起 TikTok 未再产生收入、亚马逊 2026 年起开售,与线上合计勾稽一致)。
+const REGION_KEY_BY_NAME = { 中国: 'china', 亚太: 'apac', 美洲: 'americas', 欧洲及其他: 'europe' };
+const REGION_SERIES = {
+ china: ['retail', 'robot_store', 'roboshop', 'tmall', 'douyin', 'online_other', 'wholesale'],
+ apac: ['retail', 'robot_store', 'own_app', 'shopee', 'online_other', 'wholesale'],
+ americas: ['retail', 'robot_store', 'own_app', 'tiktok', 'amazon', 'online_other', 'wholesale'],
+ europe: ['retail', 'robot_store', 'own_app', 'online_other', 'wholesale'],
+};
+const TOP_AND_SUB_NAME_TO_KEY = { ...CHANNEL_NAME_TO_KEY, ...ONLINE_SUB_NAME_TO_KEY };
+const regionAmounts = new Map();
+const regionOnlineDisclosed = new Map();
+readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
+ const regionKey = REGION_KEY_BY_NAME[region];
+ if (!regionKey) throw new Error(`地区渠道收入底表存在未知地区:${region}`);
+ if (channel === '线上渠道') {
+ regionOnlineDisclosed.set(`${regionKey}:${period}`, revenue);
+ return;
+ }
+ const key = TOP_AND_SUB_NAME_TO_KEY[channel];
+ if (!key) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
+ if (!period || !Number.isFinite(revenue)) throw new Error(`地区渠道收入底表行不完整:${region} ${channel} ${period} ${revenue}`);
+ const bucketKey = `${regionKey}:${period}`;
+ if (!regionAmounts.has(bucketKey)) regionAmounts.set(bucketKey, {});
+ const bucket = regionAmounts.get(bucketKey);
+ bucket[key] = (bucket[key] ?? 0) + revenue;
+});
+regionOnlineDisclosed.forEach((disclosed, bucketKey) => {
+ const bucket = regionAmounts.get(bucketKey);
+ if (!bucket) throw new Error(`地区渠道收入底表缺少线上合计对应的期间:${bucketKey}`);
+ const subSum = Object.values(ONLINE_SUB_NAME_TO_KEY).reduce((sum, key) => sum + (bucket[key] ?? 0), 0);
+ if (Math.abs(subSum - disclosed) > 0.05) {
+ throw new Error(`${bucketKey} 线上细分合计 ${subSum.toFixed(2)} ≠ 披露线上渠道 ${disclosed.toFixed(2)}`);
+ }
+});
+
+Object.entries(REGION_SERIES).forEach(([regionKey, seriesKeys]) => {
+ const amountViews = buildViews((year, type) => {
+ const bucket = regionAmounts.get(`${regionKey}:${type === 'H1' ? `${year}H1` : `${year}FY`}`);
+ if (!bucket) return null;
+ const values = Object.fromEntries(seriesKeys.map((key) => [key, bucket[key] ?? 0]));
+ return isComplete(values) ? values : null;
+ });
+ const shareViews = Object.fromEntries(Object.entries(amountViews).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_${regionKey}`, ['period', ...seriesKeys, 'total_revenue'], shareViews, {
+ ...Object.fromEntries(seriesKeys.map((key) => [key, 1])),
+ total_revenue: 2,
+ });
+});
+
+console.log('已生成四地区渠道收入占比(线上细分)的全部、年度、半年 CSV。');