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