feat: 增加不同地区各渠道收入占比图
This commit is contained in:
@@ -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
|
||||||
|
@@ -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 ",,,,,,,,
|
||||||
|
Binary file not shown.
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -14,6 +14,26 @@ region,channel,period,revenue
|
|||||||
中国,线上渠道,2025H1,29.37
|
中国,线上渠道,2025H1,29.37
|
||||||
中国,线上渠道,2025FY,85.22
|
中国,线上渠道,2025FY,85.22
|
||||||
中国,线上渠道,2026H1,47.79
|
中国,线上渠道,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
|
中国,批发及其他,2024H1,2.39
|
||||||
中国,批发及其他,2024FY,7.63
|
中国,批发及其他,2024FY,7.63
|
||||||
中国,批发及其他,2025H1,2.61
|
中国,批发及其他,2025H1,2.61
|
||||||
@@ -34,6 +54,21 @@ region,channel,period,revenue
|
|||||||
亚太,线上渠道,2025H1,10.69
|
亚太,线上渠道,2025H1,10.69
|
||||||
亚太,线上渠道,2025FY,30.53
|
亚太,线上渠道,2025FY,30.53
|
||||||
亚太,线上渠道,2026H1,6.44
|
亚太,线上渠道,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
|
亚太,批发及其他,2024H1,1.29
|
||||||
亚太,批发及其他,2024FY,4.30
|
亚太,批发及其他,2024FY,4.30
|
||||||
亚太,批发及其他,2025H1,2.56
|
亚太,批发及其他,2025H1,2.56
|
||||||
@@ -54,6 +89,26 @@ region,channel,period,revenue
|
|||||||
美洲,线上渠道,2025H1,13.27
|
美洲,线上渠道,2025H1,13.27
|
||||||
美洲,线上渠道,2025FY,43.54
|
美洲,线上渠道,2025FY,43.54
|
||||||
美洲,线上渠道,2026H1,7.22
|
美洲,线上渠道,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
|
美洲,批发及其他,2024H1,0.19
|
||||||
美洲,批发及其他,2024FY,0.58
|
美洲,批发及其他,2024FY,0.58
|
||||||
美洲,批发及其他,2025H1,0.96
|
美洲,批发及其他,2025H1,0.96
|
||||||
@@ -74,6 +129,16 @@ region,channel,period,revenue
|
|||||||
欧洲及其他,线上渠道,2025H1,1.65
|
欧洲及其他,线上渠道,2025H1,1.65
|
||||||
欧洲及其他,线上渠道,2025FY,5.05
|
欧洲及其他,线上渠道,2025FY,5.05
|
||||||
欧洲及其他,线上渠道,2026H1,0.68
|
欧洲及其他,线上渠道,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
|
欧洲及其他,批发及其他,2024H1,0.05
|
||||||
欧洲及其他,批发及其他,2024FY,0.25
|
欧洲及其他,批发及其他,2024FY,0.25
|
||||||
欧洲及其他,批发及其他,2025H1,0.36
|
欧洲及其他,批发及其他,2025H1,0.36
|
||||||
|
|||||||
|
@@ -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 = [`<b>${year} · ${company}</b>`, `状态:${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('<br/>');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
|||||||
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
||||||
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
||||||
import { channelShareChart } from './ppmt_channel_share.js';
|
import { channelShareChart } from './ppmt_channel_share.js';
|
||||||
|
import { makeRegionChannelShareChart } from './ppmt_channel_share_region.js';
|
||||||
import { storesRetailChart } from './ppmt_stores_retail.js';
|
import { storesRetailChart } from './ppmt_stores_retail.js';
|
||||||
import { storesRobotChart } from './ppmt_stores_robot.js';
|
import { storesRobotChart } from './ppmt_stores_robot.js';
|
||||||
import { regionShareChart } from './ppmt_region_share.js';
|
import { regionShareChart } from './ppmt_region_share.js';
|
||||||
@@ -33,6 +34,7 @@ import { inventoryBalanceChart } from './ppmt_inventory.js';
|
|||||||
import { storesRetailShareChart } from './ppmt_stores_retail_share.js';
|
import { storesRetailShareChart } from './ppmt_stores_retail_share.js';
|
||||||
import { retailStoreRevenueChart } from './ppmt_retail_store_revenue.js';
|
import { retailStoreRevenueChart } from './ppmt_retail_store_revenue.js';
|
||||||
import { robotStoreRevenueChart } from './ppmt_robot_store_revenue.js';
|
import { robotStoreRevenueChart } from './ppmt_robot_store_revenue.js';
|
||||||
|
import { brkHoldingsChart } from './brk_holdings.js';
|
||||||
|
|
||||||
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
||||||
export const chartRegistry = {
|
export const chartRegistry = {
|
||||||
@@ -64,6 +66,10 @@ export const chartRegistry = {
|
|||||||
'ppmt-expense-structure': expenseStructureChart,
|
'ppmt-expense-structure': expenseStructureChart,
|
||||||
'ppmt-channel-revenue': channelRevenueChart,
|
'ppmt-channel-revenue': channelRevenueChart,
|
||||||
'ppmt-channel-share': channelShareChart,
|
'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-retail': storesRetailChart,
|
||||||
'ppmt-stores-robot': storesRobotChart,
|
'ppmt-stores-robot': storesRobotChart,
|
||||||
'ppmt-region-share': regionShareChart,
|
'ppmt-region-share': regionShareChart,
|
||||||
@@ -71,6 +77,7 @@ export const chartRegistry = {
|
|||||||
'ppmt-stores-retail-share': storesRetailShareChart,
|
'ppmt-stores-retail-share': storesRetailShareChart,
|
||||||
'ppmt-retail-store-revenue': retailStoreRevenueChart,
|
'ppmt-retail-store-revenue': retailStoreRevenueChart,
|
||||||
'ppmt-robot-store-revenue': robotStoreRevenueChart,
|
'ppmt-robot-store-revenue': robotStoreRevenueChart,
|
||||||
|
'brk-holdings': brkHoldingsChart,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getChartBuilder(key) {
|
export function getChartBuilder(key) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
|
financialViewLabel, barSeries, stackedSharePoint, fanTinyLabelOffsets, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
const CHANNELS = [
|
const CHANNELS = [
|
||||||
@@ -11,21 +11,14 @@ const CHANNELS = [
|
|||||||
{ key: 'wholesale', name: '批发及其他', color: CHANNEL_COLORS.wholesale, labelColor: PALETTE.ink },
|
{ 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%。 */
|
/** 泡泡玛特全渠道收入占比:全部地区合并,每根柱按当期总收入归一化为 100%。 */
|
||||||
export function channelShareChart(rows, { width, height, view = 'all' }) {
|
export function channelShareChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
const barWidth = view === 'all' ? 48 : 80;
|
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 {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
@@ -59,9 +52,13 @@ export function channelShareChart(rows, { width, height, view = 'all' }) {
|
|||||||
interval: 20,
|
interval: 20,
|
||||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
},
|
},
|
||||||
series: CHANNELS.map((item) => barSeries({
|
series: CHANNELS.map((item, ci) => barSeries({
|
||||||
name: item.name,
|
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,
|
color: item.color,
|
||||||
barWidth,
|
barWidth,
|
||||||
stack: 'channel-share',
|
stack: 'channel-share',
|
||||||
|
|||||||
@@ -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 `<b>${params[0].axisValue}</b> ${region.source}总收入 ${row.total_revenue.toFixed(2)} 亿元<br/>${params
|
||||||
|
.filter((item) => item.value !== null)
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((item) => `${item.marker}${item.seriesName}: ${item.value.toFixed(1)}%`)
|
||||||
|
.join('<br/>')}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 100 },
|
||||||
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
|
yAxis: {
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
interval: 20,
|
||||||
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
|
},
|
||||||
|
series: 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),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
|
financialViewLabel, barSeries, stackedSharePoint, fanTinyLabelOffsets, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特分地区零售门店占比:每根柱标准化为各期末全球门店数的 100%。 */
|
/** 泡泡玛特分地区零售门店占比:每根柱标准化为各期末全球门店数的 100%。 */
|
||||||
@@ -13,19 +13,13 @@ const REGIONS = [
|
|||||||
{ key: 'other', name: '其他', color: PALETTE.sand, labelColor: PALETTE.ink },
|
{ 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' }) {
|
export function storesRetailShareChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
const barWidth = view === 'all' ? 48 : 80;
|
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 {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
@@ -59,9 +53,13 @@ export function storesRetailShareChart(rows, { width, height, view = 'all' }) {
|
|||||||
interval: 20,
|
interval: 20,
|
||||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
},
|
},
|
||||||
series: REGIONS.map((item) => barSeries({
|
series: REGIONS.map((item, ci) => barSeries({
|
||||||
name: item.name,
|
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,
|
color: item.color,
|
||||||
barWidth,
|
barWidth,
|
||||||
stack: 'stores-retail-share',
|
stack: 'stores-retail-share',
|
||||||
|
|||||||
+61
@@ -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) {
|
export function footnote(text) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+15
-1
@@ -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-expense-structure": { "key": "ppmt-expense-structure", "title": "费用结构与费用率", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_expense_structure.csv", "annual": "data/ppmt/ppmt_expense_structure_annual.csv", "halfyear": "data/ppmt/ppmt_expense_structure_halfyear.csv" }, "height": 700, "filename": "泡泡玛特费用结构与费用率" },
|
||||||
"ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_revenue.csv", "annual": "data/ppmt/ppmt_channel_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_channel_revenue_halfyear.csv" }, "height": 720, "filename": "泡泡玛特渠道收入结构与增速" },
|
"ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_revenue.csv", "annual": "data/ppmt/ppmt_channel_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_channel_revenue_halfyear.csv" }, "height": 720, "filename": "泡泡玛特渠道收入结构与增速" },
|
||||||
"ppmt-channel-share": { "key": "ppmt-channel-share", "title": "渠道收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_share.csv", "annual": "data/ppmt/ppmt_channel_share_annual.csv", "halfyear": "data/ppmt/ppmt_channel_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特渠道收入占比" },
|
"ppmt-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": { "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-stores-retail-share": { "key": "ppmt-stores-retail-share", "title": "分地区零售门店数量占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_retail_share.csv", "annual": "data/ppmt/ppmt_stores_retail_share_annual.csv", "halfyear": "data/ppmt/ppmt_stores_retail_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区零售门店数量占比" },
|
||||||
"ppmt-retail-store-revenue": { "key": "ppmt-retail-store-revenue", "title": "各地区零售店单店收入", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_retail_store_revenue.csv", "annual": "data/ppmt/ppmt_retail_store_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_retail_store_revenue_halfyear.csv" }, "height": 680, "filename": "泡泡玛特各地区零售店单店收入" },
|
"ppmt-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": "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": "balance-sheet", "name": "资产负债", "chartRefs": ["ppmt-assets", "ppmt-inventory", "ppmt-liquidity-working-capital", "ppmt-working-capital-days"] },
|
||||||
{ "id": "ip-category", "name": "IP 与品类", "chartRefs": ["ppmt-ip", "ppmt-ip-share", "ppmt-ip-share-history", "ppmt-ip-ex-monsters", "ppmt-category"] },
|
{ "id": "ip-category", "name": "IP 与品类", "chartRefs": ["ppmt-ip", "ppmt-ip-share", "ppmt-ip-share-history", "ppmt-ip-ex-monsters", "ppmt-category"] },
|
||||||
{ "id": "region-channel", "name": "区域渠道", "chartRefs": ["ppmt-region-channel", "ppmt-region-yoy", "ppmt-region-yoy-timeseries", "ppmt-channel-revenue", "ppmt-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": "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",
|
"id": "demo",
|
||||||
"name": "示例",
|
"name": "示例",
|
||||||
|
|||||||
@@ -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.`);
|
||||||
@@ -714,10 +714,24 @@ const CHANNEL_NAME_TO_KEY = {
|
|||||||
线上渠道: 'online',
|
线上渠道: 'online',
|
||||||
批发及其他: 'wholesale',
|
批发及其他: 'wholesale',
|
||||||
};
|
};
|
||||||
|
const ONLINE_SUB_NAME_TO_KEY = {
|
||||||
|
泡泡玛特抽盒机: 'roboshop',
|
||||||
|
天猫旗舰店: 'tmall',
|
||||||
|
抖音平台: 'douyin',
|
||||||
|
自研APP及官方网站: 'own_app',
|
||||||
|
Shopee: 'shopee',
|
||||||
|
TikTok: 'tiktok',
|
||||||
|
亚马逊: 'amazon',
|
||||||
|
其他线上渠道: 'online_other',
|
||||||
|
};
|
||||||
const channelRevenueByPeriod = new Map();
|
const channelRevenueByPeriod = new Map();
|
||||||
readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
|
readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
|
||||||
const key = CHANNEL_NAME_TO_KEY[channel];
|
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 (!period || !Number.isFinite(revenue)) throw new Error(`地区渠道收入底表行不完整:${region} ${channel} ${period} ${revenue}`);
|
||||||
if (!channelRevenueByPeriod.has(period)) {
|
if (!channelRevenueByPeriod.has(period)) {
|
||||||
channelRevenueByPeriod.set(period, { retail: 0, online: 0, robot_store: 0, wholesale: 0 });
|
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。');
|
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。');
|
||||||
|
|||||||
Reference in New Issue
Block a user